CCN invitation and write permissions to CCN
This commit is contained in:
parent
a8ffce918e
commit
36c7401fa8
5 changed files with 395 additions and 107 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue