mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 18:02:27 +00:00
Merge pull request #786 from PR0M3TH3AN/codex/fix-nip-46-handshake-decryption-issue
Handle structured NIP-46 handshake payloads
This commit is contained in:
2 files changed
+161
-1
No files matched your search
+111
-1
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user