Refine NIP-46 decryption to use conversation keys

This commit is contained in:
thePR0M3TH3AN committed 2025-10-21 21:25:25 -04:00
1 parent d1a197e5d1
commit c0e2fa3324
2 files changed
+122 -74

No files matched your search

+95 -74
View File
@@ -747,6 +747,8 @@ async function runNip07WithRetry(
export const __testExports = {
runNip07WithRetry,
DEFAULT_NIP07_ENCRYPTION_METHODS,
decryptNip46PayloadWithKeys,
createNip46Cipher,
};
function withRequestTimeout(promise, timeoutMs, onTimeout, message = "Request timed out") {
@@ -3451,26 +3453,100 @@ function sanitizeNip46Metadata(metadata) {
return normalized;
}
function resolveNip44V2ConversationKeyGetter(tools) {
if (typeof tools?.nip44?.v2?.getConversationKey === "function") {
return tools.nip44.v2.getConversationKey.bind(tools.nip44.v2);
}
if (typeof tools?.nip44?.v2?.utils?.getConversationKey === "function") {
return tools.nip44.v2.utils.getConversationKey.bind(tools.nip44.v2.utils);
}
return null;
}
function resolveLegacyNip44ConversationKeyGetter(tools) {
if (typeof tools?.nip44?.getConversationKey === "function") {
return tools.nip44.getConversationKey.bind(tools.nip44);
}
if (typeof tools?.nip44?.utils?.getConversationKey === "function") {
return tools.nip44.utils.getConversationKey.bind(tools.nip44.utils);
}
return null;
}
export function createNip46Cipher(tools, privateKey, remotePubkey) {
if (!tools) {
throw new Error("NostrTools helpers are unavailable for remote signing.");
}
const nip44v2GetConversationKey = resolveNip44V2ConversationKeyGetter(tools);
if (
tools?.nip44?.v2?.encrypt &&
tools?.nip44?.v2?.decrypt &&
typeof nip44v2GetConversationKey === "function"
) {
const conversationKey = nip44v2GetConversationKey(privateKey, remotePubkey);
if (!conversationKey) {
throw new Error("Failed to derive a nip44 conversation key for remote signing.");
}
return {
encrypt: (plaintext, nonce) =>
typeof nonce === "string"
? tools.nip44.v2.encrypt(plaintext, conversationKey, nonce)
: tools.nip44.v2.encrypt(plaintext, conversationKey),
decrypt: (ciphertext) => tools.nip44.v2.decrypt(ciphertext, conversationKey),
};
}
const nip44GetConversationKey = resolveLegacyNip44ConversationKeyGetter(tools);
if (
tools?.nip44?.encrypt &&
tools?.nip44?.decrypt &&
typeof nip44GetConversationKey === "function"
) {
const conversationKey = nip44GetConversationKey(privateKey, remotePubkey);
if (!conversationKey) {
throw new Error("Failed to derive a nip44 conversation key for remote signing.");
}
return {
encrypt: (plaintext, nonce) =>
typeof nonce === "string"
? tools.nip44.encrypt(plaintext, conversationKey, nonce)
: tools.nip44.encrypt(plaintext, conversationKey),
decrypt: (ciphertext) => tools.nip44.decrypt(ciphertext, conversationKey),
};
}
if (tools?.nip04?.encrypt && tools?.nip04?.decrypt) {
const privateKeyHex = typeof privateKey === "string" ? privateKey : "";
const remotePubkeyHex = typeof remotePubkey === "string" ? remotePubkey : "";
return {
encrypt: (plaintext) => tools.nip04.encrypt(privateKeyHex, remotePubkeyHex, plaintext),
decrypt: (ciphertext) => tools.nip04.decrypt(privateKeyHex, remotePubkeyHex, ciphertext),
};
}
throw new Error("Remote signer encryption helpers are unavailable.");
}
async function decryptNip46PayloadWithKeys(privateKey, remotePubkey, ciphertext) {
const tools = (await ensureNostrTools()) || getCachedNostrTools();
if (!tools) {
throw new Error("NostrTools helpers are unavailable for NIP-46 payload decryption.");
}
let decrypt = null;
if (tools?.nip44?.v2?.decrypt) {
decrypt = (priv, pub, payload) => tools.nip44.v2.decrypt(priv, pub, payload);
} else if (tools?.nip44?.decrypt) {
decrypt = (priv, pub, payload) => tools.nip44.decrypt(priv, pub, payload);
} else if (tools?.nip04?.decrypt) {
decrypt = (priv, pub, payload) => tools.nip04.decrypt(priv, pub, payload);
}
if (!decrypt) {
throw new Error("Remote signer encryption helpers are unavailable.");
}
return decrypt(privateKey, remotePubkey, ciphertext);
const { decrypt } = createNip46Cipher(tools, privateKey, remotePubkey);
return decrypt(ciphertext);
}
class Nip46RpcClient {
@@ -3552,66 +3628,11 @@ class Nip46RpcClient {
throw new Error("NostrTools helpers are unavailable for remote signing.");
}
let encrypt = null;
let decrypt = null;
const nip44v2GetConversationKey =
typeof tools?.nip44?.v2?.getConversationKey === "function"
? tools.nip44.v2.getConversationKey
: typeof tools?.nip44?.v2?.utils?.getConversationKey === "function"
? tools.nip44.v2.utils.getConversationKey
: null;
if (
tools?.nip44?.v2?.encrypt &&
tools?.nip44?.v2?.decrypt &&
nip44v2GetConversationKey
) {
const conversationKey = nip44v2GetConversationKey(
this.clientPrivateKey,
this.remotePubkey,
);
if (!conversationKey) {
throw new Error("Failed to derive a nip44 conversation key for remote signing.");
}
encrypt = (plaintext, nonce) =>
typeof nonce === "string"
? tools.nip44.v2.encrypt(plaintext, conversationKey, nonce)
: tools.nip44.v2.encrypt(plaintext, conversationKey);
decrypt = (ciphertext) => tools.nip44.v2.decrypt(ciphertext, conversationKey);
} else if (
tools?.nip44?.encrypt &&
tools?.nip44?.decrypt &&
typeof tools?.nip44?.getConversationKey === "function"
) {
const conversationKey = tools.nip44.getConversationKey(
this.clientPrivateKey,
this.remotePubkey,
);
if (!conversationKey) {
throw new Error("Failed to derive a nip44 conversation key for remote signing.");
}
encrypt = (plaintext, nonce) =>
typeof nonce === "string"
? tools.nip44.encrypt(plaintext, conversationKey, nonce)
: tools.nip44.encrypt(plaintext, conversationKey);
decrypt = (ciphertext) => tools.nip44.decrypt(ciphertext, conversationKey);
} else if (tools?.nip04?.encrypt && tools?.nip04?.decrypt) {
const privateKey = this.clientPrivateKey;
const remotePubkey = this.remotePubkey;
encrypt = (plaintext) => tools.nip04.encrypt(privateKey, remotePubkey, plaintext);
decrypt = (ciphertext) => tools.nip04.decrypt(privateKey, remotePubkey, ciphertext);
}
if (!encrypt || !decrypt) {
throw new Error("Remote signer encryption helpers are unavailable.");
}
this.cipher = { encrypt, decrypt };
this.cipher = createNip46Cipher(
tools,
this.clientPrivateKey,
this.remotePubkey,
);
return this.cipher;
}
+27
View File
@@ -54,3 +54,30 @@ test("Nip46RpcClient encrypts payloads with nip44 conversation keys", async () =
"client should decrypt ciphertext produced by nostr-tools",
);
});
test("decryptNip46PayloadWithKeys handles nip44.v2 ciphertext", 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 { __testExports } = await import("../js/nostr.js");
const { decryptNip46PayloadWithKeys } = __testExports;
const roundTrip = await decryptNip46PayloadWithKeys(
clientSecret,
remotePubkey,
ciphertext,
);
assert.equal(roundTrip, serialized, "helper should decrypt nip44.v2 ciphertext");
});