From e7497a559c12befceec7a604358abeb0f21d7025 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 22 Oct 2025 07:44:41 -0400 Subject: [PATCH] Handle buffer-encoded NIP-46 handshake payloads --- js/nostr.js | 94 ++++++++++++++++++++++++++++++++++++++ tests/nostr.nip46.test.mjs | 64 ++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) diff --git a/js/nostr.js b/js/nostr.js index 94909c57..b1f182b3 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -3636,6 +3636,83 @@ function resolveLegacyNip44ConversationKeyGetter(tools) { return null; } +let sharedTextDecoder = null; + +function decodeBytesToUtf8(value) { + if (value == null) { + return ""; + } + + if (typeof Buffer !== "undefined" && typeof Buffer.isBuffer === "function" && Buffer.isBuffer(value)) { + try { + return value.toString("utf8"); + } catch (error) { + return ""; + } + } + + let view = null; + + if (value instanceof Uint8Array) { + view = value; + } else if (typeof ArrayBuffer !== "undefined") { + if (value instanceof ArrayBuffer) { + view = new Uint8Array(value); + } else if (typeof ArrayBuffer.isView === "function" && ArrayBuffer.isView(value)) { + try { + view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } catch (error) { + view = null; + } + } + } + + if (!view && Array.isArray(value)) { + const bytes = []; + for (const entry of value) { + if (typeof entry === "number" && Number.isFinite(entry)) { + const normalized = entry & 0xff; + if (normalized < 0 || normalized > 255) { + return ""; + } + bytes.push(normalized); + } else { + return ""; + } + } + if (!bytes.length) { + return ""; + } + view = Uint8Array.from(bytes); + } + + if (!view || !view.length) { + return ""; + } + + if (!sharedTextDecoder && typeof TextDecoder === "function") { + try { + sharedTextDecoder = new TextDecoder(); + } catch (error) { + sharedTextDecoder = null; + } + } + + if (sharedTextDecoder) { + try { + return sharedTextDecoder.decode(view); + } catch (error) { + // fall through to manual decoding + } + } + + let result = ""; + for (let index = 0; index < view.length; index += 1) { + result += String.fromCharCode(view[index]); + } + return result; +} + function normalizeNip46CiphertextPayload(payload) { const visited = new Set(); @@ -3667,6 +3744,11 @@ function normalizeNip46CiphertextPayload(payload) { } if (Array.isArray(value)) { + const decoded = decodeBytesToUtf8(value); + if (decoded) { + return decoded.trim(); + } + const parts = []; for (const entry of value) { const normalized = coerce(entry); @@ -3684,6 +3766,18 @@ function normalizeNip46CiphertextPayload(payload) { } if (value && typeof value === "object") { + const decoded = decodeBytesToUtf8(value); + if (decoded) { + return decoded.trim(); + } + + if (typeof value.type === "string" && value.type.toLowerCase() === "buffer" && Array.isArray(value.data)) { + const bufferDecoded = decodeBytesToUtf8(value.data); + if (bufferDecoded) { + return bufferDecoded.trim(); + } + } + if (visited.has(value)) { return ""; } diff --git a/tests/nostr.nip46.test.mjs b/tests/nostr.nip46.test.mjs index dfcb55a1..2ec8cb81 100644 --- a/tests/nostr.nip46.test.mjs +++ b/tests/nostr.nip46.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { Buffer } from "node:buffer"; import test from "node:test"; const PAYLOAD = { message: "hello", count: 42 }; @@ -131,3 +132,66 @@ test("decryptNip46PayloadWithKeys coerces structured handshake payloads", async "helper should also decrypt JSON-serialized handshake payloads", ); }); + +test("decryptNip46PayloadWithKeys decodes buffer-based handshake payloads", async () => { + const nostrTools = await loadNostrTools(); + const { generateSecretKey, getPublicKey, nip44, utils } = nostrTools; + + const clientSecret = utils.bytesToHex(generateSecretKey()); + const remoteSecret = utils.bytesToHex(generateSecretKey()); + const remotePubkey = getPublicKey(remoteSecret); + + const serialized = JSON.stringify(PAYLOAD); + const conversationKey = + typeof nip44.v2.getConversationKey === "function" + ? nip44.v2.getConversationKey(clientSecret, remotePubkey) + : nip44.v2.utils.getConversationKey(clientSecret, remotePubkey); + const ciphertext = nip44.v2.encrypt(serialized, conversationKey); + const ciphertextBuffer = Buffer.from(ciphertext, "utf8"); + + const bufferPayload = ciphertextBuffer; + + const { __testExports } = await import("../js/nostr.js"); + const { decryptNip46PayloadWithKeys } = __testExports; + + const bufferRoundTrip = await decryptNip46PayloadWithKeys( + clientSecret, + remotePubkey, + bufferPayload, + ); + + assert.equal( + bufferRoundTrip, + serialized, + "helper should decrypt handshake payloads that serialize buffers", + ); + + const jsonBufferPayload = JSON.stringify(bufferPayload); + const jsonBufferRoundTrip = await decryptNip46PayloadWithKeys( + clientSecret, + remotePubkey, + jsonBufferPayload, + ); + + assert.equal( + jsonBufferRoundTrip, + serialized, + "helper should decrypt JSON-encoded buffer handshake payloads", + ); + + const objectBufferPayload = { + ciphertext: ciphertextBuffer, + }; + + const objectBufferRoundTrip = await decryptNip46PayloadWithKeys( + clientSecret, + remotePubkey, + objectBufferPayload, + ); + + assert.equal( + objectBufferRoundTrip, + serialized, + "helper should decrypt objects containing buffer encoded ciphertext", + ); +});