UNPKG

@noble/ciphers

Version:

Audited & minimal JS implementation of Salsa20, ChaCha and AES

343 lines 13.5 kB
/** * Utilities for hex, bytes, CSPRNG. * @module */ /*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */ /** Checks if something is Uint8Array. Be careful: nodejs Buffer will return true. */ export function isBytes(a) { return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array'); } /** Asserts something is boolean. */ export function abool(b) { if (typeof b !== 'boolean') throw new Error(`boolean expected, not ${b}`); } /** Asserts something is positive integer. */ export function anumber(n) { if (!Number.isSafeInteger(n) || n < 0) throw new Error('positive integer expected, got ' + n); } /** Asserts something is Uint8Array. */ export function abytes(value, length, title = '') { const bytes = isBytes(value); const len = value?.length; const needsLen = length !== undefined; if (!bytes || (needsLen && len !== length)) { const prefix = title && `"${title}" `; const ofLen = needsLen ? ` of length ${length}` : ''; const got = bytes ? `length=${len}` : `type=${typeof value}`; throw new Error(prefix + 'expected Uint8Array' + ofLen + ', got ' + got); } return value; } /** Asserts a hash instance has not been destroyed / finished */ export function aexists(instance, checkFinished = true) { if (instance.destroyed) throw new Error('Hash instance has been destroyed'); if (checkFinished && instance.finished) throw new Error('Hash#digest() has already been called'); } /** Asserts output is properly-sized byte array */ export function aoutput(out, instance) { abytes(out, undefined, 'output'); const min = instance.outputLen; if (out.length < min) { throw new Error('digestInto() expects output buffer of length at least ' + min); } } /** Cast u8 / u16 / u32 to u8. */ export function u8(arr) { return new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength); } /** Cast u8 / u16 / u32 to u32. */ export function u32(arr) { return new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); } /** Zeroize a byte array. Warning: JS provides no guarantees. */ export function clean(...arrays) { for (let i = 0; i < arrays.length; i++) { arrays[i].fill(0); } } /** Create DataView of an array for easy byte-level manipulation. */ export function createView(arr) { return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); } /** Is current platform little-endian? Most are. Big-Endian platform: IBM */ export const isLE = /* @__PURE__ */ (() => new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x44)(); // Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex const hasHexBuiltin = /* @__PURE__ */ (() => // @ts-ignore typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')(); // Array where index 0xf0 (240) is mapped to string 'f0' const hexes = /* @__PURE__ */ Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0')); /** * Convert byte array to hex string. Uses built-in function, when available. * @example bytesToHex(Uint8Array.from([0xca, 0xfe, 0x01, 0x23])) // 'cafe0123' */ export function bytesToHex(bytes) { abytes(bytes); // @ts-ignore if (hasHexBuiltin) return bytes.toHex(); // pre-caching improves the speed 6x let hex = ''; for (let i = 0; i < bytes.length; i++) { hex += hexes[bytes[i]]; } return hex; } // We use optimized technique to convert hex string to byte array const asciis = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; function asciiToBase16(ch) { if (ch >= asciis._0 && ch <= asciis._9) return ch - asciis._0; // '2' => 50-48 if (ch >= asciis.A && ch <= asciis.F) return ch - (asciis.A - 10); // 'B' => 66-(65-10) if (ch >= asciis.a && ch <= asciis.f) return ch - (asciis.a - 10); // 'b' => 98-(97-10) return; } /** * Convert hex string to byte array. Uses built-in function, when available. * @example hexToBytes('cafe0123') // Uint8Array.from([0xca, 0xfe, 0x01, 0x23]) */ export function hexToBytes(hex) { if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex); // @ts-ignore if (hasHexBuiltin) return Uint8Array.fromHex(hex); const hl = hex.length; const al = hl / 2; if (hl % 2) throw new Error('hex string expected, got unpadded hex of length ' + hl); const array = new Uint8Array(al); for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) { const n1 = asciiToBase16(hex.charCodeAt(hi)); const n2 = asciiToBase16(hex.charCodeAt(hi + 1)); if (n1 === undefined || n2 === undefined) { const char = hex[hi] + hex[hi + 1]; throw new Error('hex string expected, got non-hex character "' + char + '" at index ' + hi); } array[ai] = n1 * 16 + n2; // multiply first octet, e.g. 'a3' => 10*16+3 => 160 + 3 => 163 } return array; } // Used in micro export function hexToNumber(hex) { if (typeof hex !== 'string') throw new Error('hex string expected, got ' + typeof hex); return BigInt(hex === '' ? '0' : '0x' + hex); // Big Endian } // Used in ff1 // BE: Big Endian, LE: Little Endian export function bytesToNumberBE(bytes) { return hexToNumber(bytesToHex(bytes)); } // Used in micro, ff1 export function numberToBytesBE(n, len) { return hexToBytes(n.toString(16).padStart(len * 2, '0')); } /** * Converts string to bytes using UTF8 encoding. * @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99]) */ export function utf8ToBytes(str) { if (typeof str !== 'string') throw new Error('string expected'); return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 } /** * Converts bytes to string using UTF8 encoding. * @example bytesToUtf8(new Uint8Array([97, 98, 99])) // 'abc' */ export function bytesToUtf8(bytes) { return new TextDecoder().decode(bytes); } /** * Checks if two U8A use same underlying buffer and overlaps. * This is invalid and can corrupt data. */ export function overlapBytes(a, b) { return (a.buffer === b.buffer && // best we can do, may fail with an obscure Proxy a.byteOffset < b.byteOffset + b.byteLength && // a starts before b end b.byteOffset < a.byteOffset + a.byteLength // b starts before a end ); } /** * If input and output overlap and input starts before output, we will overwrite end of input before * we start processing it, so this is not supported for most ciphers (except chacha/salse, which designed with this) */ export function complexOverlapBytes(input, output) { // This is very cursed. It works somehow, but I'm completely unsure, // reasoning about overlapping aligned windows is very hard. if (overlapBytes(input, output) && input.byteOffset < output.byteOffset) throw new Error('complex overlap of input and output is not supported'); } /** * Copies several Uint8Arrays into one. */ export function concatBytes(...arrays) { let sum = 0; for (let i = 0; i < arrays.length; i++) { const a = arrays[i]; abytes(a); sum += a.length; } const res = new Uint8Array(sum); for (let i = 0, pad = 0; i < arrays.length; i++) { const a = arrays[i]; res.set(a, pad); pad += a.length; } return res; } export function checkOpts(defaults, opts) { if (opts == null || typeof opts !== 'object') throw new Error('options must be defined'); const merged = Object.assign(defaults, opts); return merged; } /** Compares 2 uint8array-s in kinda constant time. */ export 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; } /** * Wraps a cipher: validates args, ensures encrypt() can only be called once. * @__NO_SIDE_EFFECTS__ */ export const wrapCipher = (params, constructor) => { function wrappedCipher(key, ...args) { // Validate key abytes(key, undefined, 'key'); // Big-Endian hardware is rare. Just in case someone still decides to run ciphers: if (!isLE) throw new Error('Non little-endian hardware is not yet supported'); // Validate nonce if nonceLength is present if (params.nonceLength !== undefined) { const nonce = args[0]; abytes(nonce, params.varSizeNonce ? undefined : params.nonceLength, 'nonce'); } // Validate AAD if tagLength present const tagl = params.tagLength; if (tagl && args[1] !== undefined) abytes(args[1], undefined, 'AAD'); const cipher = constructor(key, ...args); const checkOutput = (fnLength, output) => { if (output !== undefined) { if (fnLength !== 2) throw new Error('cipher output not supported'); abytes(output, undefined, 'output'); } }; // Create wrapped cipher with validation and single-use encryption let called = false; const wrCipher = { encrypt(data, output) { if (called) throw new Error('cannot encrypt() twice with same key + nonce'); called = true; abytes(data); checkOutput(cipher.encrypt.length, output); return cipher.encrypt(data, output); }, decrypt(data, output) { abytes(data); if (tagl && data.length < tagl) throw new Error('"ciphertext" expected length bigger than tagLength=' + tagl); checkOutput(cipher.decrypt.length, output); return cipher.decrypt(data, output); }, }; return wrCipher; } Object.assign(wrappedCipher, params); return wrappedCipher; }; /** * By default, returns u8a of length. * When out is available, it checks it for validity and uses it. */ export function getOutput(expectedLength, out, onlyAligned = true) { if (out === undefined) return new Uint8Array(expectedLength); if (out.length !== expectedLength) throw new Error('"output" expected Uint8Array of length ' + expectedLength + ', got: ' + out.length); if (onlyAligned && !isAligned32(out)) throw new Error('invalid output, must be aligned'); return out; } export function u64Lengths(dataLength, aadLength, isLE) { abool(isLE); const num = new Uint8Array(16); const view = createView(num); view.setBigUint64(0, BigInt(aadLength), isLE); view.setBigUint64(8, BigInt(dataLength), isLE); return num; } // Is byte array aligned to 4 byte offset (u32)? export function isAligned32(bytes) { return bytes.byteOffset % 4 === 0; } // copy bytes to new u8a (aligned). Because Buffer.slice is broken. export function copyBytes(bytes) { return Uint8Array.from(bytes); } /** Cryptographically secure PRNG. Uses internal OS-level `crypto.getRandomValues`. */ export function randomBytes(bytesLength = 32) { const cr = typeof globalThis === 'object' ? globalThis.crypto : null; if (typeof cr?.getRandomValues !== 'function') throw new Error('crypto.getRandomValues must be defined'); return cr.getRandomValues(new Uint8Array(bytesLength)); } /** * Uses CSPRG for nonce, nonce injected in ciphertext. * For `encrypt`, a `nonceBytes`-length buffer is fetched from CSPRNG and * prepended to encrypted ciphertext. For `decrypt`, first `nonceBytes` of ciphertext * are treated as nonce. * * NOTE: Under the same key, using random nonces (e.g. `managedNonce`) with AES-GCM and ChaCha * should be limited to `2**23` (8M) messages to get a collision chance of `2**-50`. Stretching to * `2**32` (4B) messages, chance would become `2**-33` - still negligible, but creeping up. * @example * const gcm = managedNonce(aes.gcm); * const ciphr = gcm(key).encrypt(data); * const plain = gcm(key).decrypt(ciph); */ export function managedNonce(fn, randomBytes_ = randomBytes) { const { nonceLength } = fn; anumber(nonceLength); const addNonce = (nonce, ciphertext) => { const out = concatBytes(nonce, ciphertext); ciphertext.fill(0); return out; }; // NOTE: we cannot support DST here, it would be mistake: // - we don't know how much dst length cipher requires // - nonce may unalign dst and break everything // - we create new u8a anyway (concatBytes) // - previously we passed all args to cipher, but that was mistake! return ((key, ...args) => ({ encrypt(plaintext) { abytes(plaintext); const nonce = randomBytes_(nonceLength); const encrypted = fn(key, nonce, ...args).encrypt(plaintext); // @ts-ignore if (encrypted instanceof Promise) return encrypted.then((ct) => addNonce(nonce, ct)); return addNonce(nonce, encrypted); }, decrypt(ciphertext) { abytes(ciphertext); const nonce = ciphertext.subarray(0, nonceLength); const decrypted = ciphertext.subarray(nonceLength); return fn(key, nonce, ...args).decrypt(decrypted); }, })); } //# sourceMappingURL=utils.js.map