import type { Database } from '@db/sqlite'; import * as nostrTools from '@nostr/tools'; import { nip44 } from '@nostr/tools'; import { MAX_CHUNK_SIZE, MIN_POW, POW_TO_MINE } from './consts.ts'; import { getCCNPrivateKey, getCCNPubkey, randomTimeUpTo2DaysInThePast, } from './utils.ts'; import { sql } from './utils/queries.ts'; export class EventAlreadyExistsException extends Error {} export class ChunkedEventReceived extends Error {} export async function createEncryptedEvent( event: nostrTools.VerifiedEvent, ): Promise { if (!event.id) throw new Error('Event must have an ID'); if (!event.sig) throw new Error('Event must be signed'); const ccnPubKey = await getCCNPubkey(); const ccnPrivateKey = await getCCNPrivateKey(); const eventJson = JSON.stringify(event); if (eventJson.length <= MAX_CHUNK_SIZE) { const randomPrivateKey = nostrTools.generateSecretKey(); const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey); const conversationKey = nip44.getConversationKey( randomPrivateKey, ccnPubKey, ); const sealTemplate = { kind: 13, created_at: randomTimeUpTo2DaysInThePast(), content: nip44.encrypt(eventJson, conversationKey), tags: [], }; const seal = nostrTools.finalizeEvent(sealTemplate, ccnPrivateKey); const giftWrapTemplate = { kind: 1059, created_at: randomTimeUpTo2DaysInThePast(), content: nip44.encrypt(JSON.stringify(seal), conversationKey), tags: [['p', ccnPubKey]], pubkey: randomPrivateKeyPubKey, }; const minedGiftWrap = nostrTools.nip13.minePow( giftWrapTemplate, POW_TO_MINE, ); const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey); return giftWrap; } const chunks: string[] = []; for (let i = 0; i < eventJson.length; i += MAX_CHUNK_SIZE) chunks.push(eventJson.slice(i, i + MAX_CHUNK_SIZE)); const messageId = crypto.randomUUID(); const totalChunks = chunks.length; const encryptedChunks = []; for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; const randomPrivateKey = nostrTools.generateSecretKey(); const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey); const conversationKey = nip44.getConversationKey( randomPrivateKey, ccnPubKey, ); const sealTemplate = { kind: 13, created_at: randomTimeUpTo2DaysInThePast(), content: nip44.encrypt(chunk, conversationKey), tags: [['chunk', String(i), String(totalChunks), messageId]], }; const seal = nostrTools.finalizeEvent(sealTemplate, ccnPrivateKey); const giftWrapTemplate = { kind: 1059, created_at: randomTimeUpTo2DaysInThePast(), content: nip44.encrypt(JSON.stringify(seal), conversationKey), tags: [['p', ccnPubKey]], pubkey: randomPrivateKeyPubKey, }; const minedGiftWrap = nostrTools.nip13.minePow( giftWrapTemplate, POW_TO_MINE, ); encryptedChunks.push( nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey), ); } return encryptedChunks; } export async function decryptEvent( db: Database, event: nostrTools.Event, ): Promise { const ccnPrivkey = await getCCNPrivateKey(); if (event.kind !== 1059) { throw new Error('Cannot decrypt event -- not a gift wrap'); } const pow = nostrTools.nip13.getPow(event.id); if (pow < MIN_POW) { throw new Error('Cannot decrypt event -- PoW too low'); } const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey); const seal = JSON.parse(nip44.decrypt(event.content, conversationKey)); if (!seal) throw new Error('Cannot decrypt event -- no seal'); if (seal.kind !== 13) { throw new Error('Cannot decrypt event subevent -- not a seal'); } const chunkTag = seal.tags.find((tag: string[]) => tag[0] === 'chunk'); if (!chunkTag) { const content = JSON.parse(nip44.decrypt(seal.content, conversationKey)); return content as nostrTools.VerifiedEvent; } const [_, chunkIndex, totalChunks, messageId] = chunkTag; const chunk = nip44.decrypt(seal.content, conversationKey); try { sql` INSERT INTO event_chunks ( message_id, chunk_index, total_chunks, chunk_data, conversation_key, created_at ) VALUES ( ${messageId}, ${Number(chunkIndex)}, ${Number(totalChunks)}, ${chunk}, ${conversationKey}, ${Math.floor(Date.now() / 1000)} ) `(db); const chunks = sql` SELECT chunk_data FROM event_chunks WHERE message_id = ${messageId} ORDER BY chunk_index ASC `(db); if (chunks.length === Number(totalChunks)) { const completeEventJson = chunks.map((c) => c.chunk_data).join(''); sql`DELETE FROM event_chunks WHERE message_id = ${messageId}`(db); return JSON.parse(completeEventJson) as nostrTools.VerifiedEvent; } throw new ChunkedEventReceived( `Chunked event received (${chunks.length}/${totalChunks}) - messageId: ${messageId}`, ); } catch (e) { if (e instanceof Error && e.message.includes('UNIQUE constraint failed')) throw new Error( `Duplicate chunk received (${Number(chunkIndex) + 1}/${totalChunks}) - messageId: ${messageId}`, ); throw e; } }