// nip44.ts import { chacha20 } from "@noble/ciphers/chacha"; import { ensureBytes, equalBytes } from "@noble/ciphers/utils"; import { secp256k1 } from "@noble/curves/secp256k1"; import { hkdf } from "@noble/hashes/hkdf"; import { hmac } from "@noble/hashes/hmac"; import { sha256 } from "@noble/hashes/sha256"; import { concatBytes, randomBytes } from "@noble/hashes/utils"; import { base64 } from "@scure/base"; // utils.ts var utf8Decoder = new TextDecoder("utf-8"); var utf8Encoder = new TextEncoder(); // nip44.ts var utils = { v2: { maxPlaintextSize: 65536 - 128, minCiphertextSize: 100, maxCiphertextSize: 102400, getConversationKey(privkeyA, pubkeyB) { const key = secp256k1.getSharedSecret(privkeyA, "02" + pubkeyB); return key.subarray(1, 33); }, getMessageKeys(conversationKey, salt) { const keys = hkdf(sha256, conversationKey, salt, "nip44-v2", 76); return { encryption: keys.subarray(0, 32), nonce: keys.subarray(32, 44), auth: keys.subarray(44, 76) }; }, calcPadding(len) { if (!Number.isSafeInteger(len) || len < 0) throw new Error("expected positive integer"); if (len <= 32) return 32; const nextpower = 1 << Math.floor(Math.log2(len - 1)) + 1; const chunk = nextpower <= 256 ? 32 : nextpower / 8; return chunk * (Math.floor((len - 1) / chunk) + 1); }, pad(unpadded) { const unpaddedB = utf8Encoder.encode(unpadded); const len = unpaddedB.length; if (len < 1 || len >= utils.v2.maxPlaintextSize) throw new Error("invalid plaintext length: must be between 1b and 64KB"); const paddedLen = utils.v2.calcPadding(len); const zeros = new Uint8Array(paddedLen - len); const lenBuf = new Uint8Array(2); new DataView(lenBuf.buffer).setUint16(0, len); return concatBytes(lenBuf, unpaddedB, zeros); }, unpad(padded) { const unpaddedLen = new DataView(padded.buffer).getUint16(0); const unpadded = padded.subarray(2, 2 + unpaddedLen); if (unpaddedLen === 0 || unpadded.length !== unpaddedLen || padded.length !== 2 + utils.v2.calcPadding(unpaddedLen)) throw new Error("invalid padding"); return utf8Decoder.decode(unpadded); } } }; function encrypt(key, plaintext, options = {}) { const version = options.version ?? 2; if (version !== 2) throw new Error("unknown encryption version " + version); const salt = options.salt ?? randomBytes(32); ensureBytes(salt, 32); const keys = utils.v2.getMessageKeys(key, salt); const padded = utils.v2.pad(plaintext); const ciphertext = chacha20(keys.encryption, keys.nonce, padded); const mac = hmac(sha256, keys.auth, ciphertext); return base64.encode(concatBytes(new Uint8Array([version]), salt, ciphertext, mac)); } function decrypt(key, ciphertext) { const u = utils.v2; ensureBytes(key, 32); const clen = ciphertext.length; if (clen < u.minCiphertextSize || clen >= u.maxCiphertextSize) throw new Error("invalid ciphertext length: " + clen); if (ciphertext[0] === "#") throw new Error("unknown encryption version"); let data; try { data = base64.decode(ciphertext); } catch (error) { throw new Error("invalid base64: " + error.message); } const vers = data.subarray(0, 1)[0]; if (vers !== 2) throw new Error("unknown encryption version " + vers); const salt = data.subarray(1, 33); const ciphertext_ = data.subarray(33, -32); const mac = data.subarray(-32); const keys = u.getMessageKeys(key, salt); const calculatedMac = hmac(sha256, keys.auth, ciphertext_); if (!equalBytes(calculatedMac, mac)) throw new Error("invalid MAC"); const padded = chacha20(keys.encryption, keys.nonce, ciphertext_); return u.unpad(padded); } export { decrypt, encrypt, utils };