/** * Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers. RFC8439 requires multi-step cipher stream, where authKey starts with counter: 0, actual msg with counter: 1. For this, we need a way to re-use nonce / counter: const counter = new Uint8Array(4); chacha(..., counter, ...); // counter is now 1 chacha(..., counter, ...); // counter is now 2 This is complicated: - 32-bit counters are enough, no need for 64-bit: max ArrayBuffer size in JS is 4GB - Original papers don't allow mutating counters - Counter overflow is undefined [^1] - Idea A: allow providing (nonce | counter) instead of just nonce, re-use it - Caveat: Cannot be re-used through all cases: - * chacha has (counter | nonce) - * xchacha has (nonce16 | counter | nonce16) - Idea B: separate nonce / counter and provide separate API for counter re-use - Caveat: there are different counter sizes depending on an algorithm. - salsa & chacha also differ in structures of key & sigma: salsa20: s[0] | k(4) | s[1] | nonce(2) | cnt(2) | s[2] | k(4) | s[3] chacha: s(4) | k(8) | cnt(1) | nonce(3) chacha20orig: s(4) | k(8) | cnt(2) | nonce(2) - Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)` - Caveat: we can't re-use counter array xchacha uses the subkey and remaining 8 byte nonce with ChaCha20 as normal (prefixed by 4 NUL bytes, since RFC8439 specifies a 12-byte nonce). Counter overflow is undefined; see {@link https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/ | the CFRG thread}. Current noble policy is strict non-wrap for the shared 32-bit counter path: exported ARX ciphers reject initial `0xffffffff` and stop before any implicit wrap back to zero. See {@link https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2 | the XChaCha appendix} for the extended-nonce construction. * @module */ import { abool, abytes, anumber, checkOpts, clean, copyBytes, getOutput, isAligned32, isLE, randomBytes, swap32IfBE, u32, } from "./utils.js"; // Replaces `TextEncoder` for ASCII literals, which is enough for sigma constants. // Non-ASCII input would not match UTF-8 `TextEncoder` output. const encodeStr = (str) => Uint8Array.from(str.split(''), (c) => c.charCodeAt(0)); // Raw `createCipher(...)` exports consume these native-endian `u32(...)` views directly. // Public `wrapCipher(...)` APIs reject non-little-endian platforms before reaching this path. // RFC 8439 §2.3 / RFC 7539 §2.3 only define the 256-bit-key constants; this 16-byte sigma is // kept for legacy allowShortKeys Salsa/ChaCha variants. const sigma16_32 = /* @__PURE__ */ (() => swap32IfBE(u32(encodeStr('expand 16-byte k'))))(); // RFC 8439 §2.3 / RFC 7539 §2.3 define words 0-3 as // `0x61707865 0x3320646e 0x79622d32 0x6b206574`, i.e. `expand 32-byte k`. const sigma32_32 = /* @__PURE__ */ (() => swap32IfBE(u32(encodeStr('expand 32-byte k'))))(); /** * Rotates a 32-bit word left. * @param a - Input word. * @param b - Rotation count in bits. * @returns Rotated 32-bit word. * @example * Moves the top byte of `0x12345678` into the low byte position. * ```ts * rotl(0x12345678, 8); * ``` */ export function rotl(a, b) { return (a << b) | (a >>> (32 - b)); } // Salsa and Chacha block length is always 512-bit const BLOCK_LEN = 64; // RFC 8439 §2.2 / RFC 7539 §2.2: the ChaCha state has 16 32-bit words. const BLOCK_LEN32 = 16; // Counter policy for the shared public `counter` argument: // - RFC/IETF ChaCha20 uses a 32-bit counter. // - OpenSSL/Node `chacha20` instead treat the full 16-byte IV as a 128-bit // counter state and carry into the next word. // - Raw `chacha20orig`, `salsa20`, `xsalsa20`, and `xchacha20` use 64-bit counters in libsodium // and libtomcrypt, while some libs (for example libtomcrypt's RFC/IETF path) reject the max // boundary instead of carrying. // - AEAD wrappers diverge too: libsodium `xchacha20poly1305` uses the IETF payload counter from // block 1, while `secretstream_xchacha20poly1305` is a different protocol with rekey/reset. // Noble intentionally throws instead of silently picking one wrap model for users. In the default // path, even a 32-bit boundary would take 2^32 blocks * 64 bytes = 256 GiB, which is practically // unreachable for normal JS callers; advanced users who pass `counter` explicitly can implement // whatever wider carry / wrap policy they need on top. const MAX_COUNTER = /* @__PURE__ */ (() => 2 ** 32 - 1)(); const U32_EMPTY = /* @__PURE__ */ Uint32Array.of(); function runCipher(core, sigma, key, nonce, data, output, counter, rounds) { const len = data.length; const block = new Uint8Array(BLOCK_LEN); const b32 = u32(block); // Make sure that buffers aligned to 4 bytes const isAligned = isLE && isAligned32(data) && isAligned32(output); const d32 = isAligned ? u32(data) : U32_EMPTY; const o32 = isAligned ? u32(output) : U32_EMPTY; // RFC 8439 §2.4.1 / RFC 7539 §2.4.1 allow XORing one keystream block at a time and // truncating the final partial block instead of materializing the whole keystream. if (!isLE) { for (let pos = 0; pos < len; counter++) { core(sigma, key, nonce, b32, counter, rounds); // RFC 8439 §2.4 / RFC 7539 §2.4 serialize keystream words in little-endian order. swap32IfBE(b32); if (counter >= MAX_COUNTER) throw new Error('arx: counter overflow'); const take = Math.min(BLOCK_LEN, len - pos); for (let j = 0, posj; j < take; j++) { posj = pos + j; output[posj] = data[posj] ^ block[j]; } pos += take; } return; } for (let pos = 0; pos < len; counter++) { core(sigma, key, nonce, b32, counter, rounds); // See MAX_COUNTER policy note above: never silently wrap the shared public counter. if (counter >= MAX_COUNTER) throw new Error('arx: counter overflow'); const take = Math.min(BLOCK_LEN, len - pos); // aligned to 4 bytes if (isAligned && take === BLOCK_LEN) { const pos32 = pos / 4; if (pos % 4 !== 0) throw new Error('arx: invalid block position'); for (let j = 0, posj; j < BLOCK_LEN32; j++) { posj = pos32 + j; o32[posj] = d32[posj] ^ b32[j]; } pos += BLOCK_LEN; continue; } for (let j = 0, posj; j < take; j++) { posj = pos + j; output[posj] = data[posj] ^ block[j]; } pos += take; } } /** * Creates an ARX stream cipher from a 32-bit core permutation. * Used internally to build the exported Salsa and ChaCha stream ciphers. * @param core - Core function that fills one keystream block. * @param opts - Cipher layout and nonce-extension options. See {@link CipherOpts}. * @returns Stream cipher function over byte arrays. * @throws If the core callback, key size, counter, or output sizing is invalid. {@link Error} */ export function createCipher(core, opts) { const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = checkOpts({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts); if (typeof core !== 'function') throw new Error('core must be a function'); anumber(counterLength); anumber(rounds); abool(counterRight); abool(allowShortKeys); return (key, nonce, data, output, counter = 0) => { abytes(key, undefined, 'key'); abytes(nonce, undefined, 'nonce'); abytes(data, undefined, 'data'); const len = data.length; // Raw XorStream APIs return ciphertext/plaintext bytes directly, so caller-provided outputs // must match the logical result length exactly instead of returning an oversized workspace. output = getOutput(len, output, false); anumber(counter); // See MAX_COUNTER policy note above: reject advanced explicit-counter requests before any wrap. if (counter < 0 || counter >= MAX_COUNTER) throw new Error('arx: counter overflow'); const toClean = []; // Key & sigma // key=16 -> sigma16, k=key|key // key=32 -> sigma32, k=key let l = key.length; let k; let sigma; if (l === 32) { // Copy caller keys too: big-endian normalization, extended-nonce subkey derivation, and // final clean(...) all mutate or wipe the temporary buffer in place. toClean.push((k = copyBytes(key))); sigma = sigma32_32; } else if (l === 16 && allowShortKeys) { k = new Uint8Array(32); k.set(key); k.set(key, 16); sigma = sigma16_32; toClean.push(k); } else { abytes(key, 32, 'arx key'); throw new Error('invalid key size'); // throw new Error(`"arx key" expected Uint8Array of length 32, got length=${l}`); } // Nonce // salsa20: 8 (8-byte counter) // chacha20orig: 8 (8-byte counter) // chacha20: 12 (4-byte counter) // xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce) // xchacha20: 24 (16 -> hchacha, 8 -> old nonce) // Copy before taking u32(...) views on misaligned inputs, and on big-endian so later // swap32IfBE(...) never mutates caller nonce bytes in place. if (!isLE || !isAligned32(nonce)) toClean.push((nonce = copyBytes(nonce))); let k32 = u32(k); // hsalsa & hchacha: handle extended nonce if (extendNonceFn) { if (nonce.length !== 24) throw new Error(`arx: extended nonce must be 24 bytes`); const n16 = nonce.subarray(0, 16); if (isLE) extendNonceFn(sigma, k32, u32(n16), k32); else { const sigmaRaw = swap32IfBE(Uint32Array.from(sigma)); extendNonceFn(sigmaRaw, k32, u32(n16), k32); clean(sigmaRaw); swap32IfBE(k32); } nonce = nonce.subarray(16); } else if (!isLE) swap32IfBE(k32); // Handle nonce counter const nonceNcLen = 16 - counterLength; if (nonceNcLen !== nonce.length) throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); // Normalize 64-bit-nonce layouts to the 12-byte core input: ChaCha/XChaCha prefix 4 zero // counter bytes, while Salsa/XSalsa append them after the nonce words. if (nonceNcLen !== 12) { const nc = new Uint8Array(12); nc.set(nonce, counterRight ? 0 : 12 - nonce.length); nonce = nc; toClean.push(nonce); } const n32 = swap32IfBE(u32(nonce)); // Ensure temporary key/nonce copies are wiped even if the remaining // runtime guard in runCipher(...) throws on counter overflow. try { runCipher(core, sigma, k32, n32, data, output, counter, rounds); return output; } finally { clean(...toClean); } }; } /** Internal class which wraps chacha20 or chacha8 to create CSPRNG. */ export class _XorStreamPRG { blockLen; keyLen; nonceLen; state; buf; key; nonce; pos; ctr; cipher; constructor(cipher, blockLen, keyLen, nonceLen, seed) { this.cipher = cipher; this.blockLen = blockLen; this.keyLen = keyLen; this.nonceLen = nonceLen; this.state = new Uint8Array(this.keyLen + this.nonceLen); this.reseed(seed); this.ctr = 0; this.pos = this.blockLen; this.buf = new Uint8Array(this.blockLen); // Keep a single key||nonce backing buffer so reseed/addEntropy/clean update the live cipher // inputs in place through these subarray views. this.key = this.state.subarray(0, this.keyLen); this.nonce = this.state.subarray(this.keyLen); } reseed(seed) { abytes(seed); if (!seed || seed.length === 0) throw new Error('entropy required'); // Mix variable-length entropy cyclically across the whole key||nonce state, then restart the // keystream so buffered leftovers from the previous state are never reused. for (let i = 0; i < seed.length; i++) this.state[i % this.state.length] ^= seed[i]; this.ctr = 0; this.pos = this.blockLen; } addEntropy(seed) { // Reject empty entropy before re-keying, otherwise a throwing call would still advance state. abytes(seed); if (seed.length === 0) throw new Error('entropy required'); // Re-key from the current stream first, then mix external entropy into the fresh key||nonce // state through reseed() so stale buffered bytes are discarded. this.state.set(this.randomBytes(this.state.length)); this.reseed(seed); } randomBytes(len) { anumber(len); if (len === 0) return new Uint8Array(0); const avail = this.pos < this.blockLen ? this.blockLen - this.pos : 0; const blocks = Math.ceil(Math.max(0, len - avail) / this.blockLen); // Preflight overflow so failed reads don't partially consume keystream // and leave the PRG repeating blocks. if (blocks > 0 && this.ctr > MAX_COUNTER - blocks) throw new Error('arx: counter overflow'); const out = new Uint8Array(len); let outPos = 0; // `out` starts zero-filled, and `buf.fill(0)` below does the same for leftovers: XOR-stream // ciphers then emit raw keystream bytes directly into those buffers. // Serve buffered leftovers first so split reads stay identical to one larger read. if (this.pos < this.blockLen) { const take = Math.min(len, this.blockLen - this.pos); out.set(this.buf.subarray(this.pos, this.pos + take), 0); this.pos += take; outPos += take; if (outPos === len) return out; // fast path } // Full blocks directly to out const full = Math.floor((len - outPos) / this.blockLen); if (full > 0) { const blockBytes = full * this.blockLen; const b = out.subarray(outPos, outPos + blockBytes); this.cipher(this.key, this.nonce, b, b, this.ctr); this.ctr += full; outPos += blockBytes; } // Save leftovers const left = len - outPos; if (left > 0) { this.buf.fill(0); // NOTE: cipher will handle overflow this.cipher(this.key, this.nonce, this.buf, this.buf, this.ctr++); out.set(this.buf.subarray(0, left), outPos); this.pos = left; } return out; } // Clone seeds the new instance from this stream, so the source PRG advances too. clone() { return new _XorStreamPRG(this.cipher, this.blockLen, this.keyLen, this.nonceLen, this.randomBytes(this.state.length)); } // Zeroes the current state and leftover buffer, but does not make the instance unusable: // Later reads first drain zeros from the cleared buffer and then continue // from zero key||nonce state. clean() { this.pos = 0; this.ctr = 0; this.buf.fill(0); this.state.fill(0); } } /** * Creates a PRG constructor from a stream cipher. * @param cipher - Stream cipher used to fill output blocks. * @param blockLen - Keystream block length in bytes. * @param keyLen - Internal key length in bytes. * @param nonceLen - Internal nonce length in bytes. * @returns PRG factory for seeded concrete `_XorStreamPRG` instances. * @example * Builds a PRG from XChaCha20 and reads bytes from a randomly seeded instance. * ```ts * import { xchacha20 } from '@noble/ciphers/chacha.js'; * import { createPRG } from '@noble/ciphers/_arx.js'; * import { randomBytes } from '@noble/ciphers/utils.js'; * const seed = randomBytes(32); * const init = createPRG(xchacha20, 64, 32, 24); * const prg = init(seed); * prg.randomBytes(8); * ``` */ export const createPRG = (cipher, blockLen, keyLen, nonceLen) => { return ((seed = randomBytes(32)) => new _XorStreamPRG(cipher, blockLen, keyLen, nonceLen, seed)); }; //# sourceMappingURL=_arx.js.map