/** * Cipher definitions. Common logic for all targets. * Doesn't contain WASM-specific/JS-specific code. * @module */ import type { CipherDef, CipherOpts } from './ciphers-abstract.ts'; import { ARX_SIGMA16, ARX_SIGMA32 } from './constants.ts'; import type { Modules } from './modules/index.ts'; import type * as TYPES from './targets/types.ts'; import { abytes, anumber, u32, type TArg, type TRet } from './utils.ts'; export const limit = (name: string, min: number, max: number) => (value: number) => { if (!Number.isSafeInteger(value) || min > value || value > max) { const minmax = '[' + min + '..' + max + ']'; throw new Error('' + name + ': expected value in range ' + minmax + ', got ' + value); } }; const setCounter = (counterSeg: TArg, counter: number) => { counterSeg[0] = counter >>> 0; counterSeg[1] = 0; }; export type ArxOpts = { counter?: number; }; export type ArxInit = { allowShortKeys: boolean; extendNonce?: boolean; counterLength: number; counterRight: boolean; }; const arxLimit = (len: number, args?: unknown[], pending = 0) => { const opts = args?.[1] as ArxOpts | undefined; const counter = opts?.counter === undefined ? 0 : opts.counter; const avail = pending > 0 && pending < 64 ? 64 - pending : 0; const blocks = Math.ceil(Math.max(0, len - avail) / 64); if (blocks > 0 && counter > 2 ** 32 - 1 - blocks) throw new Error('arx: counter overflow'); }; // Validations live in validate() because init() is lazy; tests expect errors at factory creation. export const initArx = ( mod: any, key: TArg, nonce: TArg, opts: TArg, cfg: TArg ) => { const { allowShortKeys, extendNonce, counterLength, counterRight } = cfg; const counter = opts?.counter === undefined ? 0 : opts.counter; let k = key; let sigma = key.length === 16 ? ARX_SIGMA16 : ARX_SIGMA32; if (key.length === 16 && allowShortKeys) { const kk = new Uint8Array(32); kk.set(key); kk.set(key, 16); k = kk; sigma = ARX_SIGMA16; } let n = nonce; if (extendNonce) { if (!mod.derive) throw new Error('arx: extendNonce requires derive'); mod.segments['state.sigma'].set(sigma); mod.segments['state.key'].set(k); mod.segments['derive.nonce'].set(nonce.subarray(0, 16)); mod.derive(); k = mod.segments['derive.out']; n = nonce.subarray(16); } const nonceNcLen = 16 - counterLength; if (nonceNcLen !== 12) { const nc = new Uint8Array(12); const off = counterRight ? 0 : 12 - n.length; nc.set(n, off); n = nc; } mod.segments['state.sigma'].set(sigma); mod.segments['state.key'].set(k); const nonceBytes = mod.segments['state.nonce'].length; mod.segments['state.nonce'].set(n.length === nonceBytes ? n : n.subarray(0, nonceBytes)); setCounter(u32(mod.segments['state.counter']), counter); }; // Factory-time validation: mkCipher init is lazy. const validateArx = ( key: TArg, nonce: TArg, opts: TArg, cfg: TArg ) => { const { allowShortKeys, extendNonce, counterLength } = cfg; abytes(key, undefined, 'key'); abytes(nonce, undefined, 'nonce'); const counter = opts?.counter === undefined ? 0 : opts.counter; anumber(counter, 'counter'); if (counter < 0 || counter >= 2 ** 32 - 1) throw new Error('arx: counter overflow'); if (key.length === 32) { // ok } else if (key.length === 16 && allowShortKeys) { // ok } else { abytes(key, 32, 'arx key'); throw new Error('invalid key size'); } let n = nonce; if (extendNonce) { if (nonce.length !== 24) throw new Error('arx: extended nonce must be 24 bytes'); n = nonce.subarray(16); } const nonceNcLen = 16 - counterLength; if (nonceNcLen !== n.length) throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); }; const arxBase = (cfg: TArg): TRet> => ({ blockLen: 64, // Match noble-ciphers raw stream semantics: caller-provided outputs must be the exact plaintext length. exactOutput: true, lengthLimitEnc: arxLimit, lengthLimitDec: arxLimit, validate: (key, nonce, opts) => { validateArx(key, nonce as Uint8Array, opts as ArxOpts | undefined, cfg); }, init: (mod, _dir, key, nonce, opts) => { const iv = nonce as Uint8Array; initArx(mod, key, iv, opts as ArxOpts | undefined, cfg); }, }); export const salsa20: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: true, counterLength: 8, counterRight: true }), nonceLength: 8, }))(); export const xsalsa20: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: true, counterLength: 8, counterRight: true, extendNonce: true, }), nonceLength: 24, }))(); export const chacha20orig: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: true, counterLength: 8, counterRight: false }), nonceLength: 8, }))(); export const chacha20: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: false, counterLength: 4, counterRight: false }), nonceLength: 12, }))(); export const xchacha20: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: false, counterLength: 8, counterRight: false, extendNonce: true, }), nonceLength: 24, }))(); export const chacha8: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: false, counterLength: 4, counterRight: false }), nonceLength: 12, }))(); export const chacha12: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxBase({ allowShortKeys: false, counterLength: 4, counterRight: false }), nonceLength: 12, }))(); const arxAeadBase = ( cfg: TArg, tagLeft = false, withAAD = true ): TRet> => ({ blockLen: 64, tagLength: 16, ...(withAAD ? { withAAD: true as const } : {}), tagLeft, tagError: 'invalid tag', validate: (key, nonce, aad) => { validateArx(key, nonce as Uint8Array, undefined, cfg); if (withAAD && aad !== undefined) abytes(aad as Uint8Array, undefined, 'AAD'); }, init: (mod, dir, key, nonce, aad) => { const iv = nonce as Uint8Array; const aadBytes = withAAD ? (aad as Uint8Array | undefined) : undefined; initArx(mod, key, iv, undefined, cfg); const [aadLo, aadHi] = splitLen(aadBytes ? aadBytes.length : 0); if (dir === 'encrypt') mod.encryptInit(aadLo, aadHi); else mod.decryptInit(aadLo, aadHi); if (aadBytes && aadBytes.length) aadBlocks(mod, aadBytes); }, getTag: (mod) => { mod.tagFinish(); return mod.segments['state.poly.tag'].subarray(0, 16) as TRet; }, }); export const chacha20poly1305: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxAeadBase({ allowShortKeys: false, counterLength: 4, counterRight: false, }), nonceLength: 12, }))(); export const xchacha20poly1305: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxAeadBase({ allowShortKeys: false, counterLength: 8, counterRight: false, extendNonce: true, }), nonceLength: 24, }))(); // XSalsa20-Poly1305 secretbox has no standard AAD. export const xsalsa20poly1305: TRet> = /* @__PURE__ */ (() => Object.freeze({ .../* @__PURE__ */ arxAeadBase( { allowShortKeys: true, counterLength: 8, counterRight: true, extendNonce: true }, true, false ), dataOffset: 32, nonceLength: 24, }))(); const AES_LEN_ENC = 'aec/(cbc-ecb): unpadded plaintext with disabled padding'; const AES_LEN_DEC = 'aes-(cbc/ecb).decrypt ciphertext should consist of blocks with size 16'; const AES_PAD_EMPTY = 'aes/pcks5: empty ciphertext not allowed'; // Match noble-ciphers invalid-padding wording for compatibility tests. const AES_PAD_BAD = 'aes: bad decrypt'; const aesKey = (key: TArg) => { abytes(key, undefined, 'aes key'); if (key.length !== 16 && key.length !== 24 && key.length !== 32) { throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length=' + key.length); } }; const aesInit = ( mod: any, key: TArg, iv: TArg, dir: 'encrypt' | 'decrypt', useDec: boolean ) => { const keySeg = mod.segments['state.key']; keySeg.set(key); if (iv !== undefined) mod.segments['state.iv'].set(iv); if (dir === 'encrypt' || !useDec) mod.encryptInit(key.length); else mod.decryptInit(key.length); }; const aesInitCtr = ( mod: any, key: TArg, nonce: TArg, dir: 'encrypt' | 'decrypt' ) => { const keySeg = mod.segments['state.key']; keySeg.set(key); if (nonce !== undefined) mod.segments['state.nonce'].set(nonce); if (dir === 'encrypt') mod.encryptInit(key.length); else mod.decryptInit(key.length); }; // NIST SP 800-38A §6.1: ECB repeats ciphertext blocks for repeated plaintext blocks under a key. export const ecb: TRet> = { blockLen: 16, padding: true, lengthErrorEnc: AES_LEN_ENC, lengthErrorDec: AES_LEN_DEC, padError: AES_PAD_BAD, emptyError: AES_PAD_EMPTY, validate: (key) => { aesKey(key); }, init: (mod, dir, key, opts) => { aesInit(mod, key, undefined, dir, true); const o = opts as CipherOpts | undefined; return { disablePadding: !!o?.disablePadding }; }, }; // NIST SP 800-38A §5.3: CBC callers must supply an unpredictable 16-byte IV; this wrapper only validates its length. export const cbc: TRet> = { blockLen: 16, nonceLength: 16, padding: true, lengthErrorEnc: AES_LEN_ENC, lengthErrorDec: AES_LEN_DEC, padError: AES_PAD_BAD, emptyError: AES_PAD_EMPTY, validate: (key, iv) => { aesKey(key); if (iv !== undefined) abytes(iv as Uint8Array, 16, 'iv'); }, init: (mod, dir, key, iv, opts) => { aesInit(mod, key, iv as Uint8Array, dir, true); const o = opts as CipherOpts | undefined; return { disablePadding: !!o?.disablePadding }; }, }; // NIST SP 800-38A §§5.3 and 6.3: CFB callers must supply an unpredictable 16-byte IV; this wrapper fixes s to the 128-bit AES block size. export const cfb: TRet> = { blockLen: 16, nonceLength: 16, noOverlap: true, validate: (key, iv) => { aesKey(key); if (iv !== undefined) abytes(iv as Uint8Array, 16, 'iv'); }, init: (mod, dir, key, iv) => { aesInit(mod, key, iv as Uint8Array, dir, false); }, }; // NIST SP 800-38A §6.4: OFB callers must supply a unique 16-byte IV for each use under a given key; this wrapper only validates its length. export const ofb: TRet> = { blockLen: 16, nonceLength: 16, validate: (key, iv) => { aesKey(key); if (iv !== undefined) abytes(iv as Uint8Array, 16, 'iv'); }, init: (mod, dir, key, iv) => { aesInit(mod, key, iv as Uint8Array, dir, false); }, }; // NIST SP 800-38A §6.5 and Appendix B: CTR callers must supply a unique 16-byte initial counter block under each key; this wrapper only validates its length. export const ctr: TRet> = { blockLen: 16, nonceLength: 16, validate: (key, nonce) => { aesKey(key); if (nonce !== undefined) abytes(nonce as Uint8Array, 16, 'nonce'); }, init: (mod, dir, key, nonce) => { aesInitCtr(mod, key, nonce as Uint8Array, dir); }, }; // AEAD backends take 64-bit length inputs as [lo, hi] u32 words via u64.fromN('u32', ...). const _0xffffffffn = /* @__PURE__ */ BigInt(0xffffffff); const _32n = /* @__PURE__ */ BigInt(32); const splitLen = (len: number) => { const v = BigInt(len); return [Number(v & _0xffffffffn), Number((v >> _32n) & _0xffffffffn)]; }; // Feed full blocks through the module buffer, zero-filling the unused tail so callbacks can finalize partial last blocks safely. const streamBlocks = ( mod: any, data: TArg, blockLen: number, run: (blocks: number, isLast: number, left: number) => void ) => { const buffer = mod.segments.buffer as Uint8Array; const maxBlocks = Math.floor(buffer.length / blockLen); let pos = 0; while (pos < data.length) { const remaining = data.length - pos; const blocks = Math.min(maxBlocks, Math.ceil(remaining / blockLen)); const take = Math.min(remaining, blocks * blockLen); const left = blocks * blockLen - take; if (take) buffer.set(data.subarray(pos, pos + take), 0); if (left) buffer.fill(0, take, take + left); run(blocks, remaining <= maxBlocks * blockLen ? 1 : 0, left); buffer.fill(0, 0, blocks * blockLen); pos += take; } }; const aadBlocks = (mod: any, data: TArg) => { streamBlocks(mod, data, 16, (blocks, isLast, left) => { mod.aadBlocks(blocks, isLast, left); }); }; // NIST SP 800-38D §§5.2.1.1 and 8: GCM IVs must be unique per key; this wrapper supports variable-length byte IVs but only enforces a local minimum length. export const gcm: TRet> = { blockLen: 16, nonceLength: 12, tagLength: 16, withAAD: true, varSizeNonce: true, noOutput: true, validate: (key, nonce, aad) => { const iv = nonce as Uint8Array; abytes(iv, undefined, 'nonce'); if (iv.length < 8) throw new Error('aes/gcm: invalid nonce length'); aesKey(key); if (aad !== undefined) abytes(aad as Uint8Array, undefined, 'AAD'); }, init: (mod, dir, key, nonce, aad) => { const iv = nonce as Uint8Array; const aadBytes = aad as Uint8Array | undefined; mod.segments['state.key'].set(key); if (iv.length === 12) { mod.segments['state.nonce'].set(iv); } const [aadLo, aadHi] = splitLen(aadBytes ? aadBytes.length : 0); const nonceBits = iv.length === 12 ? [0, 0] : splitLen(iv.length * 8); if (dir === 'encrypt') mod.encryptInit(key.length, iv.length, nonceBits[0], nonceBits[1], aadLo, aadHi); else mod.decryptInit(key.length, iv.length, nonceBits[0], nonceBits[1], aadLo, aadHi); mod.aadInit(); if (iv.length !== 12) { aadBlocks(mod, iv); mod.nonceFinish(nonceBits[0], nonceBits[1]); } if (aadBytes && aadBytes.length) { aadBlocks(mod, aadBytes); } }, getTag: (mod) => { mod.tagFinish(); return mod.segments['state.tag'].subarray(0, 16) as TRet; }, }; export const gcmsiv: TRet> = /* @__PURE__ */ (() => { const GCMSIV_AAD = /* @__PURE__ */ limit('AAD', 0, 2 ** 36); const GCMSIV_PLAIN = /* @__PURE__ */ limit('plaintext', 0, 2 ** 36); const GCMSIV_CIPHER = /* @__PURE__ */ limit('ciphertext', 16, 2 ** 36 + 16); const GCMSIV_NONCE = /* @__PURE__ */ limit('nonce', 12, 12); return { blockLen: 16, nonceLength: 12, tagLength: 16, withAAD: true, varSizeNonce: true, noOutput: true, noStream: true, // RFC 8452 §§4-5, 9: AES-GCM-SIV decrypt must not release plaintext before tag confirmation, so round-0 output stays internal. multiPass: 2, multiPassResult: { encrypt: false, decrypt: true }, multiPassOut: { decrypt: 0 }, lengthLimitEnc: (len) => GCMSIV_PLAIN(len), lengthLimitDec: (len) => GCMSIV_CIPHER(len), validate: (key, nonce, aad) => { const iv = nonce as Uint8Array; const aadBytes = aad as Uint8Array | undefined; abytes(iv, undefined, 'nonce'); // RFC 8452 standardizes only 16-byte and 32-byte keys; 24-byte AES-192 is a local extension. aesKey(key); GCMSIV_NONCE(iv.length); if (aadBytes) { abytes(aadBytes, undefined, 'AAD'); GCMSIV_AAD(aadBytes.length); } }, init: (mod, dir, key, nonce, aad) => { const iv = nonce as Uint8Array; const aadBytes = aad as Uint8Array | undefined; mod.segments['state.key'].set(key); mod.segments['state.nonce'].set(iv); const [aadLo, aadHi] = splitLen(aadBytes ? aadBytes.length : 0); if (dir === 'encrypt') mod.encryptInit(key.length, aadLo, aadHi); else mod.decryptInit(key.length, aadLo, aadHi); if (aadBytes && aadBytes.length) aadBlocks(mod, aadBytes); }, getTag: (mod) => mod.segments['state.tag'].subarray(0, 16) as TRet, }; })(); export const aessiv: TRet> = { blockLen: 16, tagLength: 16, withAAD: true, tagLeft: true, noStream: true, multiPass: 2, multiPassResult: { encrypt: false, decrypt: true }, multiPassOut: { decrypt: 0 }, // RFC 5297 §2.7 verifies after CTR on decrypt, so mkCipher clears any round-0 candidate // plaintext it wrote into caller output before surfacing invalid-tag failure. // RFC 5297 §§3, 7: extra args are S2V AD components; nonce-based callers pass the nonce last, and plaintext is the 127th component so AD is capped at 126 entries. validate: (_key, ...aadList) => { const aad = aadList as Uint8Array[]; if (aad.length > 126) throw new Error('"AAD" number of elements must be less than or equal to 126'); const key = _key as Uint8Array; abytes(key, undefined, 'aes key'); if (key.length !== 32 && key.length !== 48 && key.length !== 64) { throw new Error('"aes key" expected Uint8Array of length 32/48/64, got length=' + key.length); } for (const a of aad) abytes(a); }, init: (mod, dir, key, ...aadList) => { const aad = aadList as Uint8Array[]; const half = key.length / 2; mod.segments['state.key1'].set(key.subarray(0, half)); mod.segments['state.key'].set(key.subarray(half)); if (dir === 'encrypt') mod.encryptInit(half); else mod.decryptInit(half); for (const a of aad) { if (a.length) aadBlocks(mod, a); else mod.aadBlocks(0, 1, 0); } }, getTag: (mod) => mod.segments['state.tag'].subarray(0, 16) as TRet, }; const AESKW_PLAIN = 'invalid plaintext length'; const AESKW_CIPHER = 'invalid ciphertext length'; const AESKW_SHORT = '8-byte keys not allowed in AESKW, use AESKWP instead'; const AESKW_PLAIN_4G = 'plaintext should be less than 4gb'; const AESKW_CIPHER_4G = 'ciphertext should be less than 4gb'; const aeskwInit = (mod: any, dir: 'encrypt' | 'decrypt', key: TArg) => { mod.segments['state.key'].set(key); if (dir === 'encrypt') mod.encryptInit(key.length); else mod.decryptInit(key.length); }; // RFC 3394 §2 leaves AES-KW input length unbounded; these wrappers still cap inputs below 2^32 bytes because callers and outputs are Uint8Array-sized. const kwPlainLimit = (len: number) => { if (len >= 2 ** 32) throw new Error(AESKW_PLAIN_4G); if (len === 8) throw new Error(AESKW_SHORT); if (!len || len % 8 !== 0) throw new Error(AESKW_PLAIN); }; // RFC 3394 unwrap consumes (n+1) 64-bit blocks with n>=2, so AES-KW ciphertext is at least 24 bytes; this wrapper also keeps the local Uint8Array-sized cap below 2^32+8 bytes. const kwCipherLimit = (len: number) => { if (len - 8 >= 2 ** 32) throw new Error(AESKW_CIPHER_4G); if (len % 8 !== 0 || len < 24) throw new Error(AESKW_CIPHER); }; // RFC 5649 allows 1..2^32 octets before padding; this Uint8Array API can only realize lengths below 2^32. const kwpPlainLimit = (len: number) => { if (len >= 2 ** 32) throw new Error(AESKW_PLAIN_4G); if (!len) throw new Error(AESKW_PLAIN); }; // RFC 5649 unwrap works on two or more 64-bit blocks; this helper preflights only the min/max bound, and the shared paddingLeft branch rejects non-8-byte ciphertext lengths before module work. const kwpCipherLimit = (len: number) => { if (len - 8 >= 2 ** 32) throw new Error(AESKW_CIPHER_4G); if (len < 16) throw new Error(AESKW_CIPHER); }; export const aeskw: TRet> = { blockLen: 8, // RFC 3394 wraps a leading 64-bit A/C0 register plus six outer passes over A || R[1..n]. paddingLeft: 8, noStream: true, noOutput: true, padError: 'integrity check failed', multiPass: 6, multiPassResult: true, validate: (key) => { aesKey(key); }, lengthLimitEnc: kwPlainLimit, lengthLimitDec: kwCipherLimit, init: (mod, dir, key) => { aeskwInit(mod, dir, key); }, }; export const aeskwp: TRet> = { blockLen: 8, // RFC 5649 wraps a leading 64-bit AIV register, pads only to the next 64-bit boundary, and uses the RFC 3394 six-pass path once more than one plaintext block is present. paddingLeft: 8, padding: true, padFull: false, noStream: true, noOutput: true, padError: 'integrity check failed', multiPass: 6, multiPassResult: true, validate: (key) => { aesKey(key); }, lengthLimitEnc: kwpPlainLimit, lengthLimitDec: kwpCipherLimit, init: (mod, dir, key) => { aeskwInit(mod, dir, key); }, }; // NOTE: safe for tree-shaking since used only in building process // prettier-ignore const defs = { ctr, cbc, ofb, cfb, ecb, gcm, gcmsiv, aessiv, aeskw, aeskwp, // AES salsa20, xsalsa20, chacha8, chacha12, chacha20, chacha20orig, xchacha20, // ARX chacha20poly1305, xchacha20poly1305, xsalsa20poly1305 // ARX AED } as const satisfies Record>; // Map each name to module const MOD_OF = { salsa20: 'salsa20', xsalsa20: 'salsa20', chacha8: 'chacha8', chacha12: 'chacha12', chacha20: 'chacha20', chacha20orig: 'chacha20', xchacha20: 'chacha20', chacha20poly1305: 'chacha_poly1305', xchacha20poly1305: 'chacha_poly1305', xsalsa20poly1305: 'salsa_poly1305', ctr: 'aes_ctr', cbc: 'aes_cbc', cfb: 'aes_cfb', ecb: 'aes_ecb', gcm: 'aes_gcm', gcmsiv: 'aes_gcmsiv', aessiv: 'aes_siv', aeskw: 'aes_kw', aeskwp: 'aes_kwp', ofb: 'aes_ofb', } as const satisfies { [K in keyof typeof defs & string]: Modules }; // Verifies that definition actually can be used with module function buildDefinitions< D extends Record>, M extends { [K in keyof D]: Modules }, >(defs: D, modOf: M) { type Pair = { mod: M[K]; def: D[K] }; const out = {} as { [K in keyof D]: Pair }; for (const k in defs) out[k] = { mod: modOf[k], def: defs[k] } as Pair; return out; } export const Definitions = /* @__PURE__ */ buildDefinitions(defs, MOD_OF);