From 2df3b11092a8f7ff1047924b9f92159676c8fa4d Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 8 Feb 2024 16:01:47 +0000 Subject: [PATCH] Some refactoring. Improve tree-shaking --- package.json | 1 + src/_assert.ts | 11 ++++---- src/_micro.ts | 34 +++++++++++++----------- src/aes.ts | 66 ++++++++++++++++++++++++++--------------------- src/chacha.ts | 8 ++---- src/crypto.ts | 10 +++---- src/cryptoNode.ts | 10 +++---- src/ff1.ts | 2 +- src/salsa.ts | 6 ++--- src/utils.ts | 24 +++++++---------- 10 files changed, 84 insertions(+), 88 deletions(-) diff --git a/package.json b/package.json index da1b6df..4b2d492 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "url": "git+https://github.com/paulmillr/noble-ciphers.git" }, "license": "MIT", + "sideEffects": false, "devDependencies": { "@scure/base": "1.1.3", "fast-check": "3.0.0", diff --git a/src/_assert.ts b/src/_assert.ts index 7ffe44c..e77e0f9 100644 --- a/src/_assert.ts +++ b/src/_assert.ts @@ -1,17 +1,15 @@ function number(n: number) { - if (!Number.isSafeInteger(n) || n < 0) throw new Error(`wrong positive integer: ${n}`); + if (!Number.isSafeInteger(n) || n < 0) throw new Error(`positive integer expected, not ${n}`); } function bool(b: boolean) { if (typeof b !== 'boolean') throw new Error(`boolean expected, not ${b}`); } -// TODO: merge with utils -function isBytes(a: unknown): a is Uint8Array { +export function isBytes(a: unknown): a is Uint8Array { return ( - a != null && - typeof a === 'object' && - (a instanceof Uint8Array || a.constructor.name === 'Uint8Array') + a instanceof Uint8Array || + (a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array') ); } @@ -38,6 +36,7 @@ function exists(instance: any, 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'); } + function output(out: any, instance: any) { bytes(out); const min = instance.outputLen; diff --git a/src/_micro.ts b/src/_micro.ts index 30c7f82..77e42ee 100644 --- a/src/_micro.ts +++ b/src/_micro.ts @@ -1,9 +1,4 @@ /*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */ - -// micro-noble-ciphers: more auditable, but slower version of salsa20, chacha & poly1305. -// Implements the same algorithms that are present in other files, -// but without unrolled loops (https://en.wikipedia.org/wiki/Loop_unrolling). - // prettier-ignore import { Cipher, XorStream, createView, setBigUint64, wrapCipher, @@ -12,6 +7,12 @@ import { import { createCipher, rotl } from './_arx.js'; import { bytes as abytes } from './_assert.js'; +/* +noble-ciphers-micro: more auditable, but slower version of salsa20, chacha & poly1305. +Implements the same algorithms that are present in other files, but without +unrolled loops (https://en.wikipedia.org/wiki/Loop_unrolling). +*/ + function bytesToNumberLE(bytes: Uint8Array): bigint { return hexToNumber(bytesToHex(Uint8Array.from(bytes).reverse())); } @@ -135,12 +136,15 @@ export function hchacha(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uin /** * salsa20, 12-byte nonce. */ -export const salsa20 = createCipher(salsaCore, { allowShortKeys: true, counterRight: true }); +export const salsa20 = /* @__PURE__ */ createCipher(salsaCore, { + allowShortKeys: true, + counterRight: true, +}); /** * xsalsa20, 24-byte nonce. */ -export const xsalsa20 = createCipher(salsaCore, { +export const xsalsa20 = /* @__PURE__ */ createCipher(salsaCore, { counterRight: true, extendNonceFn: hsalsa, }); @@ -148,7 +152,7 @@ export const xsalsa20 = createCipher(salsaCore, { /** * chacha20 non-RFC, original version by djb. 8-byte nonce, 8-byte counter. */ -export const chacha20orig = createCipher(chachaCore, { +export const chacha20orig = /* @__PURE__ */ createCipher(chachaCore, { allowShortKeys: true, counterRight: false, counterLength: 8, @@ -157,7 +161,7 @@ export const chacha20orig = createCipher(chachaCore, { /** * chacha20 RFC 8439 (IETF / TLS). 12-byte nonce, 4-byte counter. */ -export const chacha20 = createCipher(chachaCore, { +export const chacha20 = /* @__PURE__ */ createCipher(chachaCore, { counterRight: false, counterLength: 4, }); @@ -165,7 +169,7 @@ export const chacha20 = createCipher(chachaCore, { /** * xchacha20 eXtended-nonce. https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha */ -export const xchacha20 = createCipher(chachaCore, { +export const xchacha20 = /* @__PURE__ */ createCipher(chachaCore, { counterRight: false, counterLength: 8, extendNonceFn: hchacha, @@ -174,7 +178,7 @@ export const xchacha20 = createCipher(chachaCore, { /** * 8-round chacha from the original paper. */ -export const chacha8 = createCipher(chachaCore, { +export const chacha8 = /* @__PURE__ */ createCipher(chachaCore, { counterRight: false, counterLength: 4, rounds: 8, @@ -183,7 +187,7 @@ export const chacha8 = createCipher(chachaCore, { /** * 12-round chacha from the original paper. */ -export const chacha12 = createCipher(chachaCore, { +export const chacha12 = /* @__PURE__ */ createCipher(chachaCore, { counterRight: false, counterLength: 4, rounds: 12, @@ -240,7 +244,7 @@ function computeTag( /** * xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa. */ -export const xsalsa20poly1305 = wrapCipher( +export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( { blockSize: 64, nonceLength: 24, tagLength: 16 }, function xsalsa20poly1305(key: Uint8Array, nonce: Uint8Array) { abytes(key); @@ -306,7 +310,7 @@ export const _poly1305_aead = /** * chacha20-poly1305 12-byte-nonce chacha. */ -export const chacha20poly1305 = wrapCipher( +export const chacha20poly1305 = /* @__PURE__ */ wrapCipher( { blockSize: 64, nonceLength: 12, tagLength: 16 }, _poly1305_aead(chacha20) ); @@ -315,7 +319,7 @@ export const chacha20poly1305 = wrapCipher( * xchacha20-poly1305 eXtended-nonce (24 bytes) chacha. * With 24-byte nonce, it's safe to use fill it with random (CSPRNG). */ -export const xchacha20poly1305 = wrapCipher( +export const xchacha20poly1305 = /* @__PURE__ */ wrapCipher( { blockSize: 64, nonceLength: 24, tagLength: 16 }, _poly1305_aead(xchacha20) ); diff --git a/src/aes.ts b/src/aes.ts index 596ffb7..6cbb506 100644 --- a/src/aes.ts +++ b/src/aes.ts @@ -1,19 +1,24 @@ -import { wrapCipher, Cipher, CipherWithOutput, equalBytes, u32, u8 } from './utils.js'; -import { createView, setBigUint64 } from './utils.js'; +// prettier-ignore +import { + wrapCipher, Cipher, CipherWithOutput, + createView, setBigUint64, equalBytes, u32, u8, +} from './utils.js'; import { ghash, polyval } from './_polyval.js'; import { bytes as abytes } from './_assert.js'; -// AES (Advanced Encryption Standard) aka Rijndael block cipher. -// -// Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256bit). Every round: -// 1. **S-box**, table substitution -// 2. **Shift rows**, cyclic shift left of all rows of data array -// 3. **Mix columns**, multiplying every column by fixed polynomial -// 4. **Add round key**, round_key xor i-th column of array -// -// Resources: -// - FIPS-197 https://csrc.nist.gov/files/pubs/fips/197/final/docs/fips-197.pdf -// - Original proposal: https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf +/* +AES (Advanced Encryption Standard) aka Rijndael block cipher. + +Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256 bits). In every round: +1. **S-box**, table substitution +2. **Shift rows**, cyclic shift left of all rows of data array +3. **Mix columns**, multiplying every column by fixed polynomial +4. **Add round key**, round_key xor i-th column of array + +Resources: +- FIPS-197 https://csrc.nist.gov/files/pubs/fips/197/final/docs/fips-197.pdf +- Original proposal: https://csrc.nist.gov/csrc/media/projects/cryptographic-standards-and-guidelines/documents/aes-development/rijndael-ammended.pdf +*/ const BLOCK_SIZE = 16; const BLOCK_SIZE32 = 4; @@ -24,6 +29,7 @@ const POLY = 0x11b; // 1 + x + x**3 + x**4 + x**8 function mul2(n: number) { return (n << 1) ^ (POLY & -(n >> 7)); } + function mul(a: number, b: number) { let res = 0; for (; b > 0; b >>= 1) { @@ -36,21 +42,21 @@ function mul(a: number, b: number) { // AES S-box is generated using finite field inversion, // an affine transform, and xor of a constant 0x63. -const _sbox = /* @__PURE__ */ (() => { +const sbox = /* @__PURE__ */ (() => { let t = new Uint8Array(256); for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x)) t[i] = x; - const sbox = new Uint8Array(256); - sbox[0] = 0x63; // first elm + const box = new Uint8Array(256); + box[0] = 0x63; // first elm for (let i = 0; i < 255; i++) { let x = t[255 - i]; x |= x << 8; - sbox[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff; + box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff; } - return sbox; + return box; })(); // Inverted S-box -const _inv_sbox = /* @__PURE__ */ _sbox.map((_, j) => _sbox.indexOf(j)); +const invSbox = /* @__PURE__ */ sbox.map((_, j) => sbox.indexOf(j)); // Rotate u32 by 8 const rotr32_8 = (n: number) => (n << 24) | (n >>> 8); @@ -80,16 +86,16 @@ function genTtable(sbox: Uint8Array, fn: (n: number) => number) { return { sbox, sbox2, T0, T1, T2, T3, T01, T23 }; } -const TABLE_ENC = /* @__PURE__ */ genTtable( - _sbox, +const tableEncoding = /* @__PURE__ */ genTtable( + sbox, (s: number) => (mul(s, 3) << 24) | (s << 16) | (s << 8) | mul(s, 2) ); -const TABLE_DEC = /* @__PURE__ */ genTtable( - _inv_sbox, +const tableDecoding = /* @__PURE__ */ genTtable( + invSbox, (s) => (mul(s, 11) << 24) | (mul(s, 13) << 16) | (mul(s, 9) << 8) | mul(s, 14) ); -const POWX = /* @__PURE__ */ (() => { +const xPowers = /* @__PURE__ */ (() => { const p = new Uint8Array(16); for (let i = 0, x = 1; i < 16; i++, x = mul2(x)) p[i] = x; return p; @@ -100,7 +106,7 @@ export function expandKeyLE(key: Uint8Array): Uint32Array { const len = key.length; if (![16, 24, 32].includes(len)) throw new Error(`aes: wrong key size: should be 16, 24 or 32, got: ${len}`); - const { sbox2 } = TABLE_ENC; + const { sbox2 } = tableEncoding; const k32 = u32(key); const Nk = k32.length; const subByte = (n: number) => applySbox(sbox2, n, n, n, n); @@ -109,7 +115,7 @@ export function expandKeyLE(key: Uint8Array): Uint32Array { // 4.3.1 Key expansion for (let i = Nk; i < xk.length; i++) { let t = xk[i - 1]; - if (i % Nk === 0) t = subByte(rotr32_8(t)) ^ POWX[i / Nk - 1]; + if (i % Nk === 0) t = subByte(rotr32_8(t)) ^ xPowers[i / Nk - 1]; else if (Nk > 6 && i % Nk === 4) t = subByte(t); xk[i] = xk[i - Nk] ^ t; } @@ -120,8 +126,8 @@ export function expandKeyDecLE(key: Uint8Array): Uint32Array { const encKey = expandKeyLE(key); const xk = encKey.slice(); const Nk = encKey.length; - const { sbox2 } = TABLE_ENC; - const { T0, T1, T2, T3 } = TABLE_DEC; + const { sbox2 } = tableEncoding; + const { T0, T1, T2, T3 } = tableDecoding; // Inverse key by chunks of 4 (rounds) for (let i = 0; i < Nk; i += 4) { for (let j = 0; j < 4; j++) xk[i + j] = encKey[Nk - i - 4 + j]; @@ -159,7 +165,7 @@ function applySbox(sbox2: Uint16Array, s0: number, s1: number, s2: number, s3: n } function encrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number) { - const { sbox2, T01, T23 } = TABLE_ENC; + const { sbox2, T01, T23 } = tableEncoding; let k = 0; (s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]); const rounds = xk.length / 4 - 2; @@ -179,7 +185,7 @@ function encrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number } function decrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number) { - const { sbox2, T01, T23 } = TABLE_DEC; + const { sbox2, T01, T23 } = tableDecoding; let k = 0; (s0 ^= xk[k++]), (s1 ^= xk[k++]), (s2 ^= xk[k++]), (s3 ^= xk[k++]); const rounds = xk.length / 4 - 2; diff --git a/src/chacha.ts b/src/chacha.ts index 2385f6b..bcd9ce6 100644 --- a/src/chacha.ts +++ b/src/chacha.ts @@ -1,10 +1,6 @@ +// prettier-ignore import { - wrapCipher, - CipherWithOutput, - XorStream, - createView, - equalBytes, - setBigUint64, + wrapCipher, CipherWithOutput, XorStream, createView, equalBytes, setBigUint64, } from './utils.js'; import { poly1305 } from './_poly1305.js'; import { createCipher, rotl } from './_arx.js'; diff --git a/src/crypto.ts b/src/crypto.ts index 3fc96a1..2ada69a 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,17 +1,15 @@ // We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+. // See utils.ts for details. declare const globalThis: Record | undefined; -const crypto = - typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; +const cr = typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; export function randomBytes(bytesLength = 32): Uint8Array { - if (crypto && typeof crypto.getRandomValues === 'function') { - return crypto.getRandomValues(new Uint8Array(bytesLength)); - } + if (cr && typeof cr.getRandomValues === 'function') + return cr.getRandomValues(new Uint8Array(bytesLength)); throw new Error('crypto.getRandomValues must be defined'); } export function getWebcryptoSubtle() { - if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) return crypto.subtle; + if (cr && typeof cr.subtle === 'object' && cr.subtle != null) return cr.subtle; throw new Error('crypto.subtle must be defined'); } diff --git a/src/cryptoNode.ts b/src/cryptoNode.ts index 1f613ed..772f3b3 100644 --- a/src/cryptoNode.ts +++ b/src/cryptoNode.ts @@ -3,17 +3,15 @@ // The file will throw on node.js 14 and earlier. // @ts-ignore import * as nc from 'node:crypto'; -const crypto = - nc && typeof nc === 'object' && 'webcrypto' in nc ? (nc.webcrypto as any) : undefined; +const cr = nc && typeof nc === 'object' && 'webcrypto' in nc ? (nc.webcrypto as any) : undefined; export function randomBytes(bytesLength = 32): Uint8Array { - if (crypto && typeof crypto.getRandomValues === 'function') { - return crypto.getRandomValues(new Uint8Array(bytesLength)); - } + if (cr && typeof cr.getRandomValues === 'function') + return cr.getRandomValues(new Uint8Array(bytesLength)); throw new Error('crypto.getRandomValues must be defined'); } export function getWebcryptoSubtle() { - if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) return crypto.subtle; + if (cr && typeof cr.subtle === 'object' && cr.subtle != null) return cr.subtle; throw new Error('crypto.subtle must be defined'); } diff --git a/src/ff1.ts b/src/ff1.ts index 7e5e06e..1e3c579 100644 --- a/src/ff1.ts +++ b/src/ff1.ts @@ -1,6 +1,6 @@ import { Cipher, bytesToNumberBE, numberToBytesBE } from './utils.js'; import { unsafe } from './aes.js'; -// NOTE: no point in inlining encrypt instead encryptBlock, since BigInt stuff will be slow +// NOTE: no point in inlining encrypt instead of encryptBlock, since BigInt stuff will be slow const { expandKeyLE, encryptBlock } = unsafe; // Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G. diff --git a/src/salsa.ts b/src/salsa.ts index 52b732d..5cf5b92 100644 --- a/src/salsa.ts +++ b/src/salsa.ts @@ -1,7 +1,7 @@ -import { wrapCipher, Cipher, equalBytes } from './utils.js'; -import { poly1305 } from './_poly1305.js'; -import { createCipher, rotl } from './_arx.js'; import { bytes as abytes } from './_assert.js'; +import { createCipher, rotl } from './_arx.js'; +import { poly1305 } from './_poly1305.js'; +import { wrapCipher, Cipher, equalBytes } from './utils.js'; // Salsa20 stream cipher was released in 2005. // Salsa's goal was to implement AES replacement that does not rely on S-Boxes, diff --git a/src/utils.ts b/src/utils.ts index 9503c62..ec12452 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,5 @@ /*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */ - +import { bytes as abytes, isBytes } from './_assert'; // prettier-ignore export type TypedArray = Int8Array | Uint8ClampedArray | Uint8Array | Uint16Array | Int16Array | Uint32Array | Int32Array; @@ -11,18 +11,6 @@ export const u16 = (arr: TypedArray) => export const u32 = (arr: TypedArray) => new Uint32Array(arr.buffer, arr.byteOffset, Math.floor(arr.byteLength / 4)); -function isBytes(a: unknown): a is Uint8Array { - return ( - a instanceof Uint8Array || - (a != null && typeof a === 'object' && a.constructor.name === 'Uint8Array') - ); -} -function abytes(b: Uint8Array | undefined, ...lengths: number[]) { - if (!isBytes(b)) throw new Error('Uint8Array expected'); - if (lengths.length > 0 && !lengths.includes(b.length)) - throw new Error(`Uint8Array expected of length ${lengths}, not of length=${b.length}`); -} - // Cast array to view export const createView = (arr: TypedArray) => new DataView(arr.buffer, arr.byteOffset, arr.byteLength); @@ -121,10 +109,13 @@ declare const TextDecoder: any; * @example utf8ToBytes('abc') // new Uint8Array([97, 98, 99]) */ export function utf8ToBytes(str: string): Uint8Array { - if (typeof str !== 'string') throw new Error(`utf8ToBytes expected string, got ${typeof str}`); + if (typeof str !== 'string') throw new Error(`string expected, got ${typeof str}`); return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809 } +/** + * @example bytesToUtf8(new Uint8Array([97, 98, 99])) // 'abc' + */ export function bytesToUtf8(bytes: Uint8Array): string { return new TextDecoder().decode(bytes); } @@ -138,7 +129,7 @@ export type Input = Uint8Array | string; export function toBytes(data: Input): Uint8Array { if (typeof data === 'string') data = utf8ToBytes(data); else if (isBytes(data)) data = data.slice(); - else throw new Error(`expected Uint8Array, got ${typeof data}`); + else throw new Error(`Uint8Array expected, got ${typeof data}`); return data; } @@ -221,6 +212,9 @@ export type CipherWithOutput = Cipher & { // If function support multiple nonceLength's, we return best one export type CipherParams = { blockSize: number; nonceLength?: number; tagLength?: number }; export type CipherCons = (key: Uint8Array, ...args: T) => Cipher; +/** + * @__NO_SIDE_EFFECTS__ + */ export const wrapCipher = , P extends CipherParams>( params: P, c: C