✨ Feat: Implement support for multiple CCNs
This commit is contained in:
parent
097f02938d
commit
a8ffce918e
7 changed files with 778 additions and 169 deletions
|
@ -3,10 +3,12 @@ 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,
|
||||
getActiveCCN,
|
||||
getAllCCNs,
|
||||
getCCNPrivateKeyByPubkey,
|
||||
randomTimeUpTo2DaysInThePast,
|
||||
} from './utils.ts';
|
||||
import { None, type Option, Some, flatMap, map } from './utils/option.ts';
|
||||
import { sql } from './utils/queries.ts';
|
||||
|
||||
export class EventAlreadyExistsException extends Error {}
|
||||
|
@ -14,12 +16,16 @@ export class ChunkedEventReceived extends Error {}
|
|||
|
||||
export async function createEncryptedEvent(
|
||||
event: nostrTools.VerifiedEvent,
|
||||
db: Database,
|
||||
): Promise<nostrTools.VerifiedEvent | 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 activeCCN = getActiveCCN(db);
|
||||
if (!activeCCN) throw new Error('No active CCN found');
|
||||
|
||||
const ccnPubKey = activeCCN.pubkey;
|
||||
const ccnPrivateKey = await getCCNPrivateKeyByPubkey(ccnPubKey);
|
||||
|
||||
const eventJson = JSON.stringify(event);
|
||||
if (eventJson.length <= MAX_CHUNK_SIZE) {
|
||||
|
@ -95,12 +101,101 @@ export async function createEncryptedEvent(
|
|||
|
||||
return encryptedChunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to decrypt an event using a specific CCN private key
|
||||
* @returns The decrypted event with CCN pubkey if successful, None otherwise
|
||||
*/
|
||||
function attemptDecryptWithKey(
|
||||
event: nostrTools.Event,
|
||||
ccnPrivkey: Uint8Array,
|
||||
ccnPubkey: string,
|
||||
): Option<nostrTools.VerifiedEvent> {
|
||||
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
||||
const sealResult = map(
|
||||
Some(nip44.decrypt(event.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
|
||||
return flatMap(sealResult, (seal) => {
|
||||
if (!seal || seal.kind !== 13) return None();
|
||||
|
||||
const chunkTag = seal.tags.find((tag: string[]) => tag[0] === 'chunk');
|
||||
if (!chunkTag) {
|
||||
const contentResult = map(
|
||||
Some(nip44.decrypt(seal.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
return map(contentResult, (content) => ({ ...content, ccnPubkey }));
|
||||
}
|
||||
|
||||
return None();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a chunked message by storing it in the database and checking if all chunks are received
|
||||
* @returns The complete decrypted event if all chunks are received, throws ChunkedEventReceived otherwise
|
||||
*/
|
||||
function handleChunkedMessage(
|
||||
db: Database,
|
||||
event: nostrTools.Event,
|
||||
ccnPrivkey: Uint8Array,
|
||||
ccnPubkey: string,
|
||||
): nostrTools.VerifiedEvent {
|
||||
const conversationKey = nip44.getConversationKey(ccnPrivkey, event.pubkey);
|
||||
const sealResult = map(
|
||||
Some(nip44.decrypt(event.content, conversationKey)),
|
||||
JSON.parse,
|
||||
);
|
||||
|
||||
const seal = sealResult.isSome ? sealResult.value : null;
|
||||
if (!seal) {
|
||||
throw new Error('Invalid chunked message format');
|
||||
}
|
||||
|
||||
const chunkTag = seal.tags.find((tag: string[]) => tag[0] === 'chunk');
|
||||
if (!chunkTag) {
|
||||
throw new Error('Invalid chunked message format');
|
||||
}
|
||||
|
||||
const [_, chunkIndex, totalChunks, messageId] = chunkTag;
|
||||
const chunk = nip44.decrypt(seal.content, conversationKey);
|
||||
|
||||
const storedChunks = sql`
|
||||
SELECT COUNT(*) as count
|
||||
FROM event_chunks
|
||||
WHERE message_id = ${messageId}
|
||||
`(db)[0].count;
|
||||
|
||||
sql`
|
||||
INSERT INTO event_chunks (message_id, chunk_index, total_chunks, content, created_at, ccn_pubkey)
|
||||
VALUES (${messageId}, ${chunkIndex}, ${totalChunks}, ${chunk}, ${Math.floor(Date.now() / 1000)}, ${ccnPubkey})
|
||||
`(db);
|
||||
|
||||
if (storedChunks + 1 === Number(totalChunks)) {
|
||||
const allChunks = sql`
|
||||
SELECT * FROM event_chunks
|
||||
WHERE message_id = ${messageId}
|
||||
ORDER BY chunk_index
|
||||
`(db);
|
||||
|
||||
let fullContent = '';
|
||||
for (const chunk of allChunks) {
|
||||
fullContent += chunk.content;
|
||||
}
|
||||
|
||||
const content = JSON.parse(fullContent);
|
||||
return { ...content, ccnPubkey };
|
||||
}
|
||||
|
||||
throw new ChunkedEventReceived();
|
||||
}
|
||||
|
||||
export async function decryptEvent(
|
||||
db: Database,
|
||||
event: nostrTools.Event,
|
||||
): Promise<nostrTools.VerifiedEvent> {
|
||||
const ccnPrivkey = await getCCNPrivateKey();
|
||||
|
||||
): Promise<nostrTools.VerifiedEvent & { ccnPubkey: string }> {
|
||||
if (event.kind !== 1059) {
|
||||
throw new Error('Cannot decrypt event -- not a gift wrap');
|
||||
}
|
||||
|
@ -111,64 +206,30 @@ export async function decryptEvent(
|
|||
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 allCCNs = getAllCCNs(db);
|
||||
if (allCCNs.length === 0) {
|
||||
throw new Error('No CCNs found');
|
||||
}
|
||||
|
||||
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;
|
||||
for (const ccn of allCCNs) {
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(ccn.pubkey);
|
||||
const decryptedEvent = attemptDecryptWithKey(event, ccnPrivkey, ccn.pubkey);
|
||||
if (decryptedEvent.isSome) {
|
||||
return { ...decryptedEvent.value, ccnPubkey: ccn.pubkey };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
for (const ccn of allCCNs) {
|
||||
const ccnPrivkey = await getCCNPrivateKeyByPubkey(ccn.pubkey);
|
||||
try {
|
||||
const chuncked = handleChunkedMessage(db, event, ccnPrivkey, ccn.pubkey);
|
||||
return { ...chuncked, ccnPubkey: ccn.pubkey };
|
||||
} catch (e) {
|
||||
if (e instanceof ChunkedEventReceived) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to decrypt event with any CCN key');
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue