Handle buffer-encoded NIP-46 handshake payloads

This commit is contained in:
thePR0M3TH3AN committed 2025-10-22 07:44:41 -04:00
1 parent 18d87de7f2
commit e7497a559c
2 files changed
+158

No files matched your search

+94
View File
@@ -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 "";
}
+64
View File
@@ -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",
);
});