✂️ Implement message chunking mechanism for NIP-44 size limit compliance
This ensures all messages can be properly encrypted and transmitted regardless of size. Fixes issue #2
This commit is contained in:
parent
a4134fa416
commit
89d9dc3cbe
4 changed files with 246 additions and 65 deletions
100
index.ts
100
index.ts
|
@ -1,3 +1,5 @@
|
|||
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||
import * as nostrTools from '@nostr/tools';
|
||||
import { Database } from 'jsr:@db/sqlite';
|
||||
import { NSchema as n } from 'jsr:@nostrify/nostrify';
|
||||
import type {
|
||||
|
@ -6,10 +8,13 @@ import type {
|
|||
NostrFilter,
|
||||
} from 'jsr:@nostrify/types';
|
||||
import { encodeBase64 } from 'jsr:@std/encoding@0.224/base64';
|
||||
import { randomBytes } from '@noble/ciphers/webcrypto';
|
||||
import * as nostrTools from '@nostr/tools';
|
||||
import { nip44 } from '@nostr/tools';
|
||||
import { MIN_POW, POW_TO_MINE } from './consts.ts';
|
||||
import { CHUNK_CLEANUP_INTERVAL, CHUNK_MAX_AGE } from './consts.ts';
|
||||
import {
|
||||
ChunkedEventReceived,
|
||||
EventAlreadyExistsException,
|
||||
createEncryptedEvent,
|
||||
decryptEvent,
|
||||
} from './eventEncryptionDecryption.ts';
|
||||
import {
|
||||
getCCNPrivateKey,
|
||||
getCCNPubkey,
|
||||
|
@ -20,7 +25,6 @@ import {
|
|||
isReplaceableEvent,
|
||||
isValidJSON,
|
||||
parseATagQuery,
|
||||
randomTimeUpTo2DaysInThePast,
|
||||
} from './utils.ts';
|
||||
import { getEveFilePath } from './utils/files.ts';
|
||||
import { log, setupLogger } from './utils/logs.ts';
|
||||
|
@ -103,62 +107,6 @@ export function runMigrations(db: Database, latestVersion: number) {
|
|||
}
|
||||
}
|
||||
|
||||
async function createEncryptedEvent(
|
||||
event: nostrTools.VerifiedEvent,
|
||||
): Promise<nostrTools.VerifiedEvent> {
|
||||
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 randomPrivateKey = nostrTools.generateSecretKey();
|
||||
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
|
||||
const conversationKey = nip44.getConversationKey(randomPrivateKey, ccnPubKey);
|
||||
const sealTemplate = {
|
||||
kind: 13,
|
||||
created_at: randomTimeUpTo2DaysInThePast(),
|
||||
content: nip44.encrypt(JSON.stringify(event), 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;
|
||||
}
|
||||
|
||||
async function decryptEvent(
|
||||
event: nostrTools.Event,
|
||||
): Promise<nostrTools.VerifiedEvent> {
|
||||
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 content = JSON.parse(nip44.decrypt(seal.content, conversationKey));
|
||||
return content as nostrTools.VerifiedEvent;
|
||||
}
|
||||
|
||||
class EventAlreadyExistsException extends Error {}
|
||||
|
||||
function addEventToDb(
|
||||
decryptedEvent: nostrTools.VerifiedEvent,
|
||||
encryptedEvent: nostrTools.VerifiedEvent,
|
||||
|
@ -267,6 +215,11 @@ function encryptedEventIsInDb(event: nostrTools.VerifiedEvent) {
|
|||
`(db)[0];
|
||||
}
|
||||
|
||||
function cleanupOldChunks() {
|
||||
const cutoffTime = Math.floor((Date.now() - CHUNK_MAX_AGE) / 1000);
|
||||
sql`DELETE FROM event_chunks WHERE created_at < ${cutoffTime}`(db);
|
||||
}
|
||||
|
||||
async function setupAndSubscribeToExternalEvents() {
|
||||
const ccnPubkey = await getCCNPubkey();
|
||||
|
||||
|
@ -303,11 +256,14 @@ async function setupAndSubscribeToExternalEvents() {
|
|||
return;
|
||||
}
|
||||
if (encryptedEventIsInDb(event)) return;
|
||||
const decryptedEvent = await decryptEvent(event);
|
||||
try {
|
||||
const decryptedEvent = await decryptEvent(db, event);
|
||||
addEventToDb(decryptedEvent, event);
|
||||
} catch (e) {
|
||||
if (e instanceof EventAlreadyExistsException) return;
|
||||
if (e instanceof ChunkedEventReceived) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -338,8 +294,15 @@ async function setupAndSubscribeToExternalEvents() {
|
|||
const encryptedCCNCreationEvent =
|
||||
await createEncryptedEvent(ccnCreationEvent);
|
||||
if (timerCleaned) return; // in case we get an event before the timer is cleaned
|
||||
await Promise.any(pool.publish(relays, encryptedCCNCreationEvent));
|
||||
if (Array.isArray(encryptedCCNCreationEvent)) {
|
||||
for (const event of encryptedCCNCreationEvent)
|
||||
await Promise.any(pool.publish(relays, event));
|
||||
} else {
|
||||
await Promise.any(pool.publish(relays, encryptedCCNCreationEvent));
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
setInterval(cleanupOldChunks, CHUNK_CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
await setupAndSubscribeToExternalEvents();
|
||||
|
@ -644,14 +607,21 @@ async function handleEvent(
|
|||
|
||||
const encryptedEvent = await createEncryptedEvent(event);
|
||||
try {
|
||||
addEventToDb(event, encryptedEvent);
|
||||
if (Array.isArray(encryptedEvent)) {
|
||||
await Promise.all(
|
||||
encryptedEvent.map((chunk) => Promise.any(pool.publish(relays, chunk))),
|
||||
);
|
||||
addEventToDb(event, encryptedEvent[0]);
|
||||
} else {
|
||||
addEventToDb(event, encryptedEvent);
|
||||
await Promise.any(pool.publish(relays, encryptedEvent));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof EventAlreadyExistsException) {
|
||||
log.warn('Event already exists');
|
||||
return;
|
||||
}
|
||||
}
|
||||
await Promise.any(pool.publish(relays, encryptedEvent));
|
||||
|
||||
connection.socket.send(JSON.stringify(['OK', event.id, true, 'Event added']));
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue