Merge pull request #786 from PR0M3TH3AN/codex/fix-nip-46-handshake-decryption-issue

Handle structured NIP-46 handshake payloads
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-22 07:28:55 -04:00
commit 18d87de7f2
2 files changed
+161 -1

No files matched your search

+111 -1
View File
@@ -3636,6 +3636,115 @@ function resolveLegacyNip44ConversationKeyGetter(tools) {
return null;
}
function normalizeNip46CiphertextPayload(payload) {
const visited = new Set();
const coerce = (value) => {
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
const firstChar = trimmed.charAt(0);
const lastChar = trimmed.charAt(trimmed.length - 1);
if (
(firstChar === "{" && lastChar === "}") ||
(firstChar === "[" && lastChar === "]")
) {
try {
const parsed = JSON.parse(trimmed);
const normalized = coerce(parsed);
if (normalized) {
return normalized;
}
} catch (error) {
// ignore JSON parse errors and fall through to returning the trimmed string
}
}
return trimmed;
}
if (Array.isArray(value)) {
const parts = [];
for (const entry of value) {
const normalized = coerce(entry);
if (normalized) {
parts.push(normalized);
}
}
if (!parts.length) {
return "";
}
if (parts.length === 1) {
return parts[0];
}
return parts.join("\n");
}
if (value && typeof value === "object") {
if (visited.has(value)) {
return "";
}
visited.add(value);
const ciphertextCandidates = [
typeof value.ciphertext === "string" ? value.ciphertext.trim() : "",
typeof value.cipher_text === "string" ? value.cipher_text.trim() : "",
typeof value.content === "string" ? value.content.trim() : "",
typeof value.payload === "string" ? value.payload.trim() : "",
typeof value.result === "string" ? value.result.trim() : "",
typeof value.value === "string" ? value.value.trim() : "",
typeof value.data === "string" ? value.data.trim() : "",
];
const ciphertext = ciphertextCandidates.find(Boolean) || "";
const nonceCandidates = [
typeof value.nonce === "string" ? value.nonce.trim() : "",
typeof value.iv === "string" ? value.iv.trim() : "",
];
const nonce = nonceCandidates.find(Boolean) || "";
if (ciphertext && nonce) {
return `${ciphertext}\n${nonce}`;
}
if (ciphertext) {
return ciphertext;
}
const nestedKeys = [
"ciphertext",
"cipher_text",
"payload",
"result",
"value",
"data",
"content",
];
for (const key of nestedKeys) {
if (key in value) {
const normalized = coerce(value[key]);
if (normalized) {
return normalized;
}
}
}
return "";
}
if (typeof value === "number" || typeof value === "boolean") {
return String(value);
}
return "";
};
return coerce(payload);
}
export function createNip46Cipher(tools, privateKey, remotePubkey) {
if (!tools) {
throw new Error("NostrTools helpers are unavailable for remote signing.");
@@ -3705,7 +3814,8 @@ async function decryptNip46PayloadWithKeys(privateKey, remotePubkey, ciphertext)
}
const { decrypt } = createNip46Cipher(tools, privateKey, remotePubkey);
return decrypt(ciphertext);
const normalizedCiphertext = normalizeNip46CiphertextPayload(ciphertext);
return decrypt(normalizedCiphertext);
}
class Nip46RpcClient {
+50
View File
@@ -81,3 +81,53 @@ test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", async () => {
assert.equal(roundTrip, serialized, "helper should decrypt nip44.v2 ciphertext");
});
test("decryptNip46PayloadWithKeys coerces structured 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 [ciphertextPart, nonce] = ciphertext.split("\n");
const structuredPayload = {
ciphertext: ciphertextPart,
nonce,
};
const { __testExports } = await import("../js/nostr.js");
const { decryptNip46PayloadWithKeys } = __testExports;
const roundTrip = await decryptNip46PayloadWithKeys(
clientSecret,
remotePubkey,
structuredPayload,
);
assert.equal(
roundTrip,
serialized,
"helper should decrypt ciphertext encoded as an object with nonce",
);
const jsonPayload = JSON.stringify(structuredPayload);
const jsonRoundTrip = await decryptNip46PayloadWithKeys(
clientSecret,
remotePubkey,
jsonPayload,
);
assert.equal(
jsonRoundTrip,
serialized,
"helper should also decrypt JSON-serialized handshake payloads",
);
});