/** * GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV. * * Implemented in terms of GHash with conversion function for keys * GCM GHASH from * {@link https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf | NIST SP800-38d}, * SIV from * {@link https://www.rfc-editor.org/rfc/rfc8452 | RFC 8452}. * * GHASH modulo: x^128 + x^7 + x^2 + x + 1 * POLYVAL modulo: x^128 + x^127 + x^126 + x^121 + 1 * * @module */ import { abytes, aexists, aoutput, clean, copyBytes, createView, swap32IfBE, swap8IfBE, u32, wrapMacConstructor, } from "./utils.js"; const BLOCK_SIZE = 16; // TODO: rewrite // temporary padding buffer // ZEROS32 aliases these bytes, so clean(ZEROS32) also resets this shared tail-padding scratch. const ZEROS16 = /* @__PURE__ */ new Uint8Array(16); const ZEROS32 = /* @__PURE__ */ u32(ZEROS16); // GHASH reduces modulo x^128 + x^7 + x^2 + x + 1, so the low-degree terms // x^7 + x^2 + x + 1 become bits `11100001` = 0xe1 in R = 0xe1 || 0^120. const POLY = 0xe1; // v = 2*v % POLY // NOTE: because x + x = 0 (add/sub is same), mul2(x) != x+x // Montgomery ladder can multiply any field element with this doubling step; // addition stays simple xor. const mul2 = (s0, s1, s2, s3) => { const hiBit = s3 & 1; return { s3: (s2 << 31) | (s3 >>> 1), s2: (s1 << 31) | (s2 >>> 1), s1: (s0 << 31) | (s1 >>> 1), // NIST SP 800-38D §6.3 applies `V >> 1` and XORs R on carry. In this // 4x32-bit split, R = 0xe1 || 0^120 lives in the top byte of s0. s0: (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)), // reduce % poly }; }; // Per-word part of RFC 8452 `ByteReverse`; callers also reverse the 32-bit word order. const swapLE = (n) => (((n >>> 0) & 0xff) << 24) | (((n >>> 8) & 0xff) << 16) | (((n >>> 16) & 0xff) << 8) | ((n >>> 24) & 0xff) | 0; // POLYVAL first applies RFC 8452's per-word byte reversal, then re-normalizes // host-endian u32 loads to the little-endian word value `_updateBlock()` expects. const swap8IfLE = (n) => swap8IfBE(swapLE(n)); /** * `mulX_GHASH(ByteReverse(H))` from RFC 8452 Appendix A. * @param k mutated in place */ export function _toGHASHKey(k) { // The input is the original POLYVAL key H; reverse() materializes // RFC 8452's `ByteReverse(H)` before the GHASH mulX step. k.reverse(); const hiBit = k[15] & 1; // k >>= 1 let carry = 0; for (let i = 0; i < k.length; i++) { const t = k[i]; k[i] = (t >>> 1) | carry; carry = (t & 1) << 7; } k[0] ^= -hiBit & 0xe1; // if (hiBit) n ^= 0xe1000000000000000000000000000000; return k; } // Precompute-window heuristic only: larger inputs trade memory for fewer table lookups. // Any caller-provided length hint still collapses to one of the supported windows {2, 4, 8}. const estimateWindow = (bytes) => { if (bytes > 64 * 1024) return 8; if (bytes > 1024) return 4; return 2; }; /** * Incremental GHASH state for AES-GCM. * @param key - 16-byte GHASH key. * @param expectedLength - Expected message length for table sizing. * Chunking is segment-based, not hash-streaming: every `update()` call is zero-padded * to the next 16-byte boundary before it is absorbed. This matches the internal AES/GCM * use where AAD, payload, and length block are separate padded segments. * @example * Feeds one ciphertext block into an incremental GHASH state with a fresh hash key. * * ```ts * import { GHASH } from '@noble/ciphers/_polyval.js'; * import { randomBytes } from '@noble/ciphers/utils.js'; * const key = randomBytes(16); * const mac = new GHASH(key); * mac.update(new Uint8Array(16)); * mac.digest(); * ``` */ export class GHASH { blockLen = BLOCK_SIZE; outputLen = BLOCK_SIZE; s0 = 0; s1 = 0; s2 = 0; s3 = 0; finished = false; destroyed = false; t; W; windowSize; // We select bits per window adaptively based on expectedLength constructor(key, expectedLength) { abytes(key, 16, 'key'); key = copyBytes(key); const kView = createView(key); let k0 = kView.getUint32(0, false); let k1 = kView.getUint32(4, false); let k2 = kView.getUint32(8, false); let k3 = kView.getUint32(12, false); // generate table of doubled keys (half of montgomery ladder) const doubles = []; for (let i = 0; i < 128; i++) { doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) }); ({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3)); } const W = estimateWindow(expectedLength || 1024); if (![1, 2, 4, 8].includes(W)) throw new Error('ghash: invalid window size, expected 2, 4 or 8'); this.W = W; const bits = 128; // always 128 bits; const windows = bits / W; const windowSize = (this.windowSize = 2 ** W); const items = []; // Create precompute table for window of W bits for (let w = 0; w < windows; w++) { // truth table: 00, 01, 10, 11 for (let byte = 0; byte < windowSize; byte++) { // prettier-ignore let s0 = 0, s1 = 0, s2 = 0, s3 = 0; for (let j = 0; j < W; j++) { const bit = (byte >>> (W - j - 1)) & 1; if (!bit) continue; const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j]; ((s0 ^= d0), (s1 ^= d1), (s2 ^= d2), (s3 ^= d3)); } items.push({ s0, s1, s2, s3 }); } } this.t = items; } _updateBlock(s0, s1, s2, s3) { ((s0 ^= this.s0), (s1 ^= this.s1), (s2 ^= this.s2), (s3 ^= this.s3)); const { W, t, windowSize } = this; // prettier-ignore let o0 = 0, o1 = 0, o2 = 0, o3 = 0; const mask = (1 << W) - 1; // 2**W will kill performance. let w = 0; // NIST SP 800-38D §6.3 interprets blocks as little-endian polynomials, // so the lookup walk consumes each word byte-by-byte from // least-significant to most-significant bits. for (const num of [s0, s1, s2, s3]) { for (let bytePos = 0; bytePos < 4; bytePos++) { const byte = (num >>> (8 * bytePos)) & 0xff; for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) { const bit = (byte >>> (W * bitPos)) & mask; const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit]; ((o0 ^= e0), (o1 ^= e1), (o2 ^= e2), (o3 ^= e3)); w += 1; } } } this.s0 = o0; this.s1 = o1; this.s2 = o2; this.s3 = o3; } update(data) { aexists(this); abytes(data); data = copyBytes(data); const b32 = u32(data); const blocks = Math.floor(data.length / BLOCK_SIZE); const left = data.length % BLOCK_SIZE; for (let i = 0; i < blocks; i++) { this._updateBlock(swap8IfBE(b32[i * 4 + 0]), swap8IfBE(b32[i * 4 + 1]), swap8IfBE(b32[i * 4 + 2]), swap8IfBE(b32[i * 4 + 3])); } if (left) { ZEROS16.set(data.subarray(blocks * BLOCK_SIZE)); // Tail blocks go through the shared ZEROS32 scratch, so they need the same host-endian // normalization as full blocks; otherwise segmented GHASH/POLYVAL updates diverge on BE. this._updateBlock(swap8IfBE(ZEROS32[0]), swap8IfBE(ZEROS32[1]), swap8IfBE(ZEROS32[2]), swap8IfBE(ZEROS32[3])); clean(ZEROS32); // clean tmp buffer } return this; } destroy() { // `aexists(this)` guards update/digest paths, so destroy must mark the instance unusable too. this.destroyed = true; const { t } = this; // Wipe the key-derived precompute table; scalar accumulator words remain, // but the destroyed guard blocks further use. // clean precompute table for (const elm of t) { ((elm.s0 = 0), (elm.s1 = 0), (elm.s2 = 0), (elm.s3 = 0)); } } digestInto(out) { aexists(this); // `digestInto(out)` is the no-allocation fast path, so callers must pass a // 32-bit-aligned buffer before we reinterpret it with `u32(out)`. aoutput(out, this, true); this.finished = true; // NIST SP 800-38D §6.4 returns the final 128-bit block Y_m. // `digestInto()` follows the relaxed `aoutput()` contract, so only // out[0..15] may be touched. const { s0, s1, s2, s3 } = this; const o32 = u32(out); o32[0] = s0; o32[1] = s1; o32[2] = s2; o32[3] = s3; swap32IfBE(o32); } digest() { const res = new Uint8Array(BLOCK_SIZE); this.digestInto(res); // `res` is independent of internal state, so it stays valid after destroy() wipes the table. this.destroy(); return res; } } /** * Incremental POLYVAL state for AES-SIV. * @param key - 16-byte POLYVAL key. * @param expectedLength - Expected message length for table sizing. * Inherits GHASH's segment-padded `update()` behavior: each call is padded * independently to a 16-byte boundary before absorption. * @example * Feeds one block into an incremental POLYVAL state with a fresh hash key. * * ```ts * import { Polyval } from '@noble/ciphers/_polyval.js'; * import { randomBytes } from '@noble/ciphers/utils.js'; * const key = randomBytes(16); * const mac = new Polyval(key); * mac.update(new Uint8Array(16)); * mac.digest(); * ``` */ export class Polyval extends GHASH { constructor(key, expectedLength) { abytes(key); // RFC 8452 Appendix A converts the POLYVAL key with // `mulX_GHASH(ByteReverse(H))`; copy first because `_toGHASHKey(...)` // mutates in place. const ghKey = _toGHASHKey(copyBytes(key)); super(ghKey, expectedLength); clean(ghKey); } update(data) { aexists(this); abytes(data); data = copyBytes(data); const b32 = u32(data); const left = data.length % BLOCK_SIZE; const blocks = Math.floor(data.length / BLOCK_SIZE); for (let i = 0; i < blocks; i++) { // RFC 8452 Appendix A feeds `ByteReverse(X_i)` into GHASH, so POLYVAL // reverses the 32-bit word order in addition to the per-word byte swap. this._updateBlock(swap8IfLE(b32[i * 4 + 3]), swap8IfLE(b32[i * 4 + 2]), swap8IfLE(b32[i * 4 + 1]), swap8IfLE(b32[i * 4 + 0])); } if (left) { ZEROS16.set(data.subarray(blocks * BLOCK_SIZE)); this._updateBlock(swap8IfLE(ZEROS32[3]), swap8IfLE(ZEROS32[2]), swap8IfLE(ZEROS32[1]), swap8IfLE(ZEROS32[0])); clean(ZEROS32); } return this; } digestInto(out) { aexists(this); // `digestInto(out)` is the no-allocation fast path, so callers must pass a // 32-bit-aligned buffer before we reinterpret the output prefix with `u32(view)`. aoutput(out, this, true); this.finished = true; // RFC 8452 Appendix A maps POLYVAL output back through `ByteReverse(...)`. // `digestInto()` follows the relaxed `aoutput()` contract, so only out[0..15] may be touched. const view = out.subarray(0, this.outputLen); const { s0, s1, s2, s3 } = this; const o32 = u32(view); o32[0] = s0; o32[1] = s1; o32[2] = s2; o32[3] = s3; swap32IfBE(o32); view.reverse(); } } /** * GHash MAC for AES-GCM. * @param msg - Message bytes to authenticate. * @param key - 16-byte GHASH key. * @returns 16-byte authentication tag. * @example * Authenticates a short message with GHASH and a fresh hash key. * * ```ts * import { ghash } from '@noble/ciphers/_polyval.js'; * import { randomBytes } from '@noble/ciphers/utils.js'; * const key = randomBytes(16); * ghash(new Uint8Array(), key); * ``` */ export const ghash = /* @__PURE__ */ wrapMacConstructor(16, (key, expectedLength) => new GHASH(key, expectedLength), (msg) => [msg.length]); /** * POLYVAL MAC for AES-SIV. * @param msg - Message bytes to authenticate. * @param key - 16-byte POLYVAL key. * @returns 16-byte authentication tag. * @example * Authenticates a short message with POLYVAL and a fresh hash key. * * ```ts * import { polyval } from '@noble/ciphers/_polyval.js'; * import { randomBytes } from '@noble/ciphers/utils.js'; * const key = randomBytes(16); * polyval(new Uint8Array(), key); * ``` */ export const polyval = /* @__PURE__ */ wrapMacConstructor(16, (key, expectedLength) => new Polyval(key, expectedLength), (msg) => [msg.length]); //# sourceMappingURL=_polyval.js.map