UNPKG

@protontech/openpgp

Version:

OpenPGP.js is a Javascript implementation of the OpenPGP protocol. This is defined in RFC 4880.

1,003 lines (991 loc) 41.5 kB
/*! OpenPGP.js v6.1.1-patch.4 - 2025-07-14 - this is LGPL licensed code, see LICENSE/our website https://openpgpjs.org/ for more information. */ const globalThis = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; import { c as abytes, r as randomBytes$1, l as shake128, s as shake256, f as sha3_256, g as sha3_512, m as u32, d as concatBytes } from './sha3.mjs'; /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ const ensureBytes = abytes; const randomBytes = randomBytes$1; // Compares 2 u8a-s in kinda constant time function equalBytes(a, b) { if (a.length !== b.length) return false; let diff = 0; for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; return diff === 0; } function splitCoder(...lengths) { const getLength = (c) => (typeof c === 'number' ? c : c.bytesLen); const bytesLen = lengths.reduce((sum, a) => sum + getLength(a), 0); return { bytesLen, encode: (bufs) => { const res = new Uint8Array(bytesLen); for (let i = 0, pos = 0; i < lengths.length; i++) { const c = lengths[i]; const l = getLength(c); const b = typeof c === 'number' ? bufs[i] : c.encode(bufs[i]); ensureBytes(b, l); res.set(b, pos); if (typeof c !== 'number') b.fill(0); // clean pos += l; } return res; }, decode: (buf) => { ensureBytes(buf, bytesLen); const res = []; for (const c of lengths) { const l = getLength(c); const b = buf.subarray(0, l); res.push(typeof c === 'number' ? b : c.decode(b)); buf = buf.subarray(l); } return res; }, }; } // nano-packed.array (fixed size) function vecCoder(c, vecLen) { const bytesLen = vecLen * c.bytesLen; return { bytesLen, encode: (u) => { if (u.length !== vecLen) throw new Error(`vecCoder.encode: wrong length=${u.length}. Expected: ${vecLen}`); const res = new Uint8Array(bytesLen); for (let i = 0, pos = 0; i < u.length; i++) { const b = c.encode(u[i]); res.set(b, pos); b.fill(0); // clean pos += b.length; } return res; }, decode: (a) => { ensureBytes(a, bytesLen); const r = []; for (let i = 0; i < a.length; i += c.bytesLen) r.push(c.decode(a.subarray(i, i + c.bytesLen))); return r; }, }; } // cleanBytes(new Uint8Array(), [new Uint16Array(), new Uint32Array()]) function cleanBytes(...list) { for (const t of list) { if (Array.isArray(t)) for (const b of t) b.fill(0); else t.fill(0); } } function getMask(bits) { return (1 << bits) - 1; // 4 -> 0b1111 } /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ // TODO: benchmark function bitReversal(n, bits = 8) { const padded = n.toString(2).padStart(8, '0'); const sliced = padded.slice(-bits).padStart(7, '0'); const revrsd = sliced.split('').reverse().join(''); return Number.parseInt(revrsd, 2); } const genCrystals = (opts) => { // isKyber: true means Kyber, false means Dilithium const { newPoly, N, Q, F, ROOT_OF_UNITY, brvBits, isKyber } = opts; const mod = (a, modulo = Q) => { const result = a % modulo | 0; return (result >= 0 ? result | 0 : (modulo + result) | 0) | 0; }; // -(Q-1)/2 < a <= (Q-1)/2 const smod = (a, modulo = Q) => { const r = mod(a, modulo) | 0; return (r > modulo >> 1 ? (r - modulo) | 0 : r) | 0; }; // Generate zettas function getZettas() { const out = newPoly(N); for (let i = 0; i < N; i++) { const b = bitReversal(i, brvBits); const p = BigInt(ROOT_OF_UNITY) ** BigInt(b) % BigInt(Q); out[i] = Number(p) | 0; } return out; } const nttZetas = getZettas(); // Number-Theoretic Transform // Explained: https://electricdusk.com/ntt.html // Kyber has slightly different params, since there is no 512th primitive root of unity mod q, // only 256th primitive root of unity mod. Which also complicates MultiplyNTT. // TODO: there should be less ugly way to define this. const LEN1 = isKyber ? 128 : N; const LEN2 = isKyber ? 1 : 0; const NTT = { encode: (r) => { for (let k = 1, len = 128; len > LEN2; len >>= 1) { for (let start = 0; start < N; start += 2 * len) { const zeta = nttZetas[k++]; for (let j = start; j < start + len; j++) { const t = mod(zeta * r[j + len]); r[j + len] = mod(r[j] - t) | 0; r[j] = mod(r[j] + t) | 0; } } } return r; }, decode: (r) => { for (let k = LEN1 - 1, len = 1 + LEN2; len < LEN1 + LEN2; len <<= 1) { for (let start = 0; start < N; start += 2 * len) { const zeta = nttZetas[k--]; for (let j = start; j < start + len; j++) { const t = r[j]; r[j] = mod(t + r[j + len]); r[j + len] = mod(zeta * (r[j + len] - t)); } } } for (let i = 0; i < r.length; i++) r[i] = mod(F * r[i]); return r; }, }; // Encode polynominal as bits const bitsCoder = (d, c) => { const mask = getMask(d); const bytesLen = d * (N / 8); return { bytesLen, encode: (poly) => { const r = new Uint8Array(bytesLen); for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < poly.length; i++) { buf |= (c.encode(poly[i]) & mask) << bufLen; bufLen += d; for (; bufLen >= 8; bufLen -= 8, buf >>= 8) r[pos++] = buf & getMask(bufLen); } return r; }, decode: (bytes) => { const r = newPoly(N); for (let i = 0, buf = 0, bufLen = 0, pos = 0; i < bytes.length; i++) { buf |= bytes[i] << bufLen; bufLen += 8; for (; bufLen >= d; bufLen -= d, buf >>= d) r[pos++] = c.decode(buf & mask); } return r; }, }; }; return { mod, smod, nttZetas, NTT, bitsCoder }; }; const createXofShake = (shake) => (seed, blockLen) => { if (!blockLen) blockLen = shake.blockLen; // Optimizations that won't mater: // - cached seed update (two .update(), on start and on the end) // - another cache which cloned into working copy // Faster than multiple updates, since seed less than blockLen const _seed = new Uint8Array(seed.length + 2); _seed.set(seed); const seedLen = seed.length; const buf = new Uint8Array(blockLen); // == shake128.blockLen let h = shake.create({}); let calls = 0; let xofs = 0; return { stats: () => ({ calls, xofs }), get: (x, y) => { _seed[seedLen + 0] = x; _seed[seedLen + 1] = y; h.destroy(); h = shake.create({}).update(_seed); calls++; return () => { xofs++; return h.xofInto(buf); }; }, clean: () => { h.destroy(); buf.fill(0); _seed.fill(0); }, }; }; const XOF128 = /* @__PURE__ */ createXofShake(shake128); const XOF256 = /* @__PURE__ */ createXofShake(shake256); /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ /* Lattice-based key encapsulation mechanism. See [official site](https://www.pq-crystals.org/kyber/resources.shtml), [repo](https://github.com/pq-crystals/kyber), [spec](https://datatracker.ietf.org/doc/draft-cfrg-schwabe-kyber/). Key encapsulation is similar to DH / ECDH (think X25519), with important differences: - We can't verify if it was "Bob" who've sent the shared secret. In ECDH, it's always verified - Kyber is probabalistic and relies on quality of randomness (CSPRNG). ECDH doesn't (to this extent). - Kyber decapsulation never throws an error, even when shared secret was encrypted by a different public key. It will just return a different shared secret There are some concerns with regards to security: see [djb blog](https://blog.cr.yp.to/20231003-countcorrectly.html) and [mailing list](https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/W2VOzy0wz_E). */ const N$1 = 256; // Kyber (not FIPS-203) supports different lengths, but all std modes were using 256 const Q$1 = 3329; // 13*(2**8)+1, modulo prime const F$1 = 3303; // 3303 ≡ 128**(−1) mod q (FIPS-203) const ROOT_OF_UNITY$1 = 17; // ζ = 17 ∈ Zq is a primitive 256-th root of unity modulo Q. ζ**128 ≡−1 const { mod: mod$1, nttZetas, NTT: NTT$1, bitsCoder: bitsCoder$1 } = genCrystals({ N: N$1, Q: Q$1, F: F$1, ROOT_OF_UNITY: ROOT_OF_UNITY$1, newPoly: (n) => new Uint16Array(n), brvBits: 7, isKyber: true, }); // prettier-ignore const PARAMS$1 = { 512: { N: N$1, Q: Q$1, K: 2, ETA1: 3, ETA2: 2, du: 10, dv: 4, RBGstrength: 128 }, 768: { N: N$1, Q: Q$1, K: 3, ETA1: 2, ETA2: 2, du: 10, dv: 4, RBGstrength: 192 }, 1024: { N: N$1, Q: Q$1, K: 4, ETA1: 2, ETA2: 2, du: 11, dv: 5, RBGstrength: 256 }, }; // FIPS-203: compress/decompress const compress = (d) => { // Special case, no need to compress, pass as is, but strip high bytes on compression if (d >= 12) return { encode: (i) => i, decode: (i) => i }; // NOTE: we don't use float arithmetic (forbidden by FIPS-203 and high chance of bugs). // Comments map to python implementation in RFC (draft-cfrg-schwabe-kyber) // const round = (i: number) => Math.floor(i + 0.5) | 0; const a = 2 ** (d - 1); return { // const compress = (i: number) => round((2 ** d / Q) * i) % 2 ** d; encode: (i) => ((i << d) + Q$1 / 2) / Q$1, // const decompress = (i: number) => round((Q / 2 ** d) * i); decode: (i) => (i * Q$1 + a) >>> d, }; }; // NOTE: we merge encoding and compress because it is faster, also both require same d param // Converts between bytes and d-bits compressed representation. Kinda like convertRadix2 from @scure/base // decode(encode(t)) == t, but there is loss of information on encode(decode(t)) const polyCoder$1 = (d) => bitsCoder$1(d, compress(d)); function polyAdd$1(a, b) { for (let i = 0; i < N$1; i++) a[i] = mod$1(a[i] + b[i]); // a += b } function polySub$1(a, b) { for (let i = 0; i < N$1; i++) a[i] = mod$1(a[i] - b[i]); // a -= b } // FIPS-203: Computes the product of two degree-one polynomials with respect to a quadratic modulus function BaseCaseMultiply(a0, a1, b0, b1, zeta) { const c0 = mod$1(a1 * b1 * zeta + a0 * b0); const c1 = mod$1(a0 * b1 + a1 * b0); return { c0, c1 }; } // FIPS-203: Computes the product (in the ring Tq) of two NTT representations. NOTE: works inplace for f // NOTE: since multiply defined only for NTT representation, we need to convert to NTT, multiply and convert back function MultiplyNTTs$1(f, g) { for (let i = 0; i < N$1 / 2; i++) { let z = nttZetas[64 + (i >> 1)]; if (i & 1) z = -z; const { c0, c1 } = BaseCaseMultiply(f[2 * i + 0], f[2 * i + 1], g[2 * i + 0], g[2 * i + 1], z); f[2 * i + 0] = c0; f[2 * i + 1] = c1; } return f; } // Return poly in NTT representation function SampleNTT(xof) { const r = new Uint16Array(N$1); for (let j = 0; j < N$1;) { const b = xof(); if (b.length % 3) throw new Error('SampleNTT: unaligned block'); for (let i = 0; j < N$1 && i + 3 <= b.length; i += 3) { const d1 = ((b[i + 0] >> 0) | (b[i + 1] << 8)) & 0xfff; const d2 = ((b[i + 1] >> 4) | (b[i + 2] << 4)) & 0xfff; if (d1 < Q$1) r[j++] = d1; if (j < N$1 && d2 < Q$1) r[j++] = d2; } } return r; } // Sampling from the centered binomial distribution // Returns poly with small coefficients (noise/errors) function sampleCBD(PRF, seed, nonce, eta) { const buf = PRF((eta * N$1) / 4, seed, nonce); const r = new Uint16Array(N$1); const b32 = u32(buf); let len = 0; for (let i = 0, p = 0, bb = 0, t0 = 0; i < b32.length; i++) { let b = b32[i]; for (let j = 0; j < 32; j++) { bb += b & 1; b >>= 1; len += 1; if (len === eta) { t0 = bb; bb = 0; } else if (len === 2 * eta) { r[p++] = mod$1(t0 - bb); bb = 0; len = 0; } } } if (len) throw new Error(`sampleCBD: leftover bits: ${len}`); return r; } // K-PKE // As per FIPS-203, it doesn't perform any input validation and can't be used in standalone fashion. const genKPKE = (opts) => { const { K, PRF, XOF, HASH512, ETA1, ETA2, du, dv } = opts; const poly1 = polyCoder$1(1); const polyV = polyCoder$1(dv); const polyU = polyCoder$1(du); const publicCoder = splitCoder(vecCoder(polyCoder$1(12), K), 32); const secretCoder = vecCoder(polyCoder$1(12), K); const cipherCoder = splitCoder(vecCoder(polyU, K), polyV); const seedCoder = splitCoder(32, 32); return { secretCoder, secretKeyLen: secretCoder.bytesLen, publicKeyLen: publicCoder.bytesLen, cipherTextLen: cipherCoder.bytesLen, keygen: (seed) => { const seedDst = new Uint8Array(33); seedDst.set(seed); seedDst[32] = K; const seedHash = HASH512(seedDst); const [rho, sigma] = seedCoder.decode(seedHash); const sHat = []; const tHat = []; for (let i = 0; i < K; i++) sHat.push(NTT$1.encode(sampleCBD(PRF, sigma, i, ETA1))); const x = XOF(rho); for (let i = 0; i < K; i++) { const e = NTT$1.encode(sampleCBD(PRF, sigma, K + i, ETA1)); for (let j = 0; j < K; j++) { const aji = SampleNTT(x.get(j, i)); // A[j][i], inplace polyAdd$1(e, MultiplyNTTs$1(aji, sHat[j])); } tHat.push(e); // t ← A ◦ s + e } x.clean(); const res = { publicKey: publicCoder.encode([tHat, rho]), secretKey: secretCoder.encode(sHat), }; cleanBytes(rho, sigma, sHat, tHat, seedDst, seedHash); return res; }, encrypt: (publicKey, msg, seed) => { const [tHat, rho] = publicCoder.decode(publicKey); const rHat = []; for (let i = 0; i < K; i++) rHat.push(NTT$1.encode(sampleCBD(PRF, seed, i, ETA1))); const x = XOF(rho); const tmp2 = new Uint16Array(N$1); const u = []; for (let i = 0; i < K; i++) { const e1 = sampleCBD(PRF, seed, K + i, ETA2); const tmp = new Uint16Array(N$1); for (let j = 0; j < K; j++) { const aij = SampleNTT(x.get(i, j)); // A[i][j], inplace polyAdd$1(tmp, MultiplyNTTs$1(aij, rHat[j])); // t += aij * rHat[j] } polyAdd$1(e1, NTT$1.decode(tmp)); // e1 += tmp u.push(e1); polyAdd$1(tmp2, MultiplyNTTs$1(tHat[i], rHat[i])); // t2 += tHat[i] * rHat[i] tmp.fill(0); } x.clean(); const e2 = sampleCBD(PRF, seed, 2 * K, ETA2); polyAdd$1(e2, NTT$1.decode(tmp2)); // e2 += tmp2 const v = poly1.decode(msg); // encode plaintext m into polynomial v polyAdd$1(v, e2); // v += e2 cleanBytes(tHat, rHat, tmp2, e2); return cipherCoder.encode([u, v]); }, decrypt: (cipherText, privateKey) => { const [u, v] = cipherCoder.decode(cipherText); const sk = secretCoder.decode(privateKey); // s ← ByteDecode_12(dkPKE) const tmp = new Uint16Array(N$1); for (let i = 0; i < K; i++) polyAdd$1(tmp, MultiplyNTTs$1(sk[i], NTT$1.encode(u[i]))); // tmp += sk[i] * u[i] polySub$1(v, NTT$1.decode(tmp)); // v += tmp cleanBytes(tmp, sk, u); return poly1.encode(v); }, }; }; function createKyber(opts) { const KPKE = genKPKE(opts); const { HASH256, HASH512, KDF } = opts; const { secretCoder: KPKESecretCoder, cipherTextLen } = KPKE; const publicKeyLen = KPKE.publicKeyLen; // 384*K+32 const secretCoder = splitCoder(KPKE.secretKeyLen, KPKE.publicKeyLen, 32, 32); const secretKeyLen = secretCoder.bytesLen; const msgLen = 32; return { publicKeyLen, msgLen, keygen: (seed = randomBytes(64)) => { ensureBytes(seed, 64); const { publicKey, secretKey: sk } = KPKE.keygen(seed.subarray(0, 32)); const publicKeyHash = HASH256(publicKey); // (dkPKE||ek||H(ek)||z) const secretKey = secretCoder.encode([sk, publicKey, publicKeyHash, seed.subarray(32)]); cleanBytes(sk, publicKeyHash); return { publicKey, secretKey }; }, encapsulate: (publicKey, msg = randomBytes(32)) => { ensureBytes(publicKey, publicKeyLen); ensureBytes(msg, msgLen); // FIPS-203 includes additional verification check for modulus const eke = publicKey.subarray(0, 384 * opts.K); const ek = KPKESecretCoder.encode(KPKESecretCoder.decode(eke.slice())); // Copy because of inplace encoding // (Modulus check.) Perform the computation ek ← ByteEncode12(ByteDecode12(eke)). // If ek = ̸ eke, the input is invalid. (See Section 4.2.1.) if (!equalBytes(ek, eke)) { cleanBytes(ek); throw new Error('ML-KEM.encapsulate: wrong publicKey modulus'); } cleanBytes(ek); const kr = HASH512.create().update(msg).update(HASH256(publicKey)).digest(); // derive randomness const cipherText = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); kr.subarray(32).fill(0); return { cipherText, sharedSecret: kr.subarray(0, 32) }; }, decapsulate: (cipherText, secretKey) => { ensureBytes(secretKey, secretKeyLen); // 768*k + 96 ensureBytes(cipherText, cipherTextLen); // 32(du*k + dv) const [sk, publicKey, publicKeyHash, z] = secretCoder.decode(secretKey); const msg = KPKE.decrypt(cipherText, sk); const kr = HASH512.create().update(msg).update(publicKeyHash).digest(); // derive randomness, Khat, rHat = G(mHat || h) const Khat = kr.subarray(0, 32); const cipherText2 = KPKE.encrypt(publicKey, msg, kr.subarray(32, 64)); // re-encrypt using the derived randomness const isValid = equalBytes(cipherText, cipherText2); // if ciphertexts do not match, “implicitly reject” const Kbar = KDF.create({ dkLen: 32 }).update(z).update(cipherText).digest(); cleanBytes(msg, cipherText2, !isValid ? Khat : Kbar); return isValid ? Khat : Kbar; }, }; } function shakePRF(dkLen, key, nonce) { return shake256 .create({ dkLen }) .update(key) .update(new Uint8Array([nonce])) .digest(); } const opts = { HASH256: sha3_256, HASH512: sha3_512, KDF: shake256, XOF: XOF128, PRF: shakePRF, }; const ml_kem768 = /* @__PURE__ */ createKyber({ ...opts, ...PARAMS$1[768], }); /*! noble-post-quantum - MIT License (c) 2024 Paul Miller (paulmillr.com) */ /* Lattice-based digital signature algorithm. See [official site](https://www.pq-crystals.org/dilithium/index.shtml), [repo](https://github.com/pq-crystals/dilithium). Dilithium has similar internals to Kyber, but their keys and params are different. */ // Constants const N = 256; // 2**23 − 2**13 + 1, 23 bits: multiply will be 46. We have enough precision in JS to avoid bigints const Q = 8380417; const ROOT_OF_UNITY = 1753; // f = 256**−1 mod q, pow(256, -1, q) = 8347681 (python3) const F = 8347681; const D = 13; // Dilithium is kinda parametrized over GAMMA2, but everything will break with any other value. const GAMMA2_1 = Math.floor((Q - 1) / 88) | 0; const GAMMA2_2 = Math.floor((Q - 1) / 32) | 0; // prettier-ignore const PARAMS = { 2: { K: 4, L: 4, D, GAMMA1: 2 ** 17, GAMMA2: GAMMA2_1, TAU: 39, ETA: 2, OMEGA: 80 }, 3: { K: 6, L: 5, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 49, ETA: 4, OMEGA: 55 }, 5: { K: 8, L: 7, D, GAMMA1: 2 ** 19, GAMMA2: GAMMA2_2, TAU: 60, ETA: 2, OMEGA: 75 }, }; const newPoly = (n) => new Int32Array(n); const { mod, smod, NTT, bitsCoder } = genCrystals({ N, Q, F, ROOT_OF_UNITY, newPoly, isKyber: false, brvBits: 8, }); const id = (n) => n; const polyCoder = (d, compress = id, verify = id) => bitsCoder(d, { encode: (i) => compress(verify(i)), decode: (i) => verify(compress(i)), }); const polyAdd = (a, b) => { for (let i = 0; i < a.length; i++) a[i] = mod(a[i] + b[i]); return a; }; const polySub = (a, b) => { for (let i = 0; i < a.length; i++) a[i] = mod(a[i] - b[i]); return a; }; const polyShiftl = (p) => { for (let i = 0; i < N; i++) p[i] <<= D; return p; }; const polyChknorm = (p, B) => { // Not very sure about this, but FIPS204 doesn't provide any function for that :( for (let i = 0; i < N; i++) if (Math.abs(smod(p[i])) >= B) return true; return false; }; const MultiplyNTTs = (a, b) => { // NOTE: we don't use montgomery reduction in code, since it requires 64 bit ints, // which is not available in JS. mod(a[i] * b[i]) is ok, since Q is 23 bit, // which means a[i] * b[i] is 46 bit, which is safe to use in JS. (number is 53 bits). // Barrett reduction is slower than mod :( const c = newPoly(N); for (let i = 0; i < a.length; i++) c[i] = mod(a[i] * b[i]); return c; }; // Return poly in NTT representation function RejNTTPoly(xof) { // Samples a polynomial ∈ Tq. const r = newPoly(N); // NOTE: we can represent 3xu24 as 4xu32, but it doesn't improve perf :( for (let j = 0; j < N;) { const b = xof(); if (b.length % 3) throw new Error('RejNTTPoly: unaligned block'); for (let i = 0; j < N && i <= b.length - 3; i += 3) { const t = (b[i + 0] | (b[i + 1] << 8) | (b[i + 2] << 16)) & 0x7fffff; // 3 bytes if (t < Q) r[j++] = t; } } return r; } const EMPTY = new Uint8Array(0); function getDilithium(opts) { const { K, L, GAMMA1, GAMMA2, TAU, ETA, OMEGA } = opts; const { CRH_BYTES, TR_BYTES, C_TILDE_BYTES, XOF128, XOF256 } = opts; if (![2, 4].includes(ETA)) throw new Error('Wrong ETA'); if (![1 << 17, 1 << 19].includes(GAMMA1)) throw new Error('Wrong GAMMA1'); if (![GAMMA2_1, GAMMA2_2].includes(GAMMA2)) throw new Error('Wrong GAMMA2'); const BETA = TAU * ETA; const decompose = (r) => { // Decomposes r into (r1, r0) such that r ≡ r1(2γ2) + r0 mod q. const rPlus = mod(r); const r0 = smod(rPlus, 2 * GAMMA2) | 0; if (rPlus - r0 === Q - 1) return { r1: 0 | 0, r0: (r0 - 1) | 0 }; const r1 = Math.floor((rPlus - r0) / (2 * GAMMA2)) | 0; return { r1, r0 }; // r1 = HighBits, r0 = LowBits }; const HighBits = (r) => decompose(r).r1; const LowBits = (r) => decompose(r).r0; const MakeHint = (z, r) => { // Compute hint bit indicating whether adding z to r alters the high bits of r. // From dilithium code const res0 = z <= GAMMA2 || z > Q - GAMMA2 || (z === Q - GAMMA2 && r === 0) ? 0 : 1; // from FIPS204: // // const r1 = HighBits(r); // // const v1 = HighBits(r + z); // // const res1 = +(r1 !== v1); // But they return different results! However, decompose is same. // So, either there is a bug in Dilithium ref implementation or in FIPS204. // For now, lets use dilithium one, so test vectors can be passed. // See // https://github.com/GiacomoPope/dilithium-py?tab=readme-ov-file#optimising-decomposition-and-making-hints return res0; }; const UseHint = (h, r) => { // Returns the high bits of r adjusted according to hint h const m = Math.floor((Q - 1) / (2 * GAMMA2)); const { r1, r0 } = decompose(r); // 3: if h = 1 and r0 > 0 return (r1 + 1) mod m // 4: if h = 1 and r0 ≤ 0 return (r1 − 1) mod m if (h === 1) return r0 > 0 ? mod(r1 + 1, m) | 0 : mod(r1 - 1, m) | 0; return r1 | 0; }; const Power2Round = (r) => { // Decomposes r into (r1, r0) such that r ≡ r1*(2**d) + r0 mod q. const rPlus = mod(r); const r0 = smod(rPlus, 2 ** D) | 0; return { r1: Math.floor((rPlus - r0) / 2 ** D) | 0, r0 }; }; const hintCoder = { bytesLen: OMEGA + K, encode: (h) => { if (h === false) throw new Error('hint.encode: hint is false'); // should never happen const res = new Uint8Array(OMEGA + K); for (let i = 0, k = 0; i < K; i++) { for (let j = 0; j < N; j++) if (h[i][j] !== 0) res[k++] = j; res[OMEGA + i] = k; } return res; }, decode: (buf) => { const h = []; let k = 0; for (let i = 0; i < K; i++) { const hi = newPoly(N); if (buf[OMEGA + i] < k || buf[OMEGA + i] > OMEGA) return false; for (let j = k; j < buf[OMEGA + i]; j++) { if (j > k && buf[j] <= buf[j - 1]) return false; hi[buf[j]] = 1; } k = buf[OMEGA + i]; h.push(hi); } for (let j = k; j < OMEGA; j++) if (buf[j] !== 0) return false; return h; }, }; const ETACoder = polyCoder(ETA === 2 ? 3 : 4, (i) => ETA - i, (i) => { if (!(-ETA <= i && i <= ETA)) throw new Error(`malformed key s1/s3 ${i} outside of ETA range [${-ETA}, ${ETA}]`); return i; }); const T0Coder = polyCoder(13, (i) => (1 << (D - 1)) - i); const T1Coder = polyCoder(10); // Requires smod. Need to fix! const ZCoder = polyCoder(GAMMA1 === 1 << 17 ? 18 : 20, (i) => smod(GAMMA1 - i)); const W1Coder = polyCoder(GAMMA2 === GAMMA2_1 ? 6 : 4); const W1Vec = vecCoder(W1Coder, K); // Main structures const publicCoder = splitCoder(32, vecCoder(T1Coder, K)); const secretCoder = splitCoder(32, 32, TR_BYTES, vecCoder(ETACoder, L), vecCoder(ETACoder, K), vecCoder(T0Coder, K)); const sigCoder = splitCoder(C_TILDE_BYTES, vecCoder(ZCoder, L), hintCoder); const CoefFromHalfByte = ETA === 2 ? (n) => (n < 15 ? 2 - (n % 5) : false) : (n) => (n < 9 ? 4 - n : false); // Return poly in NTT representation function RejBoundedPoly(xof) { // Samples an element a ∈ Rq with coeffcients in [−η, η] computed via rejection sampling from ρ. const r = newPoly(N); for (let j = 0; j < N;) { const b = xof(); for (let i = 0; j < N && i < b.length; i += 1) { // half byte. Should be superfast with vector instructions. But very slow with js :( const d1 = CoefFromHalfByte(b[i] & 0x0f); const d2 = CoefFromHalfByte((b[i] >> 4) & 0x0f); if (d1 !== false) r[j++] = d1; if (j < N && d2 !== false) r[j++] = d2; } } return r; } const SampleInBall = (seed) => { // Samples a polynomial c ∈ Rq with coeffcients from {−1, 0, 1} and Hamming weight τ const pre = newPoly(N); const s = shake256.create({}).update(seed); const buf = new Uint8Array(shake256.blockLen); s.xofInto(buf); const masks = buf.slice(0, 8); for (let i = N - TAU, pos = 8, maskPos = 0, maskBit = 0; i < N; i++) { let b = i + 1; for (; b > i;) { b = buf[pos++]; if (pos < shake256.blockLen) continue; s.xofInto(buf); pos = 0; } pre[i] = pre[b]; pre[b] = 1 - (((masks[maskPos] >> maskBit++) & 1) << 1); if (maskBit >= 8) { maskPos++; maskBit = 0; } } return pre; }; const polyPowerRound = (p) => { const res0 = newPoly(N); const res1 = newPoly(N); for (let i = 0; i < p.length; i++) { const { r0, r1 } = Power2Round(p[i]); res0[i] = r0; res1[i] = r1; } return { r0: res0, r1: res1 }; }; const polyUseHint = (u, h) => { for (let i = 0; i < N; i++) u[i] = UseHint(h[i], u[i]); return u; }; const polyMakeHint = (a, b) => { const v = newPoly(N); let cnt = 0; for (let i = 0; i < N; i++) { const h = MakeHint(a[i], b[i]); v[i] = h; cnt += h; } return { v, cnt }; }; const signRandBytes = 32; const seedCoder = splitCoder(32, 64, 32); // API & argument positions are exactly as in FIPS204. const internal = { signRandBytes, keygen: (seed = randomBytes(32)) => { // H(𝜉||IntegerToBytes(𝑘, 1)||IntegerToBytes(ℓ, 1), 128) 2: ▷ expand seed const seedDst = new Uint8Array(32 + 2); seedDst.set(seed); seedDst[32] = K; seedDst[33] = L; const [rho, rhoPrime, K_] = seedCoder.decode(shake256(seedDst, { dkLen: seedCoder.bytesLen })); const xofPrime = XOF256(rhoPrime); const s1 = []; for (let i = 0; i < L; i++) s1.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff))); const s2 = []; for (let i = L; i < L + K; i++) s2.push(RejBoundedPoly(xofPrime.get(i & 0xff, (i >> 8) & 0xff))); const s1Hat = s1.map((i) => NTT.encode(i.slice())); const t0 = []; const t1 = []; const xof = XOF128(rho); const t = newPoly(N); for (let i = 0; i < K; i++) { // t ← NTT−1(A*NTT(s1)) + s2 t.fill(0); // don't-reallocate for (let j = 0; j < L; j++) { const aij = RejNTTPoly(xof.get(j, i)); // super slow! polyAdd(t, MultiplyNTTs(aij, s1Hat[j])); } NTT.decode(t); const { r0, r1 } = polyPowerRound(polyAdd(t, s2[i])); // (t1, t0) ← Power2Round(t, d) t0.push(r0); t1.push(r1); } const publicKey = publicCoder.encode([rho, t1]); // pk ← pkEncode(ρ, t1) const tr = shake256(publicKey, { dkLen: TR_BYTES }); // tr ← H(BytesToBits(pk), 512) const secretKey = secretCoder.encode([rho, K_, tr, s1, s2, t0]); // sk ← skEncode(ρ, K,tr, s1, s2, t0) xof.clean(); xofPrime.clean(); // STATS // Kyber512: { calls: 4, xofs: 12 }, Kyber768: { calls: 9, xofs: 27 }, Kyber1024: { calls: 16, xofs: 48 } // DSA44: { calls: 24, xofs: 24 }, DSA65: { calls: 41, xofs: 41 }, DSA87: { calls: 71, xofs: 71 } cleanBytes(rho, rhoPrime, K_, s1, s2, s1Hat, t, t0, t1, tr, seedDst); return { publicKey, secretKey }; }, // NOTE: random is optional. sign: (secretKey, msg, random) => { // This part can be pre-cached per secretKey, but there is only minor performance improvement, // since we re-use a lot of variables to computation. const [rho, _K, tr, s1, s2, t0] = secretCoder.decode(secretKey); // (ρ, K,tr, s1, s2, t0) ← skDecode(sk) // Cache matrix to avoid re-compute later const A = []; // A ← ExpandA(ρ) const xof = XOF128(rho); for (let i = 0; i < K; i++) { const pv = []; for (let j = 0; j < L; j++) pv.push(RejNTTPoly(xof.get(j, i))); A.push(pv); } xof.clean(); for (let i = 0; i < L; i++) NTT.encode(s1[i]); // sˆ1 ← NTT(s1) for (let i = 0; i < K; i++) { NTT.encode(s2[i]); // sˆ2 ← NTT(s2) NTT.encode(t0[i]); // tˆ0 ← NTT(t0) } // This part is per msg const mu = shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 6: µ ← H(tr||M, 512) ▷ Compute message representative µ // Compute private random seed const rnd = random ? random : new Uint8Array(32); ensureBytes(rnd); const rhoprime = shake256 .create({ dkLen: CRH_BYTES }) .update(_K) .update(rnd) .update(mu) .digest(); // ρ′← H(K||rnd||µ, 512) ensureBytes(rhoprime, CRH_BYTES); const x256 = XOF256(rhoprime, ZCoder.bytesLen); // Rejection sampling loop main_loop: for (let kappa = 0;;) { const y = []; // y ← ExpandMask(ρ , κ) for (let i = 0; i < L; i++, kappa++) y.push(ZCoder.decode(x256.get(kappa & 0xff, kappa >> 8)())); const z = y.map((i) => NTT.encode(i.slice())); const w = []; for (let i = 0; i < K; i++) { // w ← NTT−1(A ◦ NTT(y)) const wi = newPoly(N); for (let j = 0; j < L; j++) polyAdd(wi, MultiplyNTTs(A[i][j], z[j])); NTT.decode(wi); w.push(wi); } const w1 = w.map((j) => j.map(HighBits)); // w1 ← HighBits(w) // Commitment hash: c˜ ∈{0, 1 2λ } ← H(µ||w1Encode(w1), 2λ) const cTilde = shake256 .create({ dkLen: C_TILDE_BYTES }) .update(mu) .update(W1Vec.encode(w1)) .digest(); // Verifer’s challenge const cHat = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1); cˆ ← NTT(c) // ⟨⟨cs1⟩⟩ ← NTT−1(cˆ◦ sˆ1) const cs1 = s1.map((i) => MultiplyNTTs(i, cHat)); for (let i = 0; i < L; i++) { polyAdd(NTT.decode(cs1[i]), y[i]); // z ← y + ⟨⟨cs1⟩⟩ if (polyChknorm(cs1[i], GAMMA1 - BETA)) continue main_loop; // ||z||∞ ≥ γ1 − β } // cs1 is now z (▷ Signer’s response) let cnt = 0; const h = []; for (let i = 0; i < K; i++) { const cs2 = NTT.decode(MultiplyNTTs(s2[i], cHat)); // ⟨⟨cs2⟩⟩ ← NTT−1(cˆ◦ sˆ2) const r0 = polySub(w[i], cs2).map(LowBits); // r0 ← LowBits(w − ⟨⟨cs2⟩⟩) if (polyChknorm(r0, GAMMA2 - BETA)) continue main_loop; // ||r0||∞ ≥ γ2 − β const ct0 = NTT.decode(MultiplyNTTs(t0[i], cHat)); // ⟨⟨ct0⟩⟩ ← NTT−1(cˆ◦ tˆ0) if (polyChknorm(ct0, GAMMA2)) continue main_loop; polyAdd(r0, ct0); // ▷ Signer’s hint const hint = polyMakeHint(r0, w1[i]); // h ← MakeHint(−⟨⟨ct0⟩⟩, w− ⟨⟨cs2⟩⟩ + ⟨⟨ct0⟩⟩) h.push(hint.v); cnt += hint.cnt; } if (cnt > OMEGA) continue; // the number of 1’s in h is greater than ω x256.clean(); const res = sigCoder.encode([cTilde, cs1, h]); // σ ← sigEncode(c˜, z mod±q, h) // rho, _K, tr is subarray of secretKey, cannot clean. cleanBytes(cTilde, cs1, h, cHat, w1, w, z, y, rhoprime, mu, s1, s2, t0, ...A); return res; } // @ts-ignore throw new Error('Unreachable code path reached, report this error'); }, verify: (publicKey, msg, sig) => { // ML-DSA.Verify(pk, M, σ): Verifes a signature σ for a message M. const [rho, t1] = publicCoder.decode(publicKey); // (ρ, t1) ← pkDecode(pk) const tr = shake256(publicKey, { dkLen: TR_BYTES }); // 6: tr ← H(BytesToBits(pk), 512) if (sig.length !== sigCoder.bytesLen) return false; // return false instead of exception const [cTilde, z, h] = sigCoder.decode(sig); // (c˜, z, h) ← sigDecode(σ), ▷ Signer’s commitment hash c ˜, response z and hint if (h === false) return false; // if h = ⊥ then return false for (let i = 0; i < L; i++) if (polyChknorm(z[i], GAMMA1 - BETA)) return false; const mu = shake256.create({ dkLen: CRH_BYTES }).update(tr).update(msg).digest(); // 7: µ ← H(tr||M, 512) // Compute verifer’s challenge from c˜ const c = NTT.encode(SampleInBall(cTilde)); // c ← SampleInBall(c˜1) const zNtt = z.map((i) => i.slice()); // zNtt = NTT(z) for (let i = 0; i < L; i++) NTT.encode(zNtt[i]); const wTick1 = []; const xof = XOF128(rho); for (let i = 0; i < K; i++) { const ct12d = MultiplyNTTs(NTT.encode(polyShiftl(t1[i])), c); //c * t1 * (2**d) const Az = newPoly(N); // // A * z for (let j = 0; j < L; j++) { const aij = RejNTTPoly(xof.get(j, i)); // A[i][j] inplace polyAdd(Az, MultiplyNTTs(aij, zNtt[j])); } // wApprox = A*z - c*t1 * (2**d) const wApprox = NTT.decode(polySub(Az, ct12d)); // Reconstruction of signer’s commitment wTick1.push(polyUseHint(wApprox, h[i])); // w ′ ← UseHint(h, w'approx ) } xof.clean(); // c˜′← H (µ||w1Encode(w′1), 2λ), Hash it; this should match c˜ const c2 = shake256 .create({ dkLen: C_TILDE_BYTES }) .update(mu) .update(W1Vec.encode(wTick1)) .digest(); // Additional checks in FIPS-204: // [[ ||z||∞ < γ1 − β ]] and [[c ˜ = c˜′]] and [[number of 1’s in h is ≤ ω]] for (const t of h) { const sum = t.reduce((acc, i) => acc + i, 0); if (!(sum <= OMEGA)) return false; } for (const t of z) if (polyChknorm(t, GAMMA1 - BETA)) return false; return equalBytes(cTilde, c2); }, }; const getMessage = (msg, ctx = EMPTY) => { ensureBytes(msg); ensureBytes(ctx); if (ctx.length > 255) throw new Error('context should be less than 255 bytes'); return concatBytes(new Uint8Array([0, ctx.length]), ctx, msg); }; // TODO: no hash-dsa vectors for now, so we don't implement it yet return { internal, keygen: internal.keygen, signRandBytes: internal.signRandBytes, sign: (secretKey, msg, ctx = EMPTY, random) => { const M = getMessage(msg, ctx); const res = internal.sign(secretKey, M, random); M.fill(0); return res; }, verify: (publicKey, msg, sig, ctx = EMPTY) => { return internal.verify(publicKey, getMessage(msg, ctx), sig); }, }; } const ml_dsa65 = /* @__PURE__ */ getDilithium({ ...PARAMS[3], CRH_BYTES: 64, TR_BYTES: 64, C_TILDE_BYTES: 48, XOF128, XOF256, }); export { ml_dsa65, ml_kem768 };