CCN invitation and write permissions to CCN

This commit is contained in:
Danny Morabito 2025-04-23 18:13:29 +02:00
parent a8ffce918e
commit 36c7401fa8
Signed by: dannym
GPG key ID: 7CC8056A5A04557E
5 changed files with 395 additions and 107 deletions

View file

@ -14,6 +14,71 @@ import { sql } from './utils/queries.ts';
export class EventAlreadyExistsException extends Error {}
export class ChunkedEventReceived extends Error {}
export function createEncryptedEventForPubkey(
pubkey: string,
event: nostrTools.VerifiedEvent,
) {
const randomPrivateKey = nostrTools.generateSecretKey();
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
const conversationKey = nip44.getConversationKey(randomPrivateKey, pubkey);
const eventJson = JSON.stringify(event);
const encryptedEvent = nip44.encrypt(eventJson, conversationKey);
const sealTemplate = {
kind: 13,
created_at: randomTimeUpTo2DaysInThePast(),
content: encryptedEvent,
tags: [],
};
const seal = nostrTools.finalizeEvent(sealTemplate, randomPrivateKey);
const giftWrapTemplate = {
kind: 1059,
created_at: randomTimeUpTo2DaysInThePast(),
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
tags: [['p', pubkey]],
pubkey: randomPrivateKeyPubKey,
};
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey);
return giftWrap;
}
export function createEncryptedChunkForPubkey(
pubkey: string,
chunk: string,
chunkIndex: number,
totalChunks: number,
messageId: string,
privateKey: Uint8Array,
) {
const randomPrivateKey = nostrTools.generateSecretKey();
const randomPrivateKeyPubKey = nostrTools.getPublicKey(randomPrivateKey);
const conversationKey = nip44.getConversationKey(randomPrivateKey, pubkey);
const sealTemplate = {
kind: 13,
created_at: randomTimeUpTo2DaysInThePast(),
content: nip44.encrypt(chunk, conversationKey),
tags: [['chunk', String(chunkIndex), String(totalChunks), messageId]],
};
const seal = nostrTools.finalizeEvent(sealTemplate, privateKey);
const giftWrapTemplate = {
kind: 1059,
created_at: randomTimeUpTo2DaysInThePast(),
content: nip44.encrypt(JSON.stringify(seal), conversationKey),
tags: [['p', pubkey]],
pubkey: randomPrivateKeyPubKey,
};
const minedGiftWrap = nostrTools.nip13.minePow(giftWrapTemplate, POW_TO_MINE);
const giftWrap = nostrTools.finalizeEvent(minedGiftWrap, randomPrivateKey);
return giftWrap;
}
export async function createEncryptedEvent(
event: nostrTools.VerifiedEvent,
db: Database,
@ -29,32 +94,7 @@ export async function createEncryptedEvent(
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;
return createEncryptedEventForPubkey(ccnPubKey, event);
}
const chunks: string[] = [];
@ -67,35 +107,15 @@ export async function createEncryptedEvent(
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),
createEncryptedChunkForPubkey(
ccnPubKey,
chunk,
i,
totalChunks,
messageId,
ccnPrivateKey,
),
);
}
@ -111,26 +131,30 @@ function attemptDecryptWithKey(
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,
);
try {
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();
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 }));
}
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();
});
} catch {
return None();
});
}
}
/**
@ -163,8 +187,8 @@ function handleChunkedMessage(
const chunk = nip44.decrypt(seal.content, conversationKey);
const storedChunks = sql`
SELECT COUNT(*) as count
FROM event_chunks
SELECT COUNT(*) as count
FROM event_chunks
WHERE message_id = ${messageId}
`(db)[0].count;
@ -175,8 +199,8 @@ function handleChunkedMessage(
if (storedChunks + 1 === Number(totalChunks)) {
const allChunks = sql`
SELECT * FROM event_chunks
WHERE message_id = ${messageId}
SELECT * FROM event_chunks
WHERE message_id = ${messageId}
ORDER BY chunk_index
`(db);
@ -200,34 +224,67 @@ export async function decryptEvent(
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 allCCNs = getAllCCNs(db);
if (allCCNs.length === 0) {
throw new Error('No CCNs found');
}
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 };
}
const pow = nostrTools.nip13.getPow(event.id);
const isInvite =
event.tags.findIndex(
(tag: string[]) => tag[0] === 'type' && tag[1] === 'invite',
) !== -1;
const eventDestination = event.tags.find(
(tag: string[]) => tag[0] === 'p',
)?.[1];
if (!eventDestination) {
throw new Error('Cannot decrypt event -- no destination');
}
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;
}
if (isInvite) {
const ccnPrivkey = await getCCNPrivateKeyByPubkey(eventDestination);
const decryptedEvent = attemptDecryptWithKey(
event,
ccnPrivkey,
eventDestination,
);
if (decryptedEvent.isSome) {
const recipient = decryptedEvent.value.tags.find(
(tag: string[]) => tag[0] === 'p',
)?.[1];
if (recipient !== eventDestination)
throw new Error('Cannot decrypt invite');
return { ...decryptedEvent.value, ccnPubkey: eventDestination };
}
throw new Error('Cannot decrypt invite');
}
if (pow < MIN_POW) {
throw new Error('Cannot decrypt event -- PoW too low');
}
const ccnPrivkey = await getCCNPrivateKeyByPubkey(eventDestination);
const decryptedEvent = attemptDecryptWithKey(
event,
ccnPrivkey,
eventDestination,
);
if (decryptedEvent.isSome) {
return { ...decryptedEvent.value, ccnPubkey: eventDestination };
}
try {
const chuncked = handleChunkedMessage(
db,
event,
ccnPrivkey,
eventDestination,
);
return { ...chuncked, ccnPubkey: eventDestination };
} catch (e) {
if (e instanceof ChunkedEventReceived) {
throw e;
}
}