import type { IpnsString } from '@wovin/core/applog' import type { IShare, ISubscription } from '@wovin/core/pubsub' import { dateNowIso } from '@wovin/core/applog' import { flow } from '@wovin/core/mobx' import { arrayBufferToBase64 } from '@wovin/utils' import { Logger } from 'besonders-logger' import { keys } from 'libp2p-crypto' import * as W3Name from 'w3name' import { createAESkeyForShare, derivePublicationKeySeed, deriveSharedEncryptionKey } from '../data/agent/AgentCrypto' import { useAgent } from '../data/agent/AgentState' import { getSubOrShare } from '../data/agent/utils-agent' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.DEBUG, { prefix: '[sub-pub]' }) export const upsertSubscription = flow(function* upsertSubscription( ipns: IpnsString, subFields: & Partial> & { encryptedBy?: string, derivationJWKstring?: string } = {}, ) { LOG('Upserting subscription:', { ipns, subFields }) const agent = useAgent() const { ag } = agent const subscriptionObj: ISubscription = { id: ipns, createdAt: dateNowIso(), name: ipns.slice(-7), isDeleted: false, lastPull: null, autopull: false, ...subFields, } if (subFields.encryptedBy && subFields.derivationJWKstring) { let privateKey: CryptoKey, remotePubDerivationKey: CryptoKey try { privateKey = (yield agent.getDerivationKeypair()).privateKey } catch (error) { throw new Error('failed to get private key') } try { remotePubDerivationKey = yield importPublicDerivationKey(subFields.derivationJWKstring) subscriptionObj.encryptedWith = yield deriveSharedEncryptionKey(privateKey, remotePubDerivationKey, true) // doTestEncryptDecrypt(subscriptionObj) // ! This derived key works in the test... but not with the payloads from ipfs } catch (error) { throw new Error('failed to importPublicDerivationKey') } subscriptionObj.publishedBy = subFields.encryptedBy subscriptionObj.encryptedFor = ag subscriptionObj.name = `${subFields.encryptedBy} => ${ag}` } if (agent.subscriptions.find(s => s.id === subscriptionObj.id)) { yield agent.updateSub(subscriptionObj.id, subscriptionObj as ISubscription) // update the subscription } else { yield agent.addSub(subscriptionObj as ISubscription) // insert the subscription } DEBUG('[upsertSubscription]', subscriptionObj) return subscriptionObj }) export const addShare = flow(function* addShare( pubFields: Partial> & { derivationJWKstring?: string } = {}, ) { const agent = useAgent() const { ag: publishedBy, getDerivationKeypair } = agent const { privateKey, publicKey } = (yield getDerivationKeypair()) as CryptoKeyPair const counterFromLocalStorage = localStorage.getItem('agent.pubCounter') const pubCounter = counterFromLocalStorage ? +counterFromLocalStorage + 1 : 1 localStorage.setItem('agent.pubCounter', pubCounter.toFixed(0)) LOG('Adding share:', { publishedBy, pubFields }) const generateEd25519KeyPairFromSeed = keys.supportedKeys.ed25519.generateKeyPairFromSeed DEBUG('creating new pubkey derivation via mnemkey') const newPKseed: Uint8Array = yield derivePublicationKeySeed({ privateKey, publicKey }, pubCounter) VERBOSE({ newPKseed }) const newPriEd25519 = yield generateEd25519KeyPairFromSeed(newPKseed) VERBOSE({ newPriEd25519 }) // DEBUG('creating new WritableName') // Use W3Name.from() to properly convert from old libp2p-crypto format to new @libp2p/crypto format const newName: W3Name.WritableName = yield W3Name.from(newPriEd25519.marshal()) DEBUG('new pubkey:', { newPKseed, newPriEd25519, newName }) const { name = `pub ${newName.toString().slice(-7)}`, encryptedFor, derivationJWKstring } = pubFields // Store raw key bytes (not protobuf-encoded) so W3Name.from() can reconstruct it later const pk = newPriEd25519.marshal() const newShare: IShare = { id: newName.toString(), name, pk, publishedBy, pubCounter, createdAt: dateNowIso(), lastPush: null, autopush: false, } if (encryptedFor && derivationJWKstring) { newShare.name = `${newShare.name}=>${encryptedFor}` newShare.encryptedFor = encryptedFor const remotePubDerivationKey = yield importPublicDerivationKey(derivationJWKstring) newShare.encryptedWith = yield deriveSharedEncryptionKey(privateKey, remotePubDerivationKey, true) } DEBUG('[addShare]', { newShare, newName }) yield agent.addShare(newShare) return newShare }) export const updateShareEncryption = flow(async function* updateShareEncryption( pubFields: Partial> & { derivationJWKstring?: string } = {}, ) { LOG('Updating share encryption:', pubFields) const newName: W3Name.WritableName = yield W3Name.create() // ? new ipns name when updating encryption? // TODO deterministic const agent = useAgent() const { ag: publishedBy, getKnownAgents, crypto } = agent const { privateKey } = yield agent.getDerivationKeypair() const { id, encryptedFor, sharedAgents } = pubFields const pk = newName.key.bytes const existingShare = getSubOrShare(pubFields.id) as IShare const shareUpdate: Partial = {} const knownAgentsMap = getKnownAgents().get() if (sharedAgents) { // multiple agents shareUpdate.encryptedFor = null shareUpdate.encryptedWith = null shareUpdate.sharedAgents = sharedAgents // const unifiedSharedAESkey = yield createMultiAgentEncryptionKey() // non deterministic const unifiedSharedAESkey = yield createAESkeyForShare(existingShare) DEBUG({ unifiedSharedAESkey }) const exportedKey = yield globalThis.crypto.subtle.exportKey('raw', unifiedSharedAESkey) shareUpdate.sharedKey = unifiedSharedAESkey // ? here we could reimport the shared key as not extractable // then WebCrypto would hide it (but its futile for reasons:) // 1. if its not extractable then we can't export it to reincrypt it for others // 2. if thats the case, we might as well not save it in IDB (but only encrypted for ourselves in the applog) // TODO solid strategy for key rotation, saving, sharing, managing const keyBase64 = arrayBufferToBase64(exportedKey) DEBUG('sharedKey', { exportedKey, keyBase64 }) shareUpdate.sharedKeyMap = new Map() for (const eachAgent of sharedAgents) { const eachJWK = knownAgentsMap.get(eachAgent).jwkd as string if (eachJWK) { const eachPubKey = yield importPublicDerivationKey(eachJWK) const derivedKeyToEncryptSharedKeyWith = yield deriveSharedEncryptionKey(privateKey, eachPubKey, true) const encryptedSharedKey = await crypto?.aes.encrypt(exportedKey, derivedKeyToEncryptSharedKeyWith, 'AES-GCM') shareUpdate.sharedKeyMap.set(eachAgent, arrayBufferToBase64(encryptedSharedKey)) DEBUG('midloop', { shareUpdate }) } else { ERROR('unknown JWK - weirdness', { knownAgentsMap, eachAgent }) } } } else if (encryptedFor) { // single agent WARN('deprecated') // pubUpdate.encryptedFor = encryptedFor // const derivationJWKstring = knownAgentsMap.get(encryptedFor).jwkd as string // if (derivationJWKstring) { // const remotePubDerivationKey = yield importPublicDerivationKey(derivationJWKstring) // pubUpdate.encryptedWith = yield deriveSharedEncryptionKey(privateKey, remotePubDerivationKey, true) // if (pubUpdate.encryptedWith) {} // } else { // } } else { ERROR('encryptedFor unknown', { encryptedFor, knownAgentsMap }) } yield agent.updateShare(id, shareUpdate) const traceObj = encryptedFor ? { encryptedFor } : { sharedAgents } LOG('[updateShare]', { shareUpdate }, traceObj) return shareUpdate }) export async function importPublicDerivationKey(derivationJWKstring: string) { const parsedPublicJWK = JSON.parse(derivationJWKstring) DEBUG({ derivationJWKstring, parsedPublicJWK }) return await globalThis.crypto.subtle.importKey( 'jwk', parsedPublicJWK, { name: 'ECDH', namedCurve: 'P-256' }, true, [], ) } export async function createMultiAgentEncryptionKey() { const newKey = await globalThis.crypto.subtle.generateKey( { name: 'AES-GCM', length: 256, }, true, ['encrypt', 'decrypt'], ) DEBUG('created new key', { newKey }) return newKey }