feat: add support for nip44

This commit is contained in:
Ren Amamiya
2026-04-08 11:54:00 +07:00
parent 387796faa3
commit 72b9dcddc1
20 changed files with 2447 additions and 309 deletions

View File

@@ -1127,10 +1127,10 @@
sum += a.length;
}
const res = new Uint8Array(sum);
for (let i2 = 0, pad2 = 0; i2 < arrays.length; i2++) {
for (let i2 = 0, pad3 = 0; i2 < arrays.length; i2++) {
const a = arrays[i2];
res.set(a, pad2);
pad2 += a.length;
res.set(a, pad3);
pad3 += a.length;
}
return res;
}
@@ -2163,16 +2163,16 @@
this.blockLen = this.iHash.blockLen;
this.outputLen = this.iHash.outputLen;
const blockLen = this.blockLen;
const pad2 = new Uint8Array(blockLen);
pad2.set(key.length > blockLen ? hash.create().update(key).digest() : key);
for (let i2 = 0; i2 < pad2.length; i2++)
pad2[i2] ^= 54;
this.iHash.update(pad2);
const pad3 = new Uint8Array(blockLen);
pad3.set(key.length > blockLen ? hash.create().update(key).digest() : key);
for (let i2 = 0; i2 < pad3.length; i2++)
pad3[i2] ^= 54;
this.iHash.update(pad3);
this.oHash = hash.create();
for (let i2 = 0; i2 < pad2.length; i2++)
pad2[i2] ^= 54 ^ 92;
this.oHash.update(pad2);
clean(pad2);
for (let i2 = 0; i2 < pad3.length; i2++)
pad3[i2] ^= 54 ^ 92;
this.oHash.update(pad3);
clean(pad3);
}
update(buf) {
aexists(this);
@@ -2785,7 +2785,7 @@
const l = abytes(item, void 0, "key").length;
return l === publicKey || l === publicKeyUncompressed;
}
function getSharedSecret(secretKeyA, publicKeyB, isCompressed = true) {
function getSharedSecret2(secretKeyA, publicKeyB, isCompressed = true) {
if (isProbPub(secretKeyA) === true)
throw new Error("first arg must be private key");
if (isProbPub(publicKeyB) === false)
@@ -2800,7 +2800,7 @@
randomSecretKey
};
const keygen = createKeygen(randomSecretKey, getPublicKey2);
return Object.freeze({ getPublicKey: getPublicKey2, getSharedSecret, keygen, Point, utils, lengths });
return Object.freeze({ getPublicKey: getPublicKey2, getSharedSecret: getSharedSecret2, keygen, Point, utils, lengths });
}
function ecdsa(Point, hash, ecdsaOpts = {}) {
ahash(hash);
@@ -2816,7 +2816,7 @@
const hmac2 = ecdsaOpts.hmac || ((key, msg) => hmac(hash, key, msg));
const { Fp, Fn } = Point;
const { ORDER: CURVE_ORDER, BITS: fnBits } = Fn;
const { keygen, getPublicKey: getPublicKey2, getSharedSecret, utils, lengths } = ecdh(Point, ecdsaOpts);
const { keygen, getPublicKey: getPublicKey2, getSharedSecret: getSharedSecret2, utils, lengths } = ecdh(Point, ecdsaOpts);
const defaultSigOpts = {
prehash: true,
lowS: typeof ecdsaOpts.lowS === "boolean" ? ecdsaOpts.lowS : true,
@@ -3022,7 +3022,7 @@
return Object.freeze({
keygen,
getPublicKey: getPublicKey2,
getSharedSecret,
getSharedSecret: getSharedSecret2,
utils,
lengths,
Point,
@@ -4339,7 +4339,7 @@
h[9] = d9;
}
finalize() {
const { h, pad: pad2 } = this;
const { h, pad: pad3 } = this;
const g = new Uint16Array(10);
let c = h[1] >>> 13;
h[1] &= 8191;
@@ -4378,10 +4378,10 @@
h[5] = (h[6] >>> 2 | h[7] << 11) & 65535;
h[6] = (h[7] >>> 5 | h[8] << 8) & 65535;
h[7] = (h[8] >>> 8 | h[9] << 5) & 65535;
let f = h[0] + pad2[0];
let f = h[0] + pad3[0];
h[0] = f & 65535;
for (let i2 = 1; i2 < 8; i2++) {
f = (h[i2] + pad2[i2] | 0) + (f >>> 16) | 0;
f = (h[i2] + pad3[i2] | 0) + (f >>> 16) | 0;
h[i2] = f & 65535;
}
clean2(g);
@@ -7122,6 +7122,110 @@
return true;
}
// node_modules/.pnpm/nostr-tools@2.23.3/node_modules/nostr-tools/lib/esm/nip44.js
var utf8Decoder2 = new TextDecoder("utf-8");
var utf8Encoder2 = new TextEncoder();
var minPlaintextSize2 = 1;
var maxPlaintextSize2 = 65535;
function getConversationKey2(privkeyA, pubkeyB) {
const sharedX = secp256k1.getSharedSecret(privkeyA, hexToBytes("02" + pubkeyB)).subarray(1, 33);
return extract(sha256, sharedX, utf8Encoder2.encode("nip44-v2"));
}
function getMessageKeys2(conversationKey, nonce) {
const keys = expand(sha256, conversationKey, nonce, 76);
return {
chacha_key: keys.subarray(0, 32),
chacha_nonce: keys.subarray(32, 44),
hmac_key: keys.subarray(44, 76)
};
}
function calcPaddedLen2(len) {
if (!Number.isSafeInteger(len) || len < 1)
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);
}
function writeU16BE2(num2) {
if (!Number.isSafeInteger(num2) || num2 < minPlaintextSize2 || num2 > maxPlaintextSize2)
throw new Error("invalid plaintext size: must be between 1 and 65535 bytes");
const arr = new Uint8Array(2);
new DataView(arr.buffer).setUint16(0, num2, false);
return arr;
}
function pad2(plaintext) {
const unpadded = utf8Encoder2.encode(plaintext);
const unpaddedLen = unpadded.length;
const prefix = writeU16BE2(unpaddedLen);
const suffix = new Uint8Array(calcPaddedLen2(unpaddedLen) - unpaddedLen);
return concatBytes(prefix, unpadded, suffix);
}
function unpad2(padded) {
const unpaddedLen = new DataView(padded.buffer).getUint16(0);
const unpadded = padded.subarray(2, 2 + unpaddedLen);
if (unpaddedLen < minPlaintextSize2 || unpaddedLen > maxPlaintextSize2 || unpadded.length !== unpaddedLen || padded.length !== 2 + calcPaddedLen2(unpaddedLen))
throw new Error("invalid padding");
return utf8Decoder2.decode(unpadded);
}
function hmacAad2(key, message, aad) {
if (aad.length !== 32)
throw new Error("AAD associated data must be 32 bytes");
const combined = concatBytes(aad, message);
return hmac(sha256, key, combined);
}
function decodePayload2(payload) {
if (typeof payload !== "string")
throw new Error("payload must be a valid string");
const plen = payload.length;
if (plen < 132 || plen > 87472)
throw new Error("invalid payload length: " + plen);
if (payload[0] === "#")
throw new Error("unknown encryption version");
let data;
try {
data = base64.decode(payload);
} catch (error) {
throw new Error("invalid base64: " + error.message);
}
const dlen = data.length;
if (dlen < 99 || dlen > 65603)
throw new Error("invalid data length: " + dlen);
const vers = data[0];
if (vers !== 2)
throw new Error("unknown encryption version " + vers);
return {
nonce: data.subarray(1, 33),
ciphertext: data.subarray(33, -32),
mac: data.subarray(-32)
};
}
function encrypt3(plaintext, conversationKey, nonce = randomBytes(32)) {
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys2(conversationKey, nonce);
const padded = pad2(plaintext);
const ciphertext = chacha20(chacha_key, chacha_nonce, padded);
const mac = hmacAad2(hmac_key, ciphertext, nonce);
return base64.encode(concatBytes(new Uint8Array([2]), nonce, ciphertext, mac));
}
function decrypt3(payload, conversationKey) {
const { nonce, ciphertext, mac } = decodePayload2(payload);
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys2(conversationKey, nonce);
const calculatedMac = hmacAad2(hmac_key, ciphertext, nonce);
if (!equalBytes(calculatedMac, mac))
throw new Error("invalid MAC");
const padded = chacha20(chacha_key, chacha_nonce, ciphertext);
return unpad2(padded);
}
var v22 = {
utils: {
getConversationKey: getConversationKey2,
calcPaddedLen: calcPaddedLen2
},
encrypt: encrypt3,
decrypt: decrypt3
};
// node_modules/.pnpm/async-mutex@0.3.2/node_modules/async-mutex/index.mjs
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
var E_ALREADY_LOCKED = new Error("mutex already locked");
@@ -7281,17 +7385,51 @@
}
};
// extension/utils.js
var LRUCache = class {
constructor(maxSize) {
this.maxSize = maxSize;
this.map = /* @__PURE__ */ new Map();
this.keys = [];
}
clear() {
this.map.clear();
}
has(k) {
return this.map.has(k);
}
get(k) {
const v = this.map.get(k);
if (v !== void 0) {
this.keys.push(k);
if (this.keys.length > this.maxSize * 2) {
this.keys.splice(-this.maxSize);
}
}
return v;
}
set(k, v) {
this.map.set(k, v);
this.keys.push(k);
if (this.map.size > this.maxSize) {
this.map.delete(this.keys.shift());
}
}
};
// extension/common.js
var import_webextension_polyfill = __toESM(require_browser_polyfill());
var NO_PERMISSIONS_REQUIRED = {
replaceURL: true
replaceURL: true,
peekPublicKey: true
};
var PERMISSION_NAMES = Object.fromEntries([
["getPublicKey", "read your public key"],
["getRelays", "read your list of preferred relays"],
["signEvent", "sign events using your private key"],
["nip04.encrypt", "encrypt messages to peers"],
["nip04.decrypt", "decrypt messages from peers"]
["nip04.decrypt", "decrypt messages from peers"],
["nip44.encrypt", "encrypt messages to peers"],
["nip44.decrypt", "decrypt messages from peers"]
]);
function matchConditions(conditions, event) {
if (conditions?.kinds) {
@@ -7349,8 +7487,8 @@
import_webextension_polyfill.default.storage.local.set({ policies });
}
async function showNotification(host, answer, type, params) {
const ok = await import_webextension_polyfill.default.storage.local.get("notifications");
if (ok) {
const { notifications } = await import_webextension_polyfill.default.storage.local.get("notifications");
if (notifications) {
const action = answer ? "allowed" : "denied";
import_webextension_polyfill.default.notifications.create(void 0, {
type: "basic",
@@ -7368,14 +7506,48 @@
});
}
}
async function getPosition(width2, height2) {
let left = 0;
let top = 0;
try {
const lastFocused = await import_webextension_polyfill.default.windows.getLastFocused();
if (lastFocused && lastFocused.top !== void 0 && lastFocused.left !== void 0 && lastFocused.width !== void 0 && lastFocused.height !== void 0) {
top = Math.round(lastFocused.top + (lastFocused.height - height2) / 2);
left = Math.round(lastFocused.left + (lastFocused.width - width2) / 2);
} else {
console.error("Last focused window properties are undefined.");
}
} catch (error) {
console.error("Error getting window position:", error);
}
return {
top,
left
};
}
// extension/background.js
var { hexToBytes: hexToBytes2 } = utils_exports;
var { encrypt: encrypt3, decrypt: decrypt3 } = nip04_exports;
var { encrypt: encrypt4, decrypt: decrypt4 } = nip04_exports;
var openPrompt = null;
var promptMutex = new Mutex();
var releasePromptMutex = () => {
};
var secretsCache = new LRUCache(100);
var previousSk = null;
function getSharedSecret(sk, peer) {
if (previousSk !== sk) {
secretsCache.clear();
}
let key = secretsCache.get(peer);
if (!key) {
key = v22.utils.getConversationKey(sk, peer);
secretsCache.set(peer, key);
}
return key;
}
var width = 440;
var height = 420;
import_webextension_polyfill2.default.runtime.onInstalled.addListener((_, __, reason) => {
if (reason === "install")
import_webextension_polyfill2.default.runtime.openOptionsPage();
@@ -7402,6 +7574,12 @@
async function handleContentScriptMessage({ type, params, host }) {
if (NO_PERMISSIONS_REQUIRED[type]) {
switch (type) {
case "peekPublicKey": {
const allowed = await getPermissionStatus(host, "getPublicKey");
if (allowed === true)
return performOperation("getPublicKey", params);
return "";
}
case "replaceURL": {
const { protocol_handler: ph } = await import_webextension_polyfill2.default.storage.local.get([
"protocol_handler"
@@ -7459,12 +7637,15 @@
const url = `${import_webextension_polyfill2.default.runtime.getURL(
"prompt.html"
)}?${qs.toString()}`;
const { top, left } = getPosition(width, height);
if (import_webextension_polyfill2.default.windows) {
import_webextension_polyfill2.default.windows.create({
url,
type: "popup",
width: 600,
height: 600
width,
height,
top,
left
});
} else {
import_webextension_polyfill2.default.tabs.create({
@@ -7474,11 +7655,11 @@
}
});
if (!accept)
return { error: "denied" };
return { error: { message: "denied" } };
} catch (err) {
releasePromptMutex();
return {
error: `error: ${err}`
error: { message: err.message, stack: err.stack }
};
}
}
@@ -7510,11 +7691,21 @@
}
case "nip04.encrypt": {
const { peer, plaintext } = params;
return encrypt3(sk, peer, plaintext);
return encrypt4(sk, peer, plaintext);
}
case "nip04.decrypt": {
const { peer, ciphertext } = params;
return decrypt3(sk, peer, ciphertext);
return decrypt4(sk, peer, ciphertext);
}
case "nip44.encrypt": {
const { peer, plaintext } = params;
const key = getSharedSecret(sk, peer);
return v22.encrypt(plaintext, key);
}
case "nip44.decrypt": {
const { peer, ciphertext } = params;
const key = getSharedSecret(sk, peer);
return v22.decrypt(ciphertext, key);
}
}
} catch (error) {