498 lines
18 KiB
JavaScript
498 lines
18 KiB
JavaScript
// The cryptography SLYNG needs, with no dependencies: SHA-256, HMAC, HKDF, secp256k1 (BIP-340 Schnorr signatures
|
|
// and ECDH), ChaCha20, and NIP-44 v2 encryption. Pure functions over Uint8Array, usable in the browser and in Node.
|
|
//
|
|
// The curve arithmetic uses BigInt and is not constant-time. It signs the occasional event in a browser tab; for a
|
|
// key that holds real weight, sign with a nostr extension instead (SLYNG prefers one when it finds one).
|
|
|
|
const te = new TextEncoder();
|
|
const td = new TextDecoder('utf-8', { fatal: true });
|
|
|
|
export const utf8 = (s) => te.encode(s);
|
|
export const fromUtf8 = (b) => td.decode(b);
|
|
|
|
export function concat(...parts) {
|
|
let n = 0;
|
|
for (const p of parts) n += p.length;
|
|
const out = new Uint8Array(n);
|
|
let o = 0;
|
|
for (const p of parts) { out.set(p, o); o += p.length; }
|
|
return out;
|
|
}
|
|
|
|
export function hex(bytes) {
|
|
let s = '';
|
|
for (const b of bytes) s += b.toString(16).padStart(2, '0');
|
|
return s;
|
|
}
|
|
|
|
export function unhex(s) {
|
|
if (typeof s !== 'string' || s.length % 2 || /[^0-9a-f]/i.test(s)) throw new Error('not hex');
|
|
const out = new Uint8Array(s.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
|
|
return out;
|
|
}
|
|
|
|
export const isHex64 = (s) => typeof s === 'string' && /^[0-9a-f]{64}$/.test(s);
|
|
|
|
export function randomBytes(n) {
|
|
const out = new Uint8Array(n);
|
|
globalThis.crypto.getRandomValues(out);
|
|
return out;
|
|
}
|
|
|
|
export function equalBytes(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
let d = 0;
|
|
for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i];
|
|
return d === 0;
|
|
}
|
|
|
|
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
export function base64(bytes) {
|
|
let s = '';
|
|
let i = 0;
|
|
for (; i + 2 < bytes.length; i += 3) {
|
|
const n = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
|
s += B64[n >> 18] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + B64[n & 63];
|
|
}
|
|
const rest = bytes.length - i;
|
|
if (rest === 1) {
|
|
const n = bytes[i] << 16;
|
|
s += B64[n >> 18] + B64[(n >> 12) & 63] + '==';
|
|
} else if (rest === 2) {
|
|
const n = (bytes[i] << 16) | (bytes[i + 1] << 8);
|
|
s += B64[n >> 18] + B64[(n >> 12) & 63] + B64[(n >> 6) & 63] + '=';
|
|
}
|
|
return s;
|
|
}
|
|
|
|
export function unbase64(s) {
|
|
if (typeof s !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(s) || s.length % 4) throw new Error('not base64');
|
|
const pad = s.endsWith('==') ? 2 : s.endsWith('=') ? 1 : 0;
|
|
const out = new Uint8Array((s.length / 4) * 3 - pad);
|
|
let o = 0;
|
|
for (let i = 0; i < s.length; i += 4) {
|
|
const n = (B64.indexOf(s[i]) << 18) | (B64.indexOf(s[i + 1]) << 12) |
|
|
((s[i + 2] === '=' ? 0 : B64.indexOf(s[i + 2])) << 6) | (s[i + 3] === '=' ? 0 : B64.indexOf(s[i + 3]));
|
|
if (o < out.length) out[o++] = n >> 16;
|
|
if (o < out.length) out[o++] = (n >> 8) & 255;
|
|
if (o < out.length) out[o++] = n & 255;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------- SHA-256
|
|
|
|
const K256 = new Uint32Array([
|
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
]);
|
|
|
|
export function sha256(msg) {
|
|
const len = msg.length;
|
|
const total = ((len + 9 + 63) >> 6) << 6;
|
|
const buf = new Uint8Array(total);
|
|
buf.set(msg);
|
|
buf[len] = 0x80;
|
|
const dv = new DataView(buf.buffer);
|
|
dv.setUint32(total - 8, Math.floor(len / 0x20000000));
|
|
dv.setUint32(total - 4, (len << 3) >>> 0);
|
|
const H = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
const W = new Uint32Array(64);
|
|
for (let off = 0; off < total; off += 64) {
|
|
for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4);
|
|
for (let i = 16; i < 64; i++) {
|
|
const a = W[i - 15], b = W[i - 2];
|
|
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
|
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
|
W[i] = (W[i - 16] + s0 + W[i - 7] + s1) | 0;
|
|
}
|
|
let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4], f = H[5], g = H[6], h = H[7];
|
|
for (let i = 0; i < 64; i++) {
|
|
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
const ch = (e & f) ^ (~e & g);
|
|
const t1 = (h + S1 + ch + K256[i] + W[i]) | 0;
|
|
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
const t2 = (S0 + maj) | 0;
|
|
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
|
}
|
|
H[0] += a; H[1] += b; H[2] += c; H[3] += d; H[4] += e; H[5] += f; H[6] += g; H[7] += h;
|
|
}
|
|
const out = new Uint8Array(32);
|
|
const odv = new DataView(out.buffer);
|
|
for (let i = 0; i < 8; i++) odv.setUint32(i * 4, H[i]);
|
|
return out;
|
|
}
|
|
|
|
export function hmacSha256(key, msg) {
|
|
const k = new Uint8Array(64);
|
|
k.set(key.length > 64 ? sha256(key) : key);
|
|
const inner = new Uint8Array(64 + msg.length);
|
|
const outer = new Uint8Array(96);
|
|
for (let i = 0; i < 64; i++) { inner[i] = k[i] ^ 0x36; outer[i] = k[i] ^ 0x5c; }
|
|
inner.set(msg, 64);
|
|
outer.set(sha256(inner), 64);
|
|
return sha256(outer);
|
|
}
|
|
|
|
export const hkdfExtract = (salt, ikm) => hmacSha256(salt, ikm);
|
|
|
|
export function hkdfExpand(prk, info, length) {
|
|
if (length > 255 * 32) throw new Error('hkdf length');
|
|
const out = new Uint8Array(length);
|
|
let prev = new Uint8Array(0);
|
|
for (let i = 1, pos = 0; pos < length; i++, pos += 32) {
|
|
prev = hmacSha256(prk, concat(prev, info, Uint8Array.of(i)));
|
|
out.set(prev.subarray(0, Math.min(32, length - pos)), pos);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------- secp256k1
|
|
|
|
const P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
|
|
export const N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
const GX = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n;
|
|
const GY = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n;
|
|
|
|
const mod = (a, m = P) => { const r = a % m; return r >= 0n ? r : r + m; };
|
|
|
|
function inv(a, m = P) {
|
|
let r0 = mod(a, m), r1 = m, s0 = 1n, s1 = 0n;
|
|
while (r1 !== 0n) {
|
|
const q = r0 / r1;
|
|
[r0, r1] = [r1, r0 - q * r1];
|
|
[s0, s1] = [s1, s0 - q * s1];
|
|
}
|
|
if (r0 !== 1n) throw new Error('not invertible');
|
|
return mod(s0, m);
|
|
}
|
|
|
|
function powMod(b, e, m = P) {
|
|
let r = 1n;
|
|
b = mod(b, m);
|
|
while (e > 0n) {
|
|
if (e & 1n) r = (r * b) % m;
|
|
b = (b * b) % m;
|
|
e >>= 1n;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
const INF = { X: 0n, Y: 1n, Z: 0n };
|
|
|
|
function dbl(p) {
|
|
if (p.Z === 0n || p.Y === 0n) return INF;
|
|
const A = (p.X * p.X) % P;
|
|
const B = (p.Y * p.Y) % P;
|
|
const C = (B * B) % P;
|
|
const xb = p.X + B;
|
|
const D = mod(2n * (xb * xb - A - C));
|
|
const E = (3n * A) % P;
|
|
const F = (E * E) % P;
|
|
const X3 = mod(F - 2n * D);
|
|
const Y3 = mod(E * (D - X3) - 8n * C);
|
|
const Z3 = (2n * p.Y * p.Z) % P;
|
|
return { X: X3, Y: Y3, Z: Z3 };
|
|
}
|
|
|
|
function add(p, q) {
|
|
if (p.Z === 0n) return q;
|
|
if (q.Z === 0n) return p;
|
|
const Z1Z1 = (p.Z * p.Z) % P;
|
|
const Z2Z2 = (q.Z * q.Z) % P;
|
|
const U1 = (p.X * Z2Z2) % P;
|
|
const U2 = (q.X * Z1Z1) % P;
|
|
const S1 = (((p.Y * q.Z) % P) * Z2Z2) % P;
|
|
const S2 = (((q.Y * p.Z) % P) * Z1Z1) % P;
|
|
if (U1 === U2) return S1 === S2 ? dbl(p) : INF;
|
|
const H = mod(U2 - U1);
|
|
const R = mod(S2 - S1);
|
|
const H2 = (H * H) % P;
|
|
const H3 = (H * H2) % P;
|
|
const U1H2 = (U1 * H2) % P;
|
|
const X3 = mod(R * R - H3 - 2n * U1H2);
|
|
const Y3 = mod(R * (U1H2 - X3) - S1 * H3);
|
|
const Z3 = (((H * p.Z) % P) * q.Z) % P;
|
|
return { X: X3, Y: Y3, Z: Z3 };
|
|
}
|
|
|
|
const neg = (p) => (p.Z === 0n ? p : { X: p.X, Y: mod(-p.Y), Z: p.Z });
|
|
|
|
function affine(p) {
|
|
if (p.Z === 0n) return null;
|
|
const zi = inv(p.Z);
|
|
const zi2 = (zi * zi) % P;
|
|
return { x: (p.X * zi2) % P, y: (((p.Y * zi2) % P) * zi) % P };
|
|
}
|
|
|
|
// k·p with a 4-bit window: 64 windows of 4 doublings and one addition
|
|
function mul(p, k) {
|
|
k = mod(k, N);
|
|
if (k === 0n || p.Z === 0n) return INF;
|
|
const table = [INF, p];
|
|
for (let i = 2; i < 16; i++) table.push(add(table[i - 1], p));
|
|
let r = INF;
|
|
for (let shift = 252n; shift >= 0n; shift -= 4n) {
|
|
r = dbl(dbl(dbl(dbl(r))));
|
|
const nib = Number((k >> shift) & 15n);
|
|
if (nib) r = add(r, table[nib]);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
// Many points to Z = 1 with one inversion (Montgomery's trick)
|
|
function normalizeAll(points) {
|
|
const zs = points.map((p) => (p.Z === 0n ? 1n : p.Z));
|
|
const prefix = new Array(zs.length);
|
|
let acc = 1n;
|
|
for (let i = 0; i < zs.length; i++) { prefix[i] = acc; acc = (acc * zs[i]) % P; }
|
|
let invAcc = inv(acc);
|
|
const out = new Array(points.length);
|
|
for (let i = zs.length - 1; i >= 0; i--) {
|
|
const zi = (invAcc * prefix[i]) % P;
|
|
invAcc = (invAcc * zs[i]) % P;
|
|
const p = points[i];
|
|
if (p.Z === 0n) { out[i] = INF; continue; }
|
|
const zi2 = (zi * zi) % P;
|
|
out[i] = { X: (p.X * zi2) % P, Y: (((p.Y * zi2) % P) * zi) % P, Z: 1n };
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// G multiples, precomputed once: GT[w][j] = j·16^w·G, so k·G is 64 additions and no doublings
|
|
let GT = null;
|
|
function gTable() {
|
|
if (GT) return GT;
|
|
const flat = [];
|
|
let base = { X: GX, Y: GY, Z: 1n };
|
|
for (let w = 0; w < 64; w++) {
|
|
const row = [INF, base];
|
|
for (let j = 2; j < 16; j++) row.push(add(row[j - 1], base));
|
|
flat.push(...row);
|
|
base = dbl(dbl(dbl(dbl(base))));
|
|
}
|
|
const norm = normalizeAll(flat);
|
|
GT = [];
|
|
for (let w = 0; w < 64; w++) GT.push(norm.slice(w * 16, w * 16 + 16));
|
|
return GT;
|
|
}
|
|
|
|
function mulG(k) {
|
|
k = mod(k, N);
|
|
if (k === 0n) return INF;
|
|
const T = gTable();
|
|
let r = INF;
|
|
for (let w = 0; w < 64; w++) {
|
|
const nib = Number((k >> BigInt(w * 4)) & 15n);
|
|
if (nib) r = add(r, T[w][nib]);
|
|
}
|
|
return r;
|
|
}
|
|
|
|
export const bytesToBig = (b) => (b.length ? BigInt('0x' + hex(b)) : 0n);
|
|
export const bigToBytes = (n) => unhex(n.toString(16).padStart(64, '0'));
|
|
|
|
function liftX(x) {
|
|
if (x >= P) return null;
|
|
const c = mod(x * x * x + 7n);
|
|
const y = powMod(c, (P + 1n) / 4n);
|
|
if ((y * y) % P !== c) return null;
|
|
return { X: x, Y: (y & 1n) === 0n ? y : P - y, Z: 1n };
|
|
}
|
|
|
|
function privToBig(priv) {
|
|
const d = bytesToBig(priv);
|
|
if (priv.length !== 32 || d <= 0n || d >= N) throw new Error('bad private key');
|
|
return d;
|
|
}
|
|
|
|
export function isValidPrivateKey(priv) {
|
|
try { privToBig(priv); return true; } catch { return false; }
|
|
}
|
|
|
|
export function generatePrivateKey() {
|
|
for (;;) {
|
|
const k = randomBytes(32);
|
|
if (isValidPrivateKey(k)) return k;
|
|
}
|
|
}
|
|
|
|
// x-only public key (32 bytes), as nostr uses
|
|
export function getPublicKey(priv) {
|
|
return bigToBytes(affine(mulG(privToBig(priv))).x);
|
|
}
|
|
|
|
export function isValidPublicKey(pub) {
|
|
return pub.length === 32 && liftX(bytesToBig(pub)) !== null;
|
|
}
|
|
|
|
function taggedHash(tag, ...msgs) {
|
|
const t = sha256(utf8(tag));
|
|
return sha256(concat(t, t, ...msgs));
|
|
}
|
|
|
|
export function schnorrSign(msg, priv, aux = randomBytes(32)) {
|
|
const d0 = privToBig(priv);
|
|
const Pt = affine(mulG(d0));
|
|
const d = (Pt.y & 1n) === 0n ? d0 : N - d0;
|
|
const px = bigToBytes(Pt.x);
|
|
const ta = taggedHash('BIP0340/aux', aux);
|
|
const t = bigToBytes(d).map((b, i) => b ^ ta[i]);
|
|
const k0 = mod(bytesToBig(taggedHash('BIP0340/nonce', t, px, msg)), N);
|
|
if (k0 === 0n) throw new Error('nonce is zero');
|
|
const R = affine(mulG(k0));
|
|
const k = (R.y & 1n) === 0n ? k0 : N - k0;
|
|
const rx = bigToBytes(R.x);
|
|
const e = mod(bytesToBig(taggedHash('BIP0340/challenge', rx, px, msg)), N);
|
|
const sig = concat(rx, bigToBytes(mod(k + e * d, N)));
|
|
if (!schnorrVerify(sig, msg, px)) throw new Error('signature did not verify');
|
|
return sig;
|
|
}
|
|
|
|
export function schnorrVerify(sig, msg, pub) {
|
|
try {
|
|
if (sig.length !== 64 || pub.length !== 32) return false;
|
|
const Pt = liftX(bytesToBig(pub));
|
|
if (!Pt) return false;
|
|
const r = bytesToBig(sig.subarray(0, 32));
|
|
const s = bytesToBig(sig.subarray(32, 64));
|
|
if (r >= P || s >= N) return false;
|
|
const e = mod(bytesToBig(taggedHash('BIP0340/challenge', sig.subarray(0, 32), pub, msg)), N);
|
|
const R = affine(add(mulG(s), neg(mul(Pt, e))));
|
|
if (!R || (R.y & 1n) !== 0n) return false;
|
|
return R.x === r;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ECDH as NIP-04 and NIP-44 use it: the unhashed x coordinate of priv·pub
|
|
export function sharedX(priv, pubX) {
|
|
const Pt = liftX(bytesToBig(pubX));
|
|
if (!Pt) throw new Error('bad public key');
|
|
return bigToBytes(affine(mul(Pt, privToBig(priv))).x);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------- ChaCha20
|
|
|
|
function rotl(v, c) { return (v << c) | (v >>> (32 - c)); }
|
|
|
|
function quarter(s, a, b, c, d) {
|
|
s[a] = (s[a] + s[b]) | 0; s[d] = rotl(s[d] ^ s[a], 16);
|
|
s[c] = (s[c] + s[d]) | 0; s[b] = rotl(s[b] ^ s[c], 12);
|
|
s[a] = (s[a] + s[b]) | 0; s[d] = rotl(s[d] ^ s[a], 8);
|
|
s[c] = (s[c] + s[d]) | 0; s[b] = rotl(s[b] ^ s[c], 7);
|
|
}
|
|
|
|
// RFC 8439 ChaCha20: 32-byte key, 12-byte nonce, 32-bit block counter
|
|
export function chacha20(key, nonce, data, counter = 0) {
|
|
if (key.length !== 32 || nonce.length !== 12) throw new Error('chacha20 key or nonce');
|
|
const kv = new DataView(key.buffer, key.byteOffset, 32);
|
|
const nv = new DataView(nonce.buffer, nonce.byteOffset, 12);
|
|
const init = new Int32Array(16);
|
|
init[0] = 0x61707865; init[1] = 0x3320646e; init[2] = 0x79622d32; init[3] = 0x6b206574;
|
|
for (let i = 0; i < 8; i++) init[4 + i] = kv.getUint32(i * 4, true);
|
|
for (let i = 0; i < 3; i++) init[13 + i] = nv.getUint32(i * 4, true);
|
|
const out = new Uint8Array(data.length);
|
|
const s = new Int32Array(16);
|
|
const block = new Uint8Array(64);
|
|
const bv = new DataView(block.buffer);
|
|
for (let off = 0; off < data.length; off += 64) {
|
|
init[12] = counter++;
|
|
s.set(init);
|
|
for (let i = 0; i < 10; i++) {
|
|
quarter(s, 0, 4, 8, 12); quarter(s, 1, 5, 9, 13); quarter(s, 2, 6, 10, 14); quarter(s, 3, 7, 11, 15);
|
|
quarter(s, 0, 5, 10, 15); quarter(s, 1, 6, 11, 12); quarter(s, 2, 7, 8, 13); quarter(s, 3, 4, 9, 14);
|
|
}
|
|
for (let i = 0; i < 16; i++) bv.setUint32(i * 4, (s[i] + init[i]) | 0, true);
|
|
const n = Math.min(64, data.length - off);
|
|
for (let i = 0; i < n; i++) out[off + i] = data[off + i] ^ block[i];
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------- NIP-44 v2
|
|
|
|
export function nip44ConversationKey(priv, pubX) {
|
|
return hkdfExtract(utf8('nip44-v2'), sharedX(priv, pubX));
|
|
}
|
|
|
|
export function calcPaddedLen(len) {
|
|
if (len < 1) throw new Error('empty message');
|
|
if (len <= 32) return 32;
|
|
const nextPower = 2 ** (Math.floor(Math.log2(len - 1)) + 1);
|
|
const chunk = nextPower <= 256 ? 32 : nextPower / 8;
|
|
return chunk * (Math.floor((len - 1) / chunk) + 1);
|
|
}
|
|
|
|
function messageKeys(conversationKey, nonce) {
|
|
const k = hkdfExpand(conversationKey, nonce, 76);
|
|
return { chachaKey: k.subarray(0, 32), chachaNonce: k.subarray(32, 44), hmacKey: k.subarray(44, 76) };
|
|
}
|
|
|
|
function pad(text) {
|
|
const u = utf8(text);
|
|
if (u.length < 1 || u.length > 65535) throw new Error('message must be 1 to 65535 bytes');
|
|
const out = new Uint8Array(2 + calcPaddedLen(u.length));
|
|
out[0] = u.length >> 8;
|
|
out[1] = u.length & 255;
|
|
out.set(u, 2);
|
|
return out;
|
|
}
|
|
|
|
function unpad(padded) {
|
|
const len = (padded[0] << 8) | padded[1];
|
|
if (len < 1 || padded.length !== 2 + calcPaddedLen(len)) throw new Error('bad padding');
|
|
return fromUtf8(padded.subarray(2, 2 + len));
|
|
}
|
|
|
|
export function nip44Encrypt(text, conversationKey, nonce = randomBytes(32)) {
|
|
const { chachaKey, chachaNonce, hmacKey } = messageKeys(conversationKey, nonce);
|
|
const ct = chacha20(chachaKey, chachaNonce, pad(text));
|
|
const mac = hmacSha256(hmacKey, concat(nonce, ct));
|
|
return base64(concat(Uint8Array.of(2), nonce, ct, mac));
|
|
}
|
|
|
|
export function nip44Decrypt(payload, conversationKey) {
|
|
if (typeof payload !== 'string' || payload.startsWith('#')) throw new Error('unsupported encryption version');
|
|
if (payload.length < 132 || payload.length > 87472) throw new Error('bad payload size');
|
|
const data = unbase64(payload);
|
|
if (data.length < 99 || data.length > 65603) throw new Error('bad payload size');
|
|
if (data[0] !== 2) throw new Error('unsupported encryption version');
|
|
const nonce = data.subarray(1, 33);
|
|
const ct = data.subarray(33, data.length - 32);
|
|
const mac = data.subarray(data.length - 32);
|
|
const { chachaKey, chachaNonce, hmacKey } = messageKeys(conversationKey, nonce);
|
|
if (!equalBytes(hmacSha256(hmacKey, concat(nonce, ct)), mac)) throw new Error('bad mac');
|
|
return unpad(chacha20(chachaKey, chachaNonce, ct));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------------- NIP-04 (for wallets)
|
|
|
|
// AES-256-CBC with the raw ECDH x as key. Only Nostr Wallet Connect still needs it, for wallets without NIP-44.
|
|
async function aesKey(priv, pubX, usage) {
|
|
return globalThis.crypto.subtle.importKey('raw', sharedX(priv, pubX), { name: 'AES-CBC' }, false, [usage]);
|
|
}
|
|
|
|
export async function nip04Encrypt(text, priv, pubX, iv = randomBytes(16)) {
|
|
const key = await aesKey(priv, pubX, 'encrypt');
|
|
const ct = new Uint8Array(await globalThis.crypto.subtle.encrypt({ name: 'AES-CBC', iv }, key, utf8(text)));
|
|
return base64(ct) + '?iv=' + base64(iv);
|
|
}
|
|
|
|
export async function nip04Decrypt(payload, priv, pubX) {
|
|
const [ct, ivPart] = String(payload).split('?iv=');
|
|
if (!ivPart) throw new Error('not a NIP-04 payload');
|
|
const key = await aesKey(priv, pubX, 'decrypt');
|
|
const pt = await globalThis.crypto.subtle.decrypt({ name: 'AES-CBC', iv: unbase64(ivPart) }, key, unbase64(ct));
|
|
return fromUtf8(new Uint8Array(pt));
|
|
}
|