"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.rotl = rotl; exports.createCipher = createCipher; /** * 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) | ctr(2) | s[2] | k(4) | s[3] chacha: s(4) | k(8) | ctr(1) | nonce(3) chacha20orig: s(4) | k(8) | ctr(2) | nonce(2) - Idea C: helper method such as `setSalsaState(key, nonce, sigma, data)` - Caveat: we can't re-use counter array xchacha [^2] uses the subkey and remaining 8 byte nonce with ChaCha20 as normal (prefixed by 4 NUL bytes, since [RFC8439] specifies a 12-byte nonce). [^1]: https://mailarchive.ietf.org/arch/msg/cfrg/gsOnTJzcbgG6OqD8Sc0GO5aR_tU/ [^2]: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha#appendix-A.2 * @module */ const _assert_js_1 = require("./_assert.js"); const utils_js_1 = require("./utils.js"); // We can't make top-level var depend on utils.utf8ToBytes // because it's not present in all envs. Creating a similar fn here const _utf8ToBytes = (str) => Uint8Array.from(str.split('').map((c) => c.charCodeAt(0))); const sigma16 = _utf8ToBytes('expand 16-byte k'); const sigma32 = _utf8ToBytes('expand 32-byte k'); const sigma16_32 = (0, utils_js_1.u32)(sigma16); const sigma32_32 = (0, utils_js_1.u32)(sigma32); function rotl(a, b) { return (a << b) | (a >>> (32 - b)); } // Is byte array aligned to 4 byte offset (u32)? function isAligned32(b) { return b.byteOffset % 4 === 0; } // Salsa and Chacha block length is always 512-bit const BLOCK_LEN = 64; const BLOCK_LEN32 = 16; // new Uint32Array([2**32]) // => Uint32Array(1) [ 0 ] // new Uint32Array([2**32-1]) // => Uint32Array(1) [ 4294967295 ] const MAX_COUNTER = 2 ** 32 - 1; const U32_EMPTY = new Uint32Array(); function runCipher(core, sigma, key, nonce, data, output, counter, rounds) { const len = data.length; const block = new Uint8Array(BLOCK_LEN); const b32 = (0, utils_js_1.u32)(block); // Make sure that buffers aligned to 4 bytes const isAligned = isAligned32(data) && isAligned32(output); const d32 = isAligned ? (0, utils_js_1.u32)(data) : U32_EMPTY; const o32 = isAligned ? (0, utils_js_1.u32)(output) : U32_EMPTY; for (let pos = 0; pos < len; counter++) { core(sigma, key, nonce, b32, counter, rounds); 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 ARX-like (ChaCha, Salsa) cipher stream from core function. */ function createCipher(core, opts) { const { allowShortKeys, extendNonceFn, counterLength, counterRight, rounds } = (0, utils_js_1.checkOpts)({ allowShortKeys: false, counterLength: 8, counterRight: false, rounds: 20 }, opts); if (typeof core !== 'function') throw new Error('core must be a function'); (0, _assert_js_1.anumber)(counterLength); (0, _assert_js_1.anumber)(rounds); (0, _assert_js_1.abool)(counterRight); (0, _assert_js_1.abool)(allowShortKeys); return (key, nonce, data, output, counter = 0) => { (0, _assert_js_1.abytes)(key); (0, _assert_js_1.abytes)(nonce); (0, _assert_js_1.abytes)(data); const len = data.length; if (output === undefined) output = new Uint8Array(len); (0, _assert_js_1.abytes)(output); (0, _assert_js_1.anumber)(counter); if (counter < 0 || counter >= MAX_COUNTER) throw new Error('arx: counter overflow'); if (output.length < len) throw new Error(`arx: output (${output.length}) is shorter than data (${len})`); 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) { toClean.push((k = (0, utils_js_1.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 { throw new Error(`arx: invalid 32-byte key, 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) // Align nonce to 4 bytes if (!isAligned32(nonce)) toClean.push((nonce = (0, utils_js_1.copyBytes)(nonce))); const k32 = (0, utils_js_1.u32)(k); // hsalsa & hchacha: handle extended nonce if (extendNonceFn) { if (nonce.length !== 24) throw new Error(`arx: extended nonce must be 24 bytes`); extendNonceFn(sigma, k32, (0, utils_js_1.u32)(nonce.subarray(0, 16)), k32); nonce = nonce.subarray(16); } // Handle nonce counter const nonceNcLen = 16 - counterLength; if (nonceNcLen !== nonce.length) throw new Error(`arx: nonce must be ${nonceNcLen} or 16 bytes`); // Pad counter when nonce is 64 bit if (nonceNcLen !== 12) { const nc = new Uint8Array(12); nc.set(nonce, counterRight ? 0 : 12 - nonce.length); nonce = nc; toClean.push(nonce); } const n32 = (0, utils_js_1.u32)(nonce); runCipher(core, sigma, k32, n32, data, output, counter, rounds); (0, utils_js_1.clean)(...toClean); return output; }; } //# sourceMappingURL=_arx.js.map