From 4cc51d7a79ea2cf3d4a4277234471cefe6bf0bd0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 19 Jan 2026 09:40:51 -0500 Subject: [PATCH] Add NIP-44 verification to DM decrypt worker --- js/dmDecryptor.js | 15 ++++++--- js/nostr/client.js | 6 ++-- js/nostr/dmDecryptWorker.js | 55 +++++++++++++++++++++++++++++++ js/nostr/dmDecryptWorkerClient.js | 3 ++ js/services/nostrService.js | 5 ++- 5 files changed, 76 insertions(+), 8 deletions(-) diff --git a/js/dmDecryptor.js b/js/dmDecryptor.js index b6c47298..0559b094 100644 --- a/js/dmDecryptor.js +++ b/js/dmDecryptor.js @@ -268,6 +268,7 @@ function buildDecryptResult({ envelope = null, errors = [], }) { + const normalizedScheme = normalizeScheme(scheme || decryptor?.scheme || ""); const timestampCandidates = []; if (message && Number.isFinite(message.created_at)) { timestampCandidates.push(message.created_at); @@ -295,11 +296,12 @@ function buildDecryptResult({ actorPubkey: normalizeHex(actorPubkey), decryptor: decryptor ? { - scheme: normalizeScheme(decryptor.scheme) || scheme || "", + scheme: normalizeScheme(decryptor.scheme) || normalizedScheme || "", source: decryptor.source || "", } - : { scheme: scheme || "", source: "" }, - scheme: normalizeScheme(scheme || decryptor?.scheme || ""), + : { scheme: normalizedScheme || "", source: "" }, + scheme: normalizedScheme, + encryption_scheme: normalizedScheme, envelope, direction: deriveDirection(actorPubkey, senderPubkey, recipients), timestamp: Number.isFinite(timestamp) ? timestamp : Date.now() / 1000, @@ -472,6 +474,10 @@ async function decryptLegacyDm(event, decryptors, actorPubkey) { }); if (typeof plaintext === "string") { + const resolvedScheme = + normalizeScheme(hints.algorithms?.[0]) || + normalizeScheme(decryptor.scheme) || + ""; return buildDecryptResult({ ok: true, event, @@ -484,7 +490,7 @@ async function decryptLegacyDm(event, decryptors, actorPubkey) { senderPubkey, actorPubkey, decryptor, - scheme: decryptor.scheme || hints.algorithms?.[0] || "", + scheme: resolvedScheme, }); } } catch (error) { @@ -586,4 +592,3 @@ export const __testUtils = { collectRecipients, orderDecryptors, }; - diff --git a/js/nostr/client.js b/js/nostr/client.js index 6a17e94e..2cf97f0a 100644 --- a/js/nostr/client.js +++ b/js/nostr/client.js @@ -4066,12 +4066,13 @@ export class NostrClient { if (canUseWorker) { addCandidate( "nip44", - (targetPubkey, ciphertext) => + (targetPubkey, ciphertext, options = {}) => decryptDmInWorker({ scheme: "nip44", privateKey: sessionPrivateKey, targetPubkey, ciphertext, + event: options?.event, }), { priority: -6, @@ -4082,12 +4083,13 @@ export class NostrClient { addCandidate( "nip04", - (targetPubkey, ciphertext) => + (targetPubkey, ciphertext, options = {}) => decryptDmInWorker({ scheme: "nip04", privateKey: sessionPrivateKey, targetPubkey, ciphertext, + event: options?.event, }), { priority: -4, diff --git a/js/nostr/dmDecryptWorker.js b/js/nostr/dmDecryptWorker.js index caec3ec4..73603d93 100644 --- a/js/nostr/dmDecryptWorker.js +++ b/js/nostr/dmDecryptWorker.js @@ -47,6 +47,8 @@ const normalizePayload = (payload) => { typeof payload.targetPubkey === "string" ? payload.targetPubkey.trim() : ""; const ciphertext = typeof payload.ciphertext === "string" ? payload.ciphertext : ""; + const event = + payload.event && typeof payload.event === "object" ? payload.event : null; if (!id || !privateKey || !targetPubkey || !ciphertext) { return null; @@ -58,6 +60,33 @@ const normalizePayload = (payload) => { privateKey, targetPubkey, ciphertext, + event, + }; +}; + +const normalizeEventForVerification = (event) => { + if (!event || typeof event !== "object") { + return null; + } + + const pubkey = typeof event.pubkey === "string" ? event.pubkey.trim() : ""; + const sig = typeof event.sig === "string" ? event.sig.trim() : ""; + const id = typeof event.id === "string" ? event.id.trim() : ""; + + if (!pubkey || !sig || !id) { + return null; + } + + return { + id, + pubkey, + sig, + kind: Number.isFinite(event.kind) ? event.kind : 0, + created_at: Number.isFinite(event.created_at) + ? event.created_at + : Math.floor(Date.now() / 1000), + tags: Array.isArray(event.tags) ? event.tags : [], + content: typeof event.content === "string" ? event.content : "", }; }; @@ -137,6 +166,31 @@ const ensureNip44 = async () => { }; }; +const verifyEventSignature = async (event, tools = null) => { + const candidate = normalizeEventForVerification(event); + if (!candidate) { + throw new Error("missing-event-signature"); + } + + const resolvedTools = tools || (await ensureNostrTools()) || getCachedNostrTools(); + if (!resolvedTools || typeof resolvedTools !== "object") { + throw new Error("signature-verification-unavailable"); + } + + if ( + typeof resolvedTools.validateEvent !== "function" || + typeof resolvedTools.verifyEvent !== "function" + ) { + throw new Error("signature-verification-unavailable"); + } + + if (!resolvedTools.validateEvent(candidate) || !resolvedTools.verifyEvent(candidate)) { + throw new Error("invalid-event-signature"); + } + + return true; +}; + const handleMessage = async (event) => { const payload = normalizePayload(event?.data); if (!payload) { @@ -159,6 +213,7 @@ const handleMessage = async (event) => { try { if (payload.scheme === "nip44" || payload.scheme === "nip44_v2") { + await verifyEventSignature(payload.event); const nip44 = await ensureNip44(); const privateKeyBytes = nip44.hexToBytes(payload.privateKey); const conversationKey = nip44.getConversationKey( diff --git a/js/nostr/dmDecryptWorkerClient.js b/js/nostr/dmDecryptWorkerClient.js index 794abb49..ae2a70ca 100644 --- a/js/nostr/dmDecryptWorkerClient.js +++ b/js/nostr/dmDecryptWorkerClient.js @@ -82,6 +82,7 @@ export function decryptDmInWorker({ privateKey, targetPubkey, ciphertext, + event, timeoutMs = DEFAULT_TIMEOUT_MS, } = {}) { const worker = ensureWorker(); @@ -95,6 +96,7 @@ export function decryptDmInWorker({ typeof targetPubkey === "string" ? targetPubkey.trim() : ""; const payloadCiphertext = typeof ciphertext === "string" ? ciphertext : ""; const payloadScheme = typeof scheme === "string" ? scheme : ""; + const payloadEvent = event && typeof event === "object" ? event : null; if (!trimmedPrivateKey || !trimmedTarget || !payloadCiphertext) { return Promise.reject(new Error("dm-worker-invalid-input")); @@ -115,6 +117,7 @@ export function decryptDmInWorker({ privateKey: trimmedPrivateKey, targetPubkey: trimmedTarget, ciphertext: payloadCiphertext, + event: payloadEvent, }); }); } diff --git a/js/services/nostrService.js b/js/services/nostrService.js index 3de9ae10..15c2e371 100644 --- a/js/services/nostrService.js +++ b/js/services/nostrService.js @@ -353,7 +353,9 @@ function normalizeDirectMessageRecord(message, actorPubkey = "") { : []; const encryptionScheme = - typeof message?.scheme === "string" + typeof message?.encryption_scheme === "string" + ? message.encryption_scheme.trim() + : typeof message?.scheme === "string" ? message.scheme.trim() : typeof message?.decryptor?.scheme === "string" ? message.decryptor.scheme.trim() @@ -442,6 +444,7 @@ function hydrateMessagesFromSnapshot(snapshot, actorPubkey = "") { recipients: [], direction: "unknown", scheme: "", + encryption_scheme: "", decryptor: { scheme: "", source: "snapshot" }, event: { id: `dm-snapshot:${normalizedActor || "actor"}:${remote}:${timestamp}:${index}`,