From 699d784fe5e4d0d954ea2fd57a364164a198d988 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 18 Oct 2023 18:29:58 +0000 Subject: [PATCH] Speed-up polyval, re-implement ff1 --- README.md | 153 ++++++++--- benchmark/aes.js | 147 ++++++++--- benchmark/package-lock.json | 7 + benchmark/package.json | 1 + build/input.js | 5 +- package.json | 12 +- src/_micro.ts | 64 +++-- src/_polyval.ts | 172 +++++++----- src/aes.ts | 509 +++++++++++++++++++----------------- src/chacha.ts | 12 +- src/{webcrypto => }/ff1.ts | 38 +-- src/salsa.ts | 100 +++---- src/utils.ts | 15 +- src/webcrypto/aes.ts | 74 +++--- src/webcrypto/utils.ts | 76 ++++-- test/arx.test.js | 297 +++++++++++++++++++++ test/basic.test.js | 341 +++++------------------- test/ff1.test.js | 25 +- test/index.js | 1 + tsconfig.esm.json | 15 +- tsconfig.json | 15 +- 21 files changed, 1228 insertions(+), 851 deletions(-) rename src/{webcrypto => }/ff1.ts (81%) create mode 100644 test/arx.test.js diff --git a/README.md b/README.md index 914e48f..eb56267 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # noble-ciphers -Auditable & minimal JS implementation of Salsa20, ChaCha, Poly1305 & AES-SIV +Auditable & minimal JS implementation of Salsa20, ChaCha & AES - 🔒 Auditable - 🔻 Tree-shaking-friendly: use only what's necessary, other code won't be included - 🏎 [Ultra-fast](#speed), hand-optimized for caveats of JS engines - 🔍 Unique tests ensure correctness: property-based, cross-library and Wycheproof vectors -- 💼 AES: SIV (Nonce Misuse-Resistant encryption), simple GCM/CTR/CBC webcrypto wrapper +- 💼 AES: very fast ECB, CBC, CTR, GCM, SIV (nonce misuse-resistant) - 💃 Salsa20, ChaCha, XSalsa20, XChaCha, Poly1305, ChaCha8, ChaCha12 - ✍️ FF1 format-preserving encryption - 🧂 Compatible with NaCl / libsodium secretbox @@ -44,21 +44,17 @@ A standalone file // import * from '@noble/ciphers'; // Error: use sub-imports, to ensure small app size import { xchacha20poly1305 } from '@noble/ciphers/chacha'; // import { xchacha20poly1305 } from 'npm:@noble/ciphers@0.2.0/chacha'; // Deno -import { randomBytes } from '@noble/ciphers/webcrypto/utils'; -const key = randomBytes(32); -const nonce = randomBytes(24); -const stream = xchacha20poly1305(key, nonce); - -import { utf8ToBytes } from '@noble/ciphers/utils'; -const data = utf8ToBytes('hello, noble'); // strings must become Uint8Array -const ciphertext = stream.encrypt(data); -const plaintext = stream.decrypt(ciphertext); // bytesToUtf8(plaintext) ``` +- [Examples](#examples) + - [Encrypt and decrypt with ChaCha20-Poly1305](#encrypt-and-decrypt-with-chacha20-poly1305) + - [Encrypt and decrypt text with AES-GCM-256](#encrypt-and-decrypt-text-with-aes-gcm-256) + - [Securely generate random key and nonce](#securely-generate-random-key-and-nonce) + - [Use managed nonce](#use-managed-nonce) - [Implementations](#implementations) - [salsa: Salsa20 cipher](#salsa) - [chacha: ChaCha cipher](#chacha) - - [webcrypto/aes: friendly wrapper over webcrypto AES](#aes) + - [aes: AES cipher](#aes) - [ff1: format-preserving encryption](#ff1) - [Guidance](#guidance) - [How to encrypt properly](#how-to-encrypt-properly) @@ -70,6 +66,60 @@ const plaintext = stream.decrypt(ciphertext); // bytesToUtf8(plaintext) - [Contributing & testing](#contributing--testing) - [Resources](#resources) +## Examples + +#### Encrypt and decrypt with ChaCha20-Poly1305 + +```js +import { xchacha20poly1305 } from '@noble/ciphers/chacha'; +import { bytesToHex, hexToBytes, bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils'; +const key = hexToBytes('4b7f89bac90a1086fef73f5da2cbe93b2fae9dfbf7678ae1f3e75fd118ddf999'); +const nonce = hexToBytes('9610467513de0bbd7c4cc2c3c64069f1802086fbd3232b13'); +const chacha = xchacha20poly1305(key, nonce); +const data = utf8ToBytes('hello, noble'); +const ciphertext = chacha.encrypt(data); +const data_ = chacha.decrypt(ciphertext); // bytesToUtf8(data_) === data +``` + +#### Encrypt and decrypt text with AES-GCM-256 + +```js +import { gcm } from '@noble/ciphers/aes'; +import { bytesToHex, hexToBytes, bytesToUtf8, utf8ToBytes } from '@noble/ciphers/utils'; +const key = hexToBytes('5296fb2c5ceab0f59367994e5d81d9014027255f12336fabcd29596c2e9ecd87'); +const nonce = hexToBytes('9610467513de0bbd7c4cc2c3c64069f1802086fbd3232b13'); +const aes = gcm(key, nonce); +const data = utf8ToBytes('hello, noble'); +const ciphertext = aes.encrypt(data); +const data_ = aes.decrypt(ciphertext); // bytesToUtf8(data_) === data +``` + +#### Use managed nonce + +```js +import { xchacha20poly1305 } from '@noble/ciphers/chacha'; +import { managedNonce } from '@noble/ciphers/webcrypto/utils' +import { bytesToHex, hexToBytes, utf8ToBytes } from '@noble/ciphers/utils'; +const key = hexToBytes('fa686bfdffd3758f6377abbc23bf3d9bdc1a0dda4a6e7f8dbdd579fa1ff6d7e1'); +const chacha = managedNonce(xchacha20poly1305)(key); // manages nonces for you +const data = utf8ToBytes('hello, noble'); +const ciphertext = chacha.encrypt(data); +const data_ = chacha.decrypt(ciphertext); +``` + +#### Securely generate random key and nonce + +```js +import { xchacha20poly1305 } from '@noble/ciphers/chacha'; +import { randomBytes } from '@noble/ciphers/webcrypto/utils'; +const rkey = randomBytes(32); +const rnonce = randomBytes(24); +const chacha = xchacha20poly1305(rkey, rnonce); +const data = utf8ToBytes('hello, noble'); +const ciphertext = chacha.encrypt(data); +const plaintext = chacha.decrypt(ciphertext); +``` + ## Implementations ### Salsa @@ -183,32 +233,67 @@ using chacha-poly or xsalsa-poly. ### AES ```js -import { aes_128_gcm, aes_128_ctr, aes_128_cbc } from '@noble/ciphers/webcrypto/aes'; -import { aes_256_gcm, aes_256_ctr, aes_256_cbc } from '@noble/ciphers/webcrypto/aes'; +import { gcm, siv, ctr, cbc, ecb } from '@noble/ciphers/aes'; -for (let cipher of [aes_256_gcm, aes_256_ctr, aes_256_cbc]) { - const stream_new = cipher(key, nonce); - const ciphertext_new = await stream_new.encrypt(plaintext); - const plaintext_new = await stream_new.decrypt(ciphertext); +for (let cipher of [gcm, siv, ctr, cbc]) { + const stream = cipher(key, nonce); + const ciphertext_ = stream.encrypt(plaintext); + const plaintext_ = stream.decrypt(ciphertext_); } - -import { aes_256_gcm_siv } from '@noble/ciphers/webcrypto/siv'; -const stream_siv = aes_256_gcm_siv(key, nonce); -await stream_siv.encrypt(plaintext, AAD); ``` AES ([wiki](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)) is a variant of Rijndael block cipher, standardized by NIST. -We don't implement AES in pure JS for now: instead, we wrap WebCrypto built-in -and provide an improved, simple API. There is a reason for this: -webcrypto API is terrible: different block modes require different params. +We provide the fastest available pure JS implementation of AES. Optional [AES-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV) -synthetic initialization vector nonce-misuse-resistant mode is also provided. +nonce-misuse-resistant mode is also provided. Check out [AES internals and block modes](#aes-internals-and-block-modes). +### Webcrypto AES + +```js +// Wrapper over built-in webcrypto. Same API, but async +import { gcm, ctr, cbc } from '@noble/ciphers/webcrypto/aes'; +for (let cipher of [gcm, siv, ctr, cbc]) { + const stream = cipher(key, nonce); + const ciphertext_ = await stream.encrypt(plaintext); + const plaintext_ = await stream.decrypt(ciphertext_); +} +``` + +We also have separate wrapper over asynchronous WebCrypto built-in. + +It's the same as using `crypto.subtle`, but with massively simplified API. + +### Managed nonces + +```js +import { managedNonce } from '@noble/ciphers/webcrypto/utils'; +import { gcm, siv, ctr, cbc, ecb } from '@noble/ciphers/aes'; +import { xsalsa20poly1305 } from '@noble/ciphers/salsa'; +import { chacha20poly1305, xchacha20poly1305 } from '@noble/ciphers/chacha'; + +const wgcm = managedNonce(gcm); +const wsiv = managedNonce(siv); +const wcbc = managedNonce(cbc); +const wctr = managedNonce(ctr); +const wsalsapoly = managedNonce(xsalsa20poly1305); +const wchacha = managedNonce(chacha20poly1305); +const wxchacha = managedNonce(xchacha20poly1305); + +// Now: +const encrypted = wgcm(key).encrypt(data); // no nonces +``` + +We provide API that manages nonce internally instead of exposing them to library's user. + +For `encrypt`, a `nonceBytes`-length buffer is fetched from CSPRNG and prenended to encrypted ciphertext. + +For `decrypt`, first `nonceBytes` of ciphertext are treated as nonce. + ### FF1 Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G. @@ -367,26 +452,26 @@ encrypt (64B) ├─xsalsa20poly1305 x 484,966 ops/sec @ 2μs/op ├─chacha20poly1305 x 442,282 ops/sec @ 2μs/op ├─xchacha20poly1305 x 300,842 ops/sec @ 3μs/op -├─aes-gcm-256 x 93,144 ops/sec @ 10μs/op -└─aes-gcm-siv-256 x 79,339 ops/sec @ 12μs/op +├─gcm-256 x 148,522 ops/sec @ 6μs/op +└─gcm-siv-256 x 118,399 ops/sec @ 8μs/op encrypt (1KB) ├─xsalsa20poly1305 x 143,905 ops/sec @ 6μs/op ├─chacha20poly1305 x 141,663 ops/sec @ 7μs/op ├─xchacha20poly1305 x 122,639 ops/sec @ 8μs/op -├─aes-gcm-256 x 25,685 ops/sec @ 38μs/op -└─aes-gcm-siv-256 x 24,762 ops/sec @ 40μs/op +├─gcm-256 x 42,645 ops/sec @ 23μs/op +└─gcm-siv-256 x 40,112 ops/sec @ 24μs/op encrypt (8KB) ├─xsalsa20poly1305 x 23,373 ops/sec @ 42μs/op ├─chacha20poly1305 x 23,683 ops/sec @ 42μs/op ├─xchacha20poly1305 x 23,066 ops/sec @ 43μs/op -├─aes-gcm-256 x 4,067 ops/sec @ 245μs/op -└─aes-gcm-siv-256 x 4,128 ops/sec @ 242μs/op +├─gcm-256 x 8,381 ops/sec @ 119μs/op +└─gcm-siv-256 x 8,020 ops/sec @ 124μs/op encrypt (1MB) ├─xsalsa20poly1305 x 193 ops/sec @ 5ms/op ├─chacha20poly1305 x 196 ops/sec @ 5ms/op ├─xchacha20poly1305 x 195 ops/sec @ 5ms/op -├─aes-gcm-256 x 33 ops/sec @ 30ms/op -└─aes-gcm-siv-256 x 33 ops/sec @ 29ms/op +├─gcm-256 x 75 ops/sec @ 13ms/op +└─gcm-siv-256 x 72 ops/sec @ 13ms/op ``` Unauthenticated encryption: diff --git a/benchmark/aes.js b/benchmark/aes.js index 9010f93..72ec20a 100644 --- a/benchmark/aes.js +++ b/benchmark/aes.js @@ -1,7 +1,7 @@ import { utils as butils } from 'micro-bmark'; import { createCipheriv, createDecipheriv } from 'node:crypto'; -import { aes_256_gcm, aes_128_gcm } from '@noble/ciphers/webcrypto/aes'; +import * as webcrypto from '@noble/ciphers/webcrypto/aes'; import { concatBytes } from '@noble/ciphers/utils'; import * as aes from '@noble/ciphers/aes'; import { @@ -14,6 +14,7 @@ import { import { CTR as STABLE_CTR } from '@stablelib/ctr'; import { AES as STABLE_AES } from '@stablelib/aes'; import { GCM as STABLE_GCM } from '@stablelib/gcm'; +import { default as aesjs } from 'aes-js'; // Works for gcm only? const nodeGCM = (name) => { @@ -157,6 +158,14 @@ export const CIPHERS = { opts: { key: buf(16), iv: buf(16) }, node: nodeAES('aes-128-ctr'), stablelib: stableCTR, + aesjs: { + encrypt: (buf, opts) => new aesjs.ModeOfOperation.ctr(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => new aesjs.ModeOfOperation.ctr(opts.key, opts.iv).decrypt(buf), + }, + nobleOld: { + encrypt: (buf, opts) => webcrypto.ctr(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.ctr(opts.key, opts.iv).decrypt(buf), + }, noble: { encrypt: (buf, opts) => aes.ctr(opts.key, opts.iv).encrypt(buf), decrypt: (buf, opts) => aes.ctr(opts.key, opts.iv).decrypt(buf), @@ -166,6 +175,14 @@ export const CIPHERS = { opts: { key: buf(32), iv: buf(16) }, node: nodeAES('aes-256-ctr'), stablelib: stableCTR, + aesjs: { + encrypt: (buf, opts) => new aesjs.ModeOfOperation.ctr(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => new aesjs.ModeOfOperation.ctr(opts.key, opts.iv).decrypt(buf), + }, + nobleOld: { + encrypt: (buf, opts) => webcrypto.ctr(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.ctr(opts.key, opts.iv).decrypt(buf), + }, noble: { encrypt: (buf, opts) => aes.ctr(opts.key, opts.iv).encrypt(buf), decrypt: (buf, opts) => aes.ctr(opts.key, opts.iv).decrypt(buf), @@ -175,6 +192,16 @@ export const CIPHERS = { opts: { key: buf(16), iv: buf(16) }, node: nodeAES('aes-128-cbc'), stablelib: stableCBC, + aesjs: { + encrypt: (buf, opts) => + new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).encrypt(aesjs.padding.pkcs7.pad(buf)), + decrypt: (buf, opts) => + aesjs.padding.pkcs7.strip(new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).decrypt(buf)), + }, + nobleOld: { + encrypt: (buf, opts) => webcrypto.cbc(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.cbc(opts.key, opts.iv).decrypt(buf), + }, noble: { encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv).encrypt(buf), decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv).decrypt(buf), @@ -184,6 +211,16 @@ export const CIPHERS = { opts: { key: buf(32), iv: buf(16) }, node: nodeAES('aes-256-cbc'), stablelib: stableCBC, + aesjs: { + encrypt: (buf, opts) => + new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).encrypt(aesjs.padding.pkcs7.pad(buf)), + decrypt: (buf, opts) => + aesjs.padding.pkcs7.strip(new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).decrypt(buf)), + }, + nobleOld: { + encrypt: (buf, opts) => webcrypto.cbc(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.cbc(opts.key, opts.iv).decrypt(buf), + }, noble: { encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv).encrypt(buf), decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv).decrypt(buf), @@ -193,6 +230,12 @@ export const CIPHERS = { opts: { key: buf(16), iv: null }, node: nodeAES('aes-128-ecb'), stablelib: stableECB, + aesjs: { + encrypt: (buf, opts) => + new aesjs.ModeOfOperation.ecb(opts.key).encrypt(aesjs.padding.pkcs7.pad(buf)), + decrypt: (buf, opts) => + aesjs.padding.pkcs7.strip(new aesjs.ModeOfOperation.ecb(opts.key).decrypt(buf)), + }, noble: { encrypt: (buf, opts) => aes.ecb(opts.key).encrypt(buf), decrypt: (buf, opts) => aes.ecb(opts.key).decrypt(buf), @@ -202,44 +245,66 @@ export const CIPHERS = { opts: { key: buf(32), iv: null }, node: nodeAES('aes-256-ecb'), stablelib: stableECB, + aesjs: { + encrypt: (buf, opts) => + new aesjs.ModeOfOperation.ecb(opts.key).encrypt(aesjs.padding.pkcs7.pad(buf)), + decrypt: (buf, opts) => + aesjs.padding.pkcs7.strip(new aesjs.ModeOfOperation.ecb(opts.key).decrypt(buf)), + }, noble: { encrypt: (buf, opts) => aes.ecb(opts.key).encrypt(buf), decrypt: (buf, opts) => aes.ecb(opts.key).decrypt(buf), }, }, // Not very important, but useful (also cross-test) - 'cbc-128-no-padding': { - opts: { key: buf(16), iv: buf(16), blockSize: 16 }, - node: nodeAES('aes-128-cbc', false), - noble: { - encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).encrypt(buf), - decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).decrypt(buf), - }, - }, - 'cbc-256-no-padding': { - opts: { key: buf(32), iv: buf(16), blockSize: 16 }, - node: nodeAES('aes-256-cbc', false), - noble: { - encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).encrypt(buf), - decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).decrypt(buf), - }, - }, - 'ecb-128-no-padding': { - opts: { key: buf(16), iv: null, blockSize: 16 }, - node: nodeAES('aes-128-ecb', false), - noble: { - encrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).encrypt(buf), - decrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).decrypt(buf), - }, - }, - 'ecb-256-no-padding': { - opts: { key: buf(32), iv: null, blockSize: 16 }, - node: nodeAES('aes-256-ecb', false), - noble: { - encrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).encrypt(buf), - decrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).decrypt(buf), - }, - }, + // 'cbc-128-no-padding': { + // opts: { key: buf(16), iv: buf(16), blockSize: 16 }, + // node: nodeAES('aes-128-cbc', false), + // aesjs: { + // encrypt: (buf, opts) => new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).encrypt(buf), + // decrypt: (buf, opts) => new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).decrypt(buf), + // }, + // noble: { + // encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).encrypt(buf), + // decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).decrypt(buf), + // }, + // }, + // 'cbc-256-no-padding': { + // opts: { key: buf(32), iv: buf(16), blockSize: 16 }, + // node: nodeAES('aes-256-cbc', false), + // aesjs: { + // encrypt: (buf, opts) => new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).encrypt(buf), + // decrypt: (buf, opts) => new aesjs.ModeOfOperation.cbc(opts.key, opts.iv).decrypt(buf), + // }, + // noble: { + // encrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).encrypt(buf), + // decrypt: (buf, opts) => aes.cbc(opts.key, opts.iv, { disablePadding: true }).decrypt(buf), + // }, + // }, + // 'ecb-128-no-padding': { + // opts: { key: buf(16), iv: null, blockSize: 16 }, + // node: nodeAES('aes-128-ecb', false), + // aesjs: { + // encrypt: (buf, opts) => new aesjs.ModeOfOperation.ecb(opts.key).encrypt(buf), + // decrypt: (buf, opts) => new aesjs.ModeOfOperation.ecb(opts.key).decrypt(buf), + // }, + // noble: { + // encrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).encrypt(buf), + // decrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).decrypt(buf), + // }, + // }, + // 'ecb-256-no-padding': { + // opts: { key: buf(32), iv: null, blockSize: 16 }, + // node: nodeAES('aes-256-ecb', false), + // aesjs: { + // encrypt: (buf, opts) => new aesjs.ModeOfOperation.ecb(opts.key).encrypt(buf), + // decrypt: (buf, opts) => new aesjs.ModeOfOperation.ecb(opts.key).decrypt(buf), + // }, + // noble: { + // encrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).encrypt(buf), + // decrypt: (buf, opts) => aes.ecb(opts.key, { disablePadding: true }).decrypt(buf), + // }, + // }, // GCM related (slow) 'gcm-128': { opts: { key: buf(16), iv: buf(12) }, @@ -249,8 +314,8 @@ export const CIPHERS = { decrypt: (buf, opts) => new STABLE_GCM(new STABLE_AES(opts.key)).open(opts.iv, buf), }, nobleOld: { - encrypt: (buf, opts) => aes_128_gcm(opts.key, opts.iv).encrypt(buf), - decrypt: (buf, opts) => aes_128_gcm(opts.key, opts.iv).decrypt(buf), + encrypt: (buf, opts) => webcrypto.gcm(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.gcm(opts.key, opts.iv).decrypt(buf), }, noble: { encrypt: (buf, opts) => aes.gcm(opts.key, opts.iv).encrypt(buf), @@ -265,8 +330,8 @@ export const CIPHERS = { decrypt: (buf, opts) => new STABLE_GCM(new STABLE_AES(opts.key)).open(opts.iv, buf), }, nobleOld: { - encrypt: (buf, opts) => aes_256_gcm(opts.key, opts.iv).encrypt(buf), - decrypt: (buf, opts) => aes_256_gcm(opts.key, opts.iv).decrypt(buf), + encrypt: (buf, opts) => webcrypto.gcm(opts.key, opts.iv).encrypt(buf), + decrypt: (buf, opts) => webcrypto.gcm(opts.key, opts.iv).decrypt(buf), }, noble: { encrypt: (buf, opts) => aes.gcm(opts.key, opts.iv).encrypt(buf), @@ -291,9 +356,11 @@ export const CIPHERS = { // buffer title, sample count, data const buffers = [ - { size: '64B', samples: 50_000, data: buf(64) }, - { size: '1KB', samples: 15_000, data: buf(1024) }, - { size: '8KB', samples: 2_000, data: buf(1024 * 8) }, + // { size: '16B', samples: 1_500_000, data: buf(16) }, // common block size + // { size: '32B', samples: 1_500_000, data: buf(32) }, + { size: '64B', samples: 1_000_000, data: buf(64) }, + { size: '1KB', samples: 50_000, data: buf(1024) }, + { size: '8KB', samples: 10_000, data: buf(1024 * 8) }, { size: '1MB', samples: 100, data: buf(1024 * 1024) }, ]; diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 98dafc1..50206f7 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -23,6 +23,7 @@ "@stablelib/xchacha20": "1.0.1", "@stablelib/xchacha20poly1305": "1.0.1", "@stablelib/xsalsa20": "1.0.2", + "aes-js": "^3.1.2", "libsodium-wrappers": "0.7.11", "tweetnacl": "1.0.3" } @@ -33,6 +34,7 @@ "license": "MIT", "devDependencies": { "@scure/base": "1.1.1", + "@types/node": "^20.7.1", "fast-check": "3.0.0", "micro-bmark": "0.3.1", "micro-should": "0.4.0", @@ -366,6 +368,11 @@ "@stablelib/wipe": "^1.0.1" } }, + "node_modules/aes-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", + "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" + }, "node_modules/libsodium": { "version": "0.7.11", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.11.tgz", diff --git a/benchmark/package.json b/benchmark/package.json index 9a23e15..e1e37b3 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -26,6 +26,7 @@ "@stablelib/xchacha20": "1.0.1", "@stablelib/xchacha20poly1305": "1.0.1", "@stablelib/xsalsa20": "1.0.2", + "aes-js": "^3.1.2", "libsodium-wrappers": "0.7.11", "tweetnacl": "1.0.3" } diff --git a/build/input.js b/build/input.js index 211060c..c5ea152 100644 --- a/build/input.js +++ b/build/input.js @@ -2,6 +2,7 @@ export { xsalsa20poly1305, chacha20poly1305, xchacha20poly1305, salsa20, chacha20, chacha8, chacha12, } from '@noble/ciphers/_micro'; -// export { xsalsa20poly1305 } from '@noble/ciphers/salsa'; +export { ecb, ctr, cbc, gcm, siv } from '@noble/ciphers/aes'; +import { randomBytes } from '@noble/ciphers/webcrypto/utils'; import { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes } from '@noble/ciphers/utils'; -export const utils = { bytesToHex, bytesToUtf8, hexToBytes, utf8ToBytes }; +export const utils = { bytesToHex, bytesToUtf8, hexToBytes, randomBytes, utf8ToBytes }; diff --git a/package.json b/package.json index a44ef1a..f0d1baf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@noble/ciphers", "version": "0.3.0", - "description": "Auditable & minimal JS implementation of Salsa20, ChaCha, Poly1305 & AES-SIV", + "description": "Auditable & minimal JS implementation of Salsa20, ChaCha & AES", "files": [ "esm", "src", @@ -83,6 +83,11 @@ "import": "./esm/salsa.js", "default": "./salsa.js" }, + "./ff1": { + "types": "./ff1.d.ts", + "import": "./esm/ff1.js", + "default": "./ff1.js" + }, "./utils": { "types": "./utils.d.ts", "import": "./esm/utils.js", @@ -98,11 +103,6 @@ "import": "./esm/webcrypto/aes.js", "default": "./webcrypto/aes.js" }, - "./webcrypto/ff1": { - "types": "./webcrypto/ff1.d.ts", - "import": "./esm/webcrypto/ff1.js", - "default": "./webcrypto/ff1.js" - }, "./webcrypto/utils": { "types": "./webcrypto/utils.d.ts", "import": "./esm/webcrypto/utils.js", diff --git a/src/_micro.ts b/src/_micro.ts index 627fbb4..26c4f2d 100644 --- a/src/_micro.ts +++ b/src/_micro.ts @@ -6,7 +6,7 @@ // prettier-ignore import { - Cipher, XorStream, createView, setBigUint64, + Cipher, XorStream, createView, setBigUint64, wrapCipher, bytesToNumberLE, concatBytes, ensureBytes, equalBytes, numberToBytesBE, u32, u8, } from './utils.js'; import { createCipher } from './_arx.js'; @@ -235,30 +235,33 @@ function computeTag( /** * xsalsa20-poly1305 eXtended-nonce (24 bytes) salsa. */ -export function xsalsa20poly1305(key: Uint8Array, nonce: Uint8Array) { - ensureBytes(key); - ensureBytes(nonce); - return { - encrypt: (plaintext: Uint8Array) => { - ensureBytes(plaintext); - const m = concatBytes(new Uint8Array(32), plaintext); - const c = xsalsa20(key, nonce, m); - const authKey = c.subarray(0, 32); - const data = c.subarray(32); - const tag = poly1305(data, authKey); - return concatBytes(tag, data); - }, - decrypt: (ciphertext: Uint8Array) => { - ensureBytes(ciphertext); - if (ciphertext.length < 16) throw new Error('encrypted data must be at least 16 bytes'); - const c = concatBytes(new Uint8Array(16), ciphertext); - const authKey = xsalsa20(key, nonce, new Uint8Array(32)); - const tag = poly1305(c.subarray(32), authKey); - if (!equalBytes(c.subarray(16, 32), tag)) throw new Error('invalid poly1305 tag'); - return xsalsa20(key, nonce, c).subarray(32); - }, - }; -} +export const xsalsa20poly1305 = wrapCipher( + { blockSize: 64, nonceLength: 24, tagLength: 16 }, + function xsalsa20poly1305(key: Uint8Array, nonce: Uint8Array) { + ensureBytes(key); + ensureBytes(nonce); + return { + encrypt: (plaintext: Uint8Array) => { + ensureBytes(plaintext); + const m = concatBytes(new Uint8Array(32), plaintext); + const c = xsalsa20(key, nonce, m); + const authKey = c.subarray(0, 32); + const data = c.subarray(32); + const tag = poly1305(data, authKey); + return concatBytes(tag, data); + }, + decrypt: (ciphertext: Uint8Array) => { + ensureBytes(ciphertext); + if (ciphertext.length < 16) throw new Error('encrypted data must be at least 16 bytes'); + const c = concatBytes(new Uint8Array(16), ciphertext); + const authKey = xsalsa20(key, nonce, new Uint8Array(32)); + const tag = poly1305(c.subarray(32), authKey); + if (!equalBytes(c.subarray(16, 32), tag)) throw new Error('invalid poly1305 tag'); + return xsalsa20(key, nonce, c).subarray(32); + }, + }; + } +); /** * Alias to xsalsa20-poly1305 @@ -276,7 +279,6 @@ export const _poly1305_aead = ensureBytes(key, keyLength); ensureBytes(nonce); return { - tagLength, encrypt: (plaintext: Uint8Array) => { ensureBytes(plaintext); const res = fn(key, nonce, plaintext, undefined, 1); @@ -299,10 +301,16 @@ export const _poly1305_aead = /** * chacha20-poly1305 12-byte-nonce chacha. */ -export const chacha20poly1305 = _poly1305_aead(chacha20); +export const chacha20poly1305 = wrapCipher( + { blockSize: 64, nonceLength: 12, tagLength: 16 }, + _poly1305_aead(chacha20) +); /** * xchacha20-poly1305 eXtended-nonce (24 bytes) chacha. * With 24-byte nonce, it's safe to use fill it with random (CSPRNG). */ -export const xchacha20poly1305 = _poly1305_aead(xchacha20); +export const xchacha20poly1305 = wrapCipher( + { blockSize: 64, nonceLength: 24, tagLength: 16 }, + _poly1305_aead(xchacha20) +); diff --git a/src/_polyval.ts b/src/_polyval.ts index 00ee3af..ec6e640 100644 --- a/src/_polyval.ts +++ b/src/_polyval.ts @@ -1,4 +1,4 @@ -import { createView, toBytes, Input, Hash, u32 } from './utils.js'; +import { createView, toBytes, Input, Hash, u32, ensureBytes } from './utils.js'; import { exists as aexists, output as aoutput } from './_assert.js'; // GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV. @@ -16,25 +16,25 @@ const ZEROS16 = /* @__PURE__ */ new Uint8Array(16); const ZEROS32 = u32(ZEROS16); const POLY = 0xe1; // v = 2*v % POLY -// 128x128 multiplication table for key. Same as noble-curves, but window size=1. -// TODO: investigate perf boost using bigger window size. -function genMulTable(s0: number, s1: number, s2: number, s3: number): Uint32Array { - const t = new Uint32Array(128 * 4); // 128x128 multiplication table for key - for (let i = 0, pos = 0, t0, t1, t2, t3; i < 128; i++) { - (t[pos++] = s0), (t[pos++] = s1), (t[pos++] = s2), (t[pos++] = s3); - const hiBit = s3 & 1; - t3 = (s2 << 31) | (s3 >>> 1); - t2 = (s1 << 31) | (s2 >>> 1); - t1 = (s0 << 31) | (s1 >>> 1); - t0 = (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)); // reduce % poly - s0 = t0; - s1 = t1; - s2 = t2; - s3 = t3; - // ({ s0, s1, s2, s3 } = mul2(s0, s1, s2, s3)); - } - return t.map(swapLE); // convert to LE, so we can use u32 -} +// v = 2*v % POLY +// NOTE: because x + x = 0 (add/sub is same), mul2(x) != x+x +// We can multiply any number using montgomery ladder and this function (works as double, add is simple xor) +const mul2 = (s0: number, s1: number, s2: number, s3: number) => { + const hiBit = s3 & 1; + return { + s3: (s2 << 31) | (s3 >>> 1), + s2: (s1 << 31) | (s2 >>> 1), + s1: (s0 << 31) | (s1 >>> 1), + s0: (s0 >>> 1) ^ ((POLY << 24) & -(hiBit & 1)), // reduce % poly + }; +}; + +const swapLE = (n: number) => + (((n >>> 0) & 0xff) << 24) | + (((n >>> 8) & 0xff) << 16) | + (((n >>> 16) & 0xff) << 8) | + ((n >>> 24) & 0xff) | + 0; /** * `mulX_POLYVAL(ByteReverse(H))` from spec @@ -54,12 +54,13 @@ export function _toGHASHKey(k: Uint8Array): Uint8Array { return k; } -const swapLE = (n: number) => - ((((n >>> 0) & 0xff) << 24) | - (((n >>> 8) & 0xff) << 16) | - (((n >>> 16) & 0xff) << 8) | - ((n >>> 24) & 0xff)) >>> - 0; +type Value = { s0: number; s1: number; s2: number; s3: number }; + +const estimateWindow = (bytes: number) => { + if (bytes > 64 * 1024) return 8; + if (bytes > 1024) return 4; + return 2; +}; class GHASH implements Hash { readonly blockLen = BLOCK_SIZE; @@ -68,46 +69,72 @@ class GHASH implements Hash { protected s1 = 0; protected s2 = 0; protected s3 = 0; - protected mulTable: Uint32Array; // 128x128 multiplication table for key protected finished = false; - - constructor(key: Input) { + protected t: Value[]; + private W: number; + private windowSize: number; + // We select bits per window adaptively based on expectedLength + constructor(key: Input, expectedLength?: number) { key = toBytes(key); - const v = createView(key); - let k0 = v.getUint32(0, false); - let k1 = v.getUint32(4, false); - let k2 = v.getUint32(8, false); - let k3 = v.getUint32(12, false); - this.mulTable = genMulTable(k0, k1, k2, k3); + ensureBytes(key, 16); + const kView = createView(key); + let k0 = kView.getUint32(0, false); + let k1 = kView.getUint32(4, false); + let k2 = kView.getUint32(8, false); + let k3 = kView.getUint32(12, false); + // generate table of doubled keys (half of montgomery ladder) + const doubles: Value[] = []; + for (let i = 0; i < 128; i++) { + doubles.push({ s0: swapLE(k0), s1: swapLE(k1), s2: swapLE(k2), s3: swapLE(k3) }); + ({ s0: k0, s1: k1, s2: k2, s3: k3 } = mul2(k0, k1, k2, k3)); + } + const W = estimateWindow(expectedLength || 1024); + if (![1, 2, 4, 8].includes(W)) + throw new Error(`ghash: wrong window size=${W}, should be 2, 4 or 8`); + this.W = W; + const bits = 128; // always 128 bits; + const windows = bits / W; + const windowSize = (this.windowSize = 2 ** W); + const items: Value[] = []; + // Create precompute table for window of W bits + for (let w = 0; w < windows; w++) { + // truth table: 00, 01, 10, 11 + for (let byte = 0; byte < windowSize; byte++) { + // prettier-ignore + let s0 = 0, s1 = 0, s2 = 0, s3 = 0; + for (let j = 0; j < W; j++) { + const bit = (byte >>> (W - j - 1)) & 1; + if (!bit) continue; + const { s0: d0, s1: d1, s2: d2, s3: d3 } = doubles[W * w + j]; + (s0 ^= d0), (s1 ^= d1), (s2 ^= d2), (s3 ^= d3); + } + items.push({ s0, s1, s2, s3 }); + } + } + this.t = items; } protected _updateBlock(s0: number, s1: number, s2: number, s3: number) { (s0 ^= this.s0), (s1 ^= this.s1), (s2 ^= this.s2), (s3 ^= this.s3); - const { mulTable } = this; - const mulNum = (num: number, pos: number, o0: number, o1: number, o2: number, o3: number) => { + const { W, t, windowSize } = this; + // prettier-ignore + let o0 = 0, o1 = 0, o2 = 0, o3 = 0; + const mask = (1 << W) - 1; // 2**W will kill performance. + let w = 0; + for (const num of [s0, s1, s2, s3]) { for (let bytePos = 0; bytePos < 4; bytePos++) { const byte = (num >>> (8 * bytePos)) & 0xff; - for (let bitPos = 7; bitPos >= 0; bitPos--) { - const bit = (byte >>> bitPos) & 1; - const mask = ~(bit - 1); - // const-time addition regardless of bit value - o0 ^= mulTable[pos++] & mask; - o1 ^= mulTable[pos++] & mask; - o2 ^= mulTable[pos++] & mask; - o3 ^= mulTable[pos++] & mask; + for (let bitPos = 8 / W - 1; bitPos >= 0; bitPos--) { + const bit = (byte >>> (W * bitPos)) & mask; + const { s0: e0, s1: e1, s2: e2, s3: e3 } = t[w * windowSize + bit]; + (o0 ^= e0), (o1 ^= e1), (o2 ^= e2), (o3 ^= e3); + w += 1; } } - return { o0, o1, o2, o3 }; - }; - // prettier-ignore - let o0 = 0, o1 = 0, o2 = 0, o3 = 0; - ({ o0, o1, o2, o3 } = mulNum(s0, 0, o0, o1, o2, o3)); - ({ o0, o1, o2, o3 } = mulNum(s1, 128, o0, o1, o2, o3)); - ({ o0, o1, o2, o3 } = mulNum(s2, 256, o0, o1, o2, o3)); - ({ o0: s0, o1: s1, o2: s2, o3: s3 } = mulNum(s3, 384, o0, o1, o2, o3)); - this.s0 = s0; - this.s1 = s1; - this.s2 = s2; - this.s3 = s3; + } + this.s0 = o0; + this.s1 = o1; + this.s2 = o2; + this.s3 = o3; } update(data: Input): this { data = toBytes(data); @@ -125,7 +152,13 @@ class GHASH implements Hash { } return this; } - destroy() {} + destroy() { + const { t } = this; + // clean precompute table + for (const elm of t) { + (elm.s0 = 0), (elm.s1 = 0), (elm.s2 = 0), (elm.s3 = 0); + } + } digestInto(out: Uint8Array) { aexists(this); aoutput(out, this); @@ -147,10 +180,10 @@ class GHASH implements Hash { } class Polyval extends GHASH { - constructor(key: Input) { + constructor(key: Input, expectedLength?: number) { key = toBytes(key); const ghKey = _toGHASHKey(key.slice()); - super(ghKey); + super(ghKey, expectedLength); ghKey.fill(0); } update(data: Input): this { @@ -195,14 +228,21 @@ class Polyval extends GHASH { } export type CHash = ReturnType; -export function wrapConstructorWithKey>(hashCons: (key: Input) => Hash) { - const hashC = (msg: Input, key: Input): Uint8Array => hashCons(key).update(toBytes(msg)).digest(); - const tmp = hashCons(new Uint8Array(32)); +function wrapConstructorWithKey>( + hashCons: (key: Input, expectedLength?: number) => Hash +) { + const hashC = (msg: Input, key: Input): Uint8Array => + hashCons(key, msg.length).update(toBytes(msg)).digest(); + const tmp = hashCons(new Uint8Array(16), 0); hashC.outputLen = tmp.outputLen; hashC.blockLen = tmp.blockLen; - hashC.create = (key: Input) => hashCons(key); + hashC.create = (key: Input, expectedLength?: number) => hashCons(key, expectedLength); return hashC; } -export const ghash = wrapConstructorWithKey((key) => new GHASH(key)); -export const polyval = wrapConstructorWithKey((key) => new Polyval(key)); +export const ghash = wrapConstructorWithKey( + (key, expectedLength) => new GHASH(key, expectedLength) +); +export const polyval = wrapConstructorWithKey( + (key, expectedLength) => new Polyval(key, expectedLength) +); diff --git a/src/aes.ts b/src/aes.ts index a3ec5ce..de38d32 100644 --- a/src/aes.ts +++ b/src/aes.ts @@ -1,4 +1,4 @@ -import { equalBytes, u32, u8, ensureBytes, Cipher, CipherWithOutput } from './utils.js'; +import { wrapCipher, Cipher, CipherWithOutput, equalBytes, u32, u8, ensureBytes } from './utils.js'; import { createView, setBigUint64 } from './utils.js'; import { ghash, polyval } from './_polyval.js'; @@ -94,7 +94,7 @@ const POWX = /* @__PURE__ */ (() => { return p; })(); -function expandKeyLE(key: Uint8Array): Uint32Array { +export function expandKeyLE(key: Uint8Array): Uint32Array { ensureBytes(key); const len = key.length; if (![16, 24, 32].includes(len)) @@ -115,7 +115,7 @@ function expandKeyLE(key: Uint8Array): Uint32Array { return xk; } -function expandKeyDecLE(key: Uint8Array): Uint32Array { +export function expandKeyDecLE(key: Uint8Array): Uint32Array { const encKey = expandKeyLE(key); const xk = encKey.slice(); const Nk = encKey.length; @@ -290,20 +290,23 @@ function ctr32( * CTR: counter mode. Creates stream cipher. * Requires good IV. Parallelizable. OK, but no MAC. */ -export function ctr(key: Uint8Array, nonce: Uint8Array): CipherWithOutput { - ensureBytes(key); - ensureBytes(nonce, BLOCK_SIZE); - function processCtr(buf: Uint8Array, dst?: Uint8Array) { - const xk = expandKeyLE(key); - const out = ctrCounter(xk, nonce, buf, dst); - xk.fill(0); - return out; +export const ctr = wrapCipher( + { blockSize: 16, nonceLength: 16 }, + function ctr(key: Uint8Array, nonce: Uint8Array): CipherWithOutput { + ensureBytes(key); + ensureBytes(nonce, BLOCK_SIZE); + function processCtr(buf: Uint8Array, dst?: Uint8Array) { + const xk = expandKeyLE(key); + const out = ctrCounter(xk, nonce, buf, dst); + xk.fill(0); + return out; + } + return { + encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => processCtr(plaintext, dst), + decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => processCtr(ciphertext, dst), + }; } - return { - encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => processCtr(plaintext, dst), - decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => processCtr(ciphertext, dst), - }; -} +); function validateBlockDecrypt(data: Uint8Array) { ensureBytes(data); @@ -357,94 +360,100 @@ export type BlockOpts = { disablePadding?: boolean }; * ECB: Electronic CodeBook. Simple deterministic replacement. * Dangerous: always map x to y. See [AES Penguin](https://words.filippo.io/the-ecb-penguin/). */ -export function ecb(key: Uint8Array, opts: BlockOpts = {}): Cipher { - ensureBytes(key); - const pcks5 = !opts.disablePadding; - return { - encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => { - ensureBytes(plaintext); - const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); - const xk = expandKeyLE(key); - let i = 0; - for (; i + 4 <= b.length; ) { - const { s0, s1, s2, s3 } = encrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]); - (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); - } - if (pcks5) { - const tmp32 = padPCKS(plaintext.subarray(i * 4)); - const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]); - (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); - } - xk.fill(0); - return _out; - }, - decrypt: (ciphertext: Uint8Array) => { - validateBlockDecrypt(ciphertext); - const xk = expandKeyDecLE(key); - const out = ciphertext.slice(); - const b = u32(ciphertext); - const o = u32(out); - for (let i = 0; i + 4 <= b.length; ) { - const { s0, s1, s2, s3 } = decrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]); - (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); - } - xk.fill(0); - return validatePCKS(out, pcks5); - }, - }; -} +export const ecb = wrapCipher( + { blockSize: 16 }, + function ecb(key: Uint8Array, opts: BlockOpts = {}): CipherWithOutput { + ensureBytes(key); + const pcks5 = !opts.disablePadding; + return { + encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => { + ensureBytes(plaintext); + const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); + const xk = expandKeyLE(key); + let i = 0; + for (; i + 4 <= b.length; ) { + const { s0, s1, s2, s3 } = encrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]); + (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); + } + if (pcks5) { + const tmp32 = padPCKS(plaintext.subarray(i * 4)); + const { s0, s1, s2, s3 } = encrypt(xk, tmp32[0], tmp32[1], tmp32[2], tmp32[3]); + (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); + } + xk.fill(0); + return _out; + }, + decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => { + validateBlockDecrypt(ciphertext); + const xk = expandKeyDecLE(key); + const out = getDst(ciphertext.length, dst); + const b = u32(ciphertext); + const o = u32(out); + for (let i = 0; i + 4 <= b.length; ) { + const { s0, s1, s2, s3 } = decrypt(xk, b[i + 0], b[i + 1], b[i + 2], b[i + 3]); + (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); + } + xk.fill(0); + return validatePCKS(out, pcks5); + }, + }; + } +); /** * CBC: Cipher-Block-Chaining. Key is previous round’s block. * Fragile: needs proper padding. Unauthenticated: needs MAC. */ -export function cbc(key: Uint8Array, iv: Uint8Array, opts: BlockOpts = {}): Cipher { - ensureBytes(key); - ensureBytes(iv, 16); - const pcks5 = !opts.disablePadding; - return { - encrypt: (plaintext: Uint8Array) => { - const xk = expandKeyLE(key); - const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5); - const n32 = u32(iv); - // prettier-ignore - let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; - let i = 0; - for (; i + 4 <= b.length; ) { - (s0 ^= b[i + 0]), (s1 ^= b[i + 1]), (s2 ^= b[i + 2]), (s3 ^= b[i + 3]); - ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); - (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); - } - if (pcks5) { - const tmp32 = padPCKS(plaintext.subarray(i * 4)); - (s0 ^= tmp32[0]), (s1 ^= tmp32[1]), (s2 ^= tmp32[2]), (s3 ^= tmp32[3]); - ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); - (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); - } - xk.fill(0); - return _out; - }, - decrypt: (ciphertext: Uint8Array) => { - validateBlockDecrypt(ciphertext); - const xk = expandKeyDecLE(key); - const n32 = u32(iv); - const out = ciphertext.slice(); - const b = u32(ciphertext); - const o = u32(out); - // prettier-ignore - let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; - for (let i = 0; i + 4 <= b.length; ) { +export const cbc = wrapCipher( + { blockSize: 16, nonceLength: 16 }, + function cbc(key: Uint8Array, iv: Uint8Array, opts: BlockOpts = {}): CipherWithOutput { + ensureBytes(key); + ensureBytes(iv, 16); + const pcks5 = !opts.disablePadding; + return { + encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => { + const xk = expandKeyLE(key); + const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); + const n32 = u32(iv); // prettier-ignore - const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3; - (s0 = b[i + 0]), (s1 = b[i + 1]), (s2 = b[i + 2]), (s3 = b[i + 3]); - const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3); - (o[i++] = o0 ^ ps0), (o[i++] = o1 ^ ps1), (o[i++] = o2 ^ ps2), (o[i++] = o3 ^ ps3); - } - xk.fill(0); - return validatePCKS(out, pcks5); - }, - }; -} + let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; + let i = 0; + for (; i + 4 <= b.length; ) { + (s0 ^= b[i + 0]), (s1 ^= b[i + 1]), (s2 ^= b[i + 2]), (s3 ^= b[i + 3]); + ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); + (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); + } + if (pcks5) { + const tmp32 = padPCKS(plaintext.subarray(i * 4)); + (s0 ^= tmp32[0]), (s1 ^= tmp32[1]), (s2 ^= tmp32[2]), (s3 ^= tmp32[3]); + ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); + (o[i++] = s0), (o[i++] = s1), (o[i++] = s2), (o[i++] = s3); + } + xk.fill(0); + return _out; + }, + decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => { + validateBlockDecrypt(ciphertext); + const xk = expandKeyDecLE(key); + const n32 = u32(iv); + const out = getDst(ciphertext.length, dst); + const b = u32(ciphertext); + const o = u32(out); + // prettier-ignore + let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; + for (let i = 0; i + 4 <= b.length; ) { + // prettier-ignore + const ps0 = s0, ps1 = s1, ps2 = s2, ps3 = s3; + (s0 = b[i + 0]), (s1 = b[i + 1]), (s2 = b[i + 2]), (s3 = b[i + 3]); + const { s0: o0, s1: o1, s2: o2, s3: o3 } = decrypt(xk, s0, s1, s2, s3); + (o[i++] = o0 ^ ps0), (o[i++] = o1 ^ ps1), (o[i++] = o2 ^ ps2), (o[i++] = o3 ^ ps3); + } + xk.fill(0); + return validatePCKS(out, pcks5); + }, + }; + } +); // TODO: merge with chacha, however gcm has bitLen while chacha has byteLen function computeTag( @@ -454,7 +463,7 @@ function computeTag( data: Uint8Array, AAD?: Uint8Array ) { - const h = fn.create(key); + const h = fn.create(key, data.length + (AAD?.length || 0)); if (AAD) h.update(AAD); h.update(data); const num = new Uint8Array(16); @@ -470,64 +479,66 @@ function computeTag( * Good, modern version of CTR, parallel, with MAC. * Be careful: MACs can be forged. */ -export function gcm(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher { - ensureBytes(nonce); - // Nonce can be pretty much anything (even 1 byte). But smaller nonces less secure. - if (nonce.length === 0) throw new Error('aes/gcm: empty nonce'); - const tagLength = 16; - function _computeTag(authKey: Uint8Array, tagMask: Uint8Array, data: Uint8Array) { - const tag = computeTag(ghash, false, authKey, data, AAD); - for (let i = 0; i < tagMask.length; i++) tag[i] ^= tagMask[i]; - return tag; - } - function deriveKeys() { - const xk = expandKeyLE(key); - const authKey = EMPTY_BLOCK.slice(); - const counter = EMPTY_BLOCK.slice(); - ctr32(xk, false, counter, counter, authKey); - if (nonce.length === 12) { - counter.set(nonce); - } else { - // Spec (NIST 800-38d) supports variable size nonce. - // Not supported for now, but can be useful. - const nonceLen = EMPTY_BLOCK.slice(); - const view = createView(nonceLen); - setBigUint64(view, 8, BigInt(nonce.length * 8), false); - // ghash(nonce || u64be(0) || u64be(nonceLen*8)) - ghash.create(authKey).update(nonce).update(nonceLen).digestInto(counter); +export const gcm = wrapCipher( + { blockSize: 16, nonceLength: 12, tagLength: 16 }, + function gcm(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher { + ensureBytes(nonce); + // Nonce can be pretty much anything (even 1 byte). But smaller nonces less secure. + if (nonce.length === 0) throw new Error('aes/gcm: empty nonce'); + const tagLength = 16; + function _computeTag(authKey: Uint8Array, tagMask: Uint8Array, data: Uint8Array) { + const tag = computeTag(ghash, false, authKey, data, AAD); + for (let i = 0; i < tagMask.length; i++) tag[i] ^= tagMask[i]; + return tag; } - const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK); - return { xk, authKey, counter, tagMask }; + function deriveKeys() { + const xk = expandKeyLE(key); + const authKey = EMPTY_BLOCK.slice(); + const counter = EMPTY_BLOCK.slice(); + ctr32(xk, false, counter, counter, authKey); + if (nonce.length === 12) { + counter.set(nonce); + } else { + // Spec (NIST 800-38d) supports variable size nonce. + // Not supported for now, but can be useful. + const nonceLen = EMPTY_BLOCK.slice(); + const view = createView(nonceLen); + setBigUint64(view, 8, BigInt(nonce.length * 8), false); + // ghash(nonce || u64be(0) || u64be(nonceLen*8)) + ghash.create(authKey).update(nonce).update(nonceLen).digestInto(counter); + } + const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK); + return { xk, authKey, counter, tagMask }; + } + return { + encrypt: (plaintext: Uint8Array) => { + ensureBytes(plaintext); + const { xk, authKey, counter, tagMask } = deriveKeys(); + const out = new Uint8Array(plaintext.length + tagLength); + ctr32(xk, false, counter, plaintext, out); + const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength)); + out.set(tag, plaintext.length); + xk.fill(0); + return out; + }, + decrypt: (ciphertext: Uint8Array) => { + ensureBytes(ciphertext); + if (ciphertext.length < tagLength) + throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`); + const { xk, authKey, counter, tagMask } = deriveKeys(); + const data = ciphertext.subarray(0, -tagLength); + const passedTag = ciphertext.subarray(-tagLength); + const tag = _computeTag(authKey, tagMask, data); + if (!equalBytes(tag, passedTag)) throw new Error('aes/gcm: invalid ghash tag'); + const out = ctr32(xk, false, counter, data); + authKey.fill(0); + tagMask.fill(0); + xk.fill(0); + return out; + }, + }; } - return { - tagLength, - encrypt: (plaintext: Uint8Array) => { - ensureBytes(plaintext); - const { xk, authKey, counter, tagMask } = deriveKeys(); - const out = new Uint8Array(plaintext.length + tagLength); - ctr32(xk, false, counter, plaintext, out); - const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength)); - out.set(tag, plaintext.length); - xk.fill(0); - return out; - }, - decrypt: (ciphertext: Uint8Array) => { - ensureBytes(ciphertext); - if (ciphertext.length < tagLength) - throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`); - const { xk, authKey, counter, tagMask } = deriveKeys(); - const data = ciphertext.subarray(0, -tagLength); - const passedTag = ciphertext.subarray(-tagLength); - const tag = _computeTag(authKey, tagMask, data); - if (!equalBytes(tag, passedTag)) throw new Error('aes/gcm: invalid ghash tag'); - const out = ctr32(xk, false, counter, data); - authKey.fill(0); - tagMask.fill(0); - xk.fill(0); - return out; - }, - }; -} +); const limit = (name: string, min: number, max: number) => (value: number) => { if (!Number.isSafeInteger(value) || min > value || value > max) @@ -540,89 +551,113 @@ const limit = (name: string, min: number, max: number) => (value: number) => { * plaintexts will produce identical ciphertexts. * RFC 8452, https://datatracker.ietf.org/doc/html/rfc8452 */ -export function siv(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher { - const tagLength = 16; - // From RFC 8452: Section 6 - const AAD_LIMIT = limit('AAD', 0, 2 ** 36); - const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36); - const NONCE_LIMIT = limit('nonce', 12, 12); - const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16); - ensureBytes(nonce); - NONCE_LIMIT(nonce.length); - if (AAD) { - ensureBytes(AAD); - AAD_LIMIT(AAD.length); - } - function deriveKeys() { - const len = key.length; - if (len !== 16 && len !== 32) - throw new Error(`key length must be 16 or 32 bytes, got: ${len} bytes`); - const xk = expandKeyLE(key); - const encKey = new Uint8Array(len); - const authKey = new Uint8Array(16); - const n32 = u32(nonce); - // prettier-ignore - let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2]; - let counter = 0; - for (const derivedKey of [authKey, encKey].map(u32)) { - const d32 = u32(derivedKey); - for (let i = 0; i < d32.length; i += 2) { - // aes(u32le(0) || nonce)[:8] || aes(u32le(1) || nonce)[:8] ... - const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3); - d32[i + 0] = o0; - d32[i + 1] = o1; - s0 = ++counter; // increment counter inside state +export const siv = wrapCipher( + { blockSize: 16, nonceLength: 12, tagLength: 16 }, + function siv(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher { + const tagLength = 16; + // From RFC 8452: Section 6 + const AAD_LIMIT = limit('AAD', 0, 2 ** 36); + const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36); + const NONCE_LIMIT = limit('nonce', 12, 12); + const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16); + ensureBytes(nonce); + NONCE_LIMIT(nonce.length); + if (AAD) { + ensureBytes(AAD); + AAD_LIMIT(AAD.length); + } + function deriveKeys() { + const len = key.length; + if (len !== 16 && len !== 24 && len !== 32) + throw new Error(`key length must be 16, 24 or 32 bytes, got: ${len} bytes`); + const xk = expandKeyLE(key); + const encKey = new Uint8Array(len); + const authKey = new Uint8Array(16); + const n32 = u32(nonce); + // prettier-ignore + let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2]; + let counter = 0; + for (const derivedKey of [authKey, encKey].map(u32)) { + const d32 = u32(derivedKey); + for (let i = 0; i < d32.length; i += 2) { + // aes(u32le(0) || nonce)[:8] || aes(u32le(1) || nonce)[:8] ... + const { s0: o0, s1: o1 } = encrypt(xk, s0, s1, s2, s3); + d32[i + 0] = o0; + d32[i + 1] = o1; + s0 = ++counter; // increment counter inside state + } } + xk.fill(0); + return { authKey, encKey: expandKeyLE(encKey) }; } - xk.fill(0); - return { authKey, encKey: expandKeyLE(encKey) }; - } - function _computeTag(encKey: Uint32Array, authKey: Uint8Array, data: Uint8Array) { - const tag = computeTag(polyval, true, authKey, data, AAD); - // Compute the expected tag by XORing S_s and the nonce, clearing the - // most significant bit of the last byte and encrypting with the - // message-encryption key. - for (let i = 0; i < 12; i++) tag[i] ^= nonce[i]; - tag[15] &= 0x7f; // Clear the highest bit - // encrypt tag as block - const t32 = u32(tag); - // prettier-ignore - let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3]; - ({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3)); - (t32[0] = s0), (t32[1] = s1), (t32[2] = s2), (t32[3] = s3); - return tag; - } - // actual decrypt/encrypt of message. - function processSiv(encKey: Uint32Array, tag: Uint8Array, input: Uint8Array) { - let block = tag.slice(); - block[15] |= 0x80; // Force highest bit - return ctr32(encKey, true, block, input); + function _computeTag(encKey: Uint32Array, authKey: Uint8Array, data: Uint8Array) { + const tag = computeTag(polyval, true, authKey, data, AAD); + // Compute the expected tag by XORing S_s and the nonce, clearing the + // most significant bit of the last byte and encrypting with the + // message-encryption key. + for (let i = 0; i < 12; i++) tag[i] ^= nonce[i]; + tag[15] &= 0x7f; // Clear the highest bit + // encrypt tag as block + const t32 = u32(tag); + // prettier-ignore + let s0 = t32[0], s1 = t32[1], s2 = t32[2], s3 = t32[3]; + ({ s0, s1, s2, s3 } = encrypt(encKey, s0, s1, s2, s3)); + (t32[0] = s0), (t32[1] = s1), (t32[2] = s2), (t32[3] = s3); + return tag; + } + // actual decrypt/encrypt of message. + function processSiv(encKey: Uint32Array, tag: Uint8Array, input: Uint8Array) { + let block = tag.slice(); + block[15] |= 0x80; // Force highest bit + return ctr32(encKey, true, block, input); + } + return { + encrypt: (plaintext: Uint8Array) => { + ensureBytes(plaintext); + PLAIN_LIMIT(plaintext.length); + const { encKey, authKey } = deriveKeys(); + const tag = _computeTag(encKey, authKey, plaintext); + const out = new Uint8Array(plaintext.length + tagLength); + out.set(tag, plaintext.length); + out.set(processSiv(encKey, tag, plaintext)); + encKey.fill(0); + authKey.fill(0); + return out; + }, + decrypt: (ciphertext: Uint8Array) => { + ensureBytes(ciphertext); + CIPHER_LIMIT(ciphertext.length); + const tag = ciphertext.subarray(-tagLength); + const { encKey, authKey } = deriveKeys(); + const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength)); + const expectedTag = _computeTag(encKey, authKey, plaintext); + encKey.fill(0); + authKey.fill(0); + if (!equalBytes(tag, expectedTag)) throw new Error('invalid polyval tag'); + return plaintext; + }, + }; } - return { - tagLength, - encrypt: (plaintext: Uint8Array) => { - ensureBytes(plaintext); - PLAIN_LIMIT(plaintext.length); - const { encKey, authKey } = deriveKeys(); - const tag = _computeTag(encKey, authKey, plaintext); - const out = new Uint8Array(plaintext.length + tagLength); - out.set(tag, plaintext.length); - out.set(processSiv(encKey, tag, plaintext)); - encKey.fill(0); - authKey.fill(0); - return out; - }, - decrypt: (ciphertext: Uint8Array) => { - ensureBytes(ciphertext); - CIPHER_LIMIT(ciphertext.length); - const tag = ciphertext.subarray(-tagLength); - const { encKey, authKey } = deriveKeys(); - const plaintext = processSiv(encKey, tag, ciphertext.subarray(0, -tagLength)); - const expectedTag = _computeTag(encKey, authKey, plaintext); - encKey.fill(0); - authKey.fill(0); - if (!equalBytes(tag, expectedTag)) throw new Error('invalid polyval tag'); - return plaintext; - }, - }; +); + +function encryptBlock(xk: Uint32Array, block: Uint8Array) { + ensureBytes(block, 16); + if (!(xk instanceof Uint32Array)) throw new Error('_encryptBlock accepts result of expandKeyLE'); + const b32 = u32(block); + let { s0, s1, s2, s3 } = encrypt(xk, b32[0], b32[1], b32[2], b32[3]); + (b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3); + return block; } + +function decryptBlock(xk: Uint32Array, block: Uint8Array) { + ensureBytes(block, 16); + if (!(xk instanceof Uint32Array)) throw new Error('_decryptBlock accepts result of expandKeyLE'); + const b32 = u32(block); + let { s0, s1, s2, s3 } = decrypt(xk, b32[0], b32[1], b32[2], b32[3]); + (b32[0] = s0), (b32[1] = s1), (b32[2] = s2), (b32[3] = s3); + return block; +} + +// Highly unsafe private functions for implementing new modes or ciphers based on AES +// Can change at any time, no API guarantees +export const unsafe = { expandKeyLE, expandKeyDecLE, encrypt, decrypt, encryptBlock, decryptBlock }; diff --git a/src/chacha.ts b/src/chacha.ts index 8ba6ae2..5de02ba 100644 --- a/src/chacha.ts +++ b/src/chacha.ts @@ -1,4 +1,5 @@ import { + wrapCipher, CipherWithOutput, XorStream, createView, @@ -248,7 +249,6 @@ export const _poly1305_aead = ensureBytes(key, 32); ensureBytes(nonce); return { - tagLength, encrypt: (plaintext: Uint8Array, output?: Uint8Array) => { const plength = plaintext.length; const clength = plength + tagLength; @@ -286,10 +286,16 @@ export const _poly1305_aead = * ChaCha20-Poly1305 from RFC 8439. * With 12-byte nonce, it's not safe to use fill it with random (CSPRNG), due to collision chance. */ -export const chacha20poly1305 = /* @__PURE__ */ _poly1305_aead(chacha20); +export const chacha20poly1305 = /* @__PURE__ */ wrapCipher( + { blockSize: 64, nonceLength: 12, tagLength: 16 }, + _poly1305_aead(chacha20) +); /** * XChaCha20-Poly1305 extended-nonce chacha. * https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha * With 24-byte nonce, it's safe to use fill it with random (CSPRNG). */ -export const xchacha20poly1305 = /* @__PURE__ */ _poly1305_aead(xchacha20); +export const xchacha20poly1305 = /* @__PURE__ */ wrapCipher( + { blockSize: 64, nonceLength: 24, tagLength: 16 }, + _poly1305_aead(xchacha20) +); diff --git a/src/webcrypto/ff1.ts b/src/ff1.ts similarity index 81% rename from src/webcrypto/ff1.ts rename to src/ff1.ts index a13145b..e502d8a 100644 --- a/src/webcrypto/ff1.ts +++ b/src/ff1.ts @@ -1,5 +1,7 @@ -import { AsyncCipher, bytesToNumberBE, numberToBytesBE } from '../utils.js'; -import { cryptoSubtleUtils } from './utils.js'; +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 +const { expandKeyLE, encryptBlock } = unsafe; // Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G. // https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf @@ -20,7 +22,7 @@ function NUMradix(radix: number, data: number[]): bigint { return res; } -async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: number[]) { +function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: number[]) { if (radix > 2 ** 16 - 1) throw new Error(`Invalid radix: ${radix}`); // radix**minlen ≥ 100 const minLen = Math.ceil(Math.log(100) / Math.log(radix)); @@ -45,7 +47,8 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu PQ.set(P); P.fill(0); PQ.set(tweak, P.length); - const round = async (A: number[], B: number[], i: number, decrypt = false) => { + const xk = expandKeyLE(key); + const round = (A: number[], B: number[], i: number, decrypt = false) => { // Q = ... || [i]1 || [NUMradix(B)]b. PQ[PQ.length - b - 1] = i; if (b) PQ.set(numberToBytesBE(NUMradix(radix, B), b), PQ.length - b); @@ -53,7 +56,7 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu let r = new Uint8Array(16); for (let j = 0; j < PQ.length / BLOCK_LEN; j++) { for (let i = 0; i < BLOCK_LEN; i++) r[i] ^= PQ[j * BLOCK_LEN + i]; - r.set(await cryptoSubtleUtils.aesEncryptBlock(r, key)); + encryptBlock(xk, r); } // Let S be the first d bytes of the following string of ⎡d/16⎤ blocks: // R || CIPHK(R ⊕[1]16) || CIPHK(R ⊕[2]16) ...CIPHK(R ⊕[⎡d / 16⎤ – 1]16). @@ -61,7 +64,7 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu for (let j = 1; s.length < d; j++) { const block = numberToBytesBE(BigInt(j), 16); for (let k = 0; k < BLOCK_LEN; k++) block[k] ^= r[k]; - s.push(...Array.from(await cryptoSubtleUtils.aesEncryptBlock(block, key))); + s.push(...Array.from(encryptBlock(xk, block))); } let y = bytesToNumberBE(Uint8Array.from(s.slice(0, d))); s.fill(0); @@ -76,7 +79,10 @@ async function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: nu B = C; return [A, B]; }; - const destroy = () => PQ.fill(0); + const destroy = () => { + xk.fill(0); + PQ.fill(0); + }; return { u, round, destroy }; } @@ -85,25 +91,25 @@ const EMPTY_BUF = new Uint8Array([]); export function FF1(radix: number, key: Uint8Array, tweak: Uint8Array = EMPTY_BUF) { const PQ = getRound.bind(null, radix, key, tweak); return { - async encrypt(x: number[]) { - const { u, round, destroy } = await PQ(x); + encrypt(x: number[]) { + const { u, round, destroy } = PQ(x); let [A, B] = [x.slice(0, u), x.slice(u)]; - for (let i = 0; i < 10; i++) [A, B] = await round(A, B, i); + for (let i = 0; i < 10; i++) [A, B] = round(A, B, i); destroy(); const res = A.concat(B); A.fill(0); B.fill(0); return res; }, - async decrypt(x: number[]) { - const { u, round, destroy } = await PQ(x); + decrypt(x: number[]) { + const { u, round, destroy } = PQ(x); // The FF1.Decrypt algorithm is similar to the FF1.Encrypt algorithm; // the differences are in Step 6, where: // 1) the order of the indices is reversed, // 2) the roles of A and B are swapped // 3) modular addition is replaced by modular subtraction, in Step 6vi. let [B, A] = [x.slice(0, u), x.slice(u)]; - for (let i = 9; i >= 0; i--) [A, B] = await round(A, B, i, true); + for (let i = 9; i >= 0; i--) [A, B] = round(A, B, i, true); destroy(); const res = B.concat(A); A.fill(0); @@ -132,10 +138,10 @@ const binLE = { }, }; -export function BinaryFF1(key: Uint8Array, tweak: Uint8Array = EMPTY_BUF): AsyncCipher { +export function BinaryFF1(key: Uint8Array, tweak: Uint8Array = EMPTY_BUF): Cipher { const ff1 = FF1(2, key, tweak); return { - encrypt: async (x: Uint8Array) => binLE.decode(await ff1.encrypt(binLE.encode(x))), - decrypt: async (x: Uint8Array) => binLE.decode(await ff1.decrypt(binLE.encode(x))), + encrypt: (x: Uint8Array) => binLE.decode(ff1.encrypt(binLE.encode(x))), + decrypt: (x: Uint8Array) => binLE.decode(ff1.decrypt(binLE.encode(x))), }; } diff --git a/src/salsa.ts b/src/salsa.ts index 2d3790c..79cae3a 100644 --- a/src/salsa.ts +++ b/src/salsa.ts @@ -1,4 +1,4 @@ -import { Cipher, ensureBytes, equalBytes, u32 } from './utils.js'; +import { wrapCipher, Cipher, ensureBytes, equalBytes, u32 } from './utils.js'; import { poly1305 } from './_poly1305.js'; import { createCipher } from './_arx.js'; @@ -128,55 +128,57 @@ export const xsalsa20 = /* @__PURE__ */ createCipher(salsaCore, { * With 24-byte nonce, it's safe to use fill it with random (CSPRNG). * Also known as secretbox from libsodium / nacl. */ -export const xsalsa20poly1305 = (key: Uint8Array, nonce: Uint8Array): Cipher => { - const tagLength = 16; - ensureBytes(key, 32); - ensureBytes(nonce, 24); - return { - tagLength, - encrypt: (plaintext: Uint8Array, output?: Uint8Array) => { - ensureBytes(plaintext); - // This is small optimization (calculate auth key with same call as encryption itself) makes it hard - // to separate tag calculation and encryption itself, since 32 byte is half-block of salsa (64 byte) - const clength = plaintext.length + 32; - if (output) { - ensureBytes(output, clength); - } else { - output = new Uint8Array(clength); - } - output.set(plaintext, 32); - xsalsa20(key, nonce, output, output); - const authKey = output.subarray(0, 32); - const tag = poly1305(output.subarray(32), authKey); - // Clean auth key, even though JS provides no guarantees about memory cleaning - output.set(tag, tagLength); - output.subarray(0, tagLength).fill(0); - return output.subarray(tagLength); - }, - decrypt: (ciphertext: Uint8Array) => { - ensureBytes(ciphertext); - const clength = ciphertext.length; - if (clength < tagLength) throw new Error('encrypted data should be at least 16 bytes'); - // Create new ciphertext array: - // auth tag auth tag from ciphertext ciphertext - // [bytes 0..16] [bytes 16..32] [bytes 32..] - // 16 instead of 32, because we already have 16 byte tag - const ciphertext_ = new Uint8Array(clength + tagLength); // alloc - ciphertext_.set(ciphertext, tagLength); - // Each xsalsa20 calls to hsalsa to calculate key, but seems not much perf difference - // Separate call to calculate authkey, since first bytes contains tag - const authKey = xsalsa20(key, nonce, new Uint8Array(32)); // alloc(32) - const tag = poly1305(ciphertext_.subarray(32), authKey); - if (!equalBytes(ciphertext_.subarray(16, 32), tag)) throw new Error('invalid tag'); +export const xsalsa20poly1305 = wrapCipher( + { blockSize: 64, nonceLength: 24, tagLength: 16 }, + (key: Uint8Array, nonce: Uint8Array): Cipher => { + const tagLength = 16; + ensureBytes(key, 32); + ensureBytes(nonce, 24); + return { + encrypt: (plaintext: Uint8Array, output?: Uint8Array) => { + ensureBytes(plaintext); + // This is small optimization (calculate auth key with same call as encryption itself) makes it hard + // to separate tag calculation and encryption itself, since 32 byte is half-block of salsa (64 byte) + const clength = plaintext.length + 32; + if (output) { + ensureBytes(output, clength); + } else { + output = new Uint8Array(clength); + } + output.set(plaintext, 32); + xsalsa20(key, nonce, output, output); + const authKey = output.subarray(0, 32); + const tag = poly1305(output.subarray(32), authKey); + // Clean auth key, even though JS provides no guarantees about memory cleaning + output.set(tag, tagLength); + output.subarray(0, tagLength).fill(0); + return output.subarray(tagLength); + }, + decrypt: (ciphertext: Uint8Array) => { + ensureBytes(ciphertext); + const clength = ciphertext.length; + if (clength < tagLength) throw new Error('encrypted data should be at least 16 bytes'); + // Create new ciphertext array: + // auth tag auth tag from ciphertext ciphertext + // [bytes 0..16] [bytes 16..32] [bytes 32..] + // 16 instead of 32, because we already have 16 byte tag + const ciphertext_ = new Uint8Array(clength + tagLength); // alloc + ciphertext_.set(ciphertext, tagLength); + // Each xsalsa20 calls to hsalsa to calculate key, but seems not much perf difference + // Separate call to calculate authkey, since first bytes contains tag + const authKey = xsalsa20(key, nonce, new Uint8Array(32)); // alloc(32) + const tag = poly1305(ciphertext_.subarray(32), authKey); + if (!equalBytes(ciphertext_.subarray(16, 32), tag)) throw new Error('invalid tag'); - const plaintext = xsalsa20(key, nonce, ciphertext_); // alloc - // Clean auth key, even though JS provides no guarantees about memory cleaning - plaintext.subarray(0, 32).fill(0); - authKey.fill(0); - return plaintext.subarray(32); - }, - }; -}; + const plaintext = xsalsa20(key, nonce, ciphertext_); // alloc + // Clean auth key, even though JS provides no guarantees about memory cleaning + plaintext.subarray(0, 32).fill(0); + authKey.fill(0); + return plaintext.subarray(32); + }, + }; + } +); /** * Alias to xsalsa20poly1305, for compatibility with libsodium / nacl diff --git a/src/utils.ts b/src/utils.ts index 9ba1b9c..911b7d4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -196,14 +196,11 @@ export abstract class Hash> { // This will allow to re-use with composable things like packed & base encoders // Also, we probably can make tags composable export type Cipher = { - tagLength?: number; - nonceLength?: number; encrypt(plaintext: Uint8Array): Uint8Array; decrypt(ciphertext: Uint8Array): Uint8Array; }; export type AsyncCipher = { - tagLength?: number; encrypt(plaintext: Uint8Array): Promise; decrypt(ciphertext: Uint8Array): Promise; }; @@ -213,6 +210,18 @@ export type CipherWithOutput = Cipher & { decrypt(ciphertext: Uint8Array, output?: Uint8Array): Uint8Array; }; +// Params is outside return type, so it is accessible before calling constructor +// 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; +export const wrapCipher = , P extends CipherParams>( + params: P, + c: C +): C & P => { + Object.assign(c, params); + return c as C & P; +}; + export type XorStream = ( key: Uint8Array, nonce: Uint8Array, diff --git a/src/webcrypto/aes.ts b/src/webcrypto/aes.ts index 29274c3..79c193f 100644 --- a/src/webcrypto/aes.ts +++ b/src/webcrypto/aes.ts @@ -1,64 +1,60 @@ -import { ensureBytes } from '../utils.js'; -import { cryptoSubtleUtils } from './utils.js'; - -/** - * AAD is only effective on AES-256-GCM or AES-128-GCM. Otherwise it'll be ignored - */ -export type Cipher = ( - key: Uint8Array, - nonce: Uint8Array, - AAD?: Uint8Array -) => { - keyLength: number; - encrypt(plaintext: Uint8Array): Promise; - decrypt(ciphertext: Uint8Array): Promise; +import { getWebcryptoSubtle } from './utils.js'; +import { ensureBytes, AsyncCipher } from '../utils.js'; + +// Overridable +export const utils = { + async encrypt(key: Uint8Array, keyParams: any, cryptParams: any, plaintext: Uint8Array) { + const cr = getWebcryptoSubtle(); + const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']); + const ciphertext = await cr.encrypt(cryptParams, iKey, plaintext); + return new Uint8Array(ciphertext); + }, + async decrypt(key: Uint8Array, keyParams: any, cryptParams: any, ciphertext: Uint8Array) { + const cr = getWebcryptoSubtle(); + const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']); + const plaintext = await cr.decrypt(cryptParams, iKey, ciphertext); + return new Uint8Array(plaintext); + }, }; -enum AesBlockMode { +const enum BlockMode { CBC = 'AES-CBC', CTR = 'AES-CTR', GCM = 'AES-GCM', } -type BitLength = 128 | 256; - function getCryptParams( - algo: AesBlockMode, + algo: BlockMode, nonce: Uint8Array, AAD?: Uint8Array ): AesCbcParams | AesCtrParams | AesGcmParams { - if (algo === AesBlockMode.CBC) return { name: AesBlockMode.CBC, iv: nonce }; - if (algo === AesBlockMode.CTR) return { name: AesBlockMode.CTR, counter: nonce, length: 64 }; - if (algo === AesBlockMode.GCM) return { name: AesBlockMode.GCM, iv: nonce, additionalData: AAD }; - throw new Error('unknown aes cipher'); + if (algo === BlockMode.CBC) return { name: BlockMode.CBC, iv: nonce }; + if (algo === BlockMode.CTR) return { name: BlockMode.CTR, counter: nonce, length: 64 }; + if (algo === BlockMode.GCM) return { name: BlockMode.GCM, iv: nonce, additionalData: AAD }; + throw new Error('unknown aes block mode'); } -function generate(algo: AesBlockMode, length: BitLength): Cipher { - const keyLength = length / 8; - const keyParams = { name: algo, length }; - - return (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array) => { - ensureBytes(key, keyLength); +function generate(algo: BlockMode) { + return (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): AsyncCipher => { + ensureBytes(key); + ensureBytes(nonce); + // const keyLength = key.length; + const keyParams = { name: algo, length: key.length * 8 }; const cryptParams = getCryptParams(algo, nonce, AAD); return { - keyLength, + // keyLength, encrypt(plaintext: Uint8Array) { ensureBytes(plaintext); - return cryptoSubtleUtils.aesEncrypt(key, keyParams, cryptParams, plaintext); + return utils.encrypt(key, keyParams, cryptParams, plaintext); }, decrypt(ciphertext: Uint8Array) { ensureBytes(ciphertext); - return cryptoSubtleUtils.aesDecrypt(key, keyParams, cryptParams, ciphertext); + return utils.decrypt(key, keyParams, cryptParams, ciphertext); }, }; }; } -export const aes_128_ctr = generate(AesBlockMode.CTR, 128); -export const aes_256_ctr = generate(AesBlockMode.CTR, 256); - -export const aes_128_cbc = generate(AesBlockMode.CBC, 128); -export const aes_256_cbc = generate(AesBlockMode.CBC, 256); - -export const aes_128_gcm = generate(AesBlockMode.GCM, 128); -export const aes_256_gcm = generate(AesBlockMode.GCM, 256); +export const cbc = generate(BlockMode.CBC); +export const ctr = generate(BlockMode.CTR); +export const gcm = generate(BlockMode.GCM); diff --git a/src/webcrypto/utils.ts b/src/webcrypto/utils.ts index f742b70..0236c20 100644 --- a/src/webcrypto/utils.ts +++ b/src/webcrypto/utils.ts @@ -5,6 +5,8 @@ // Makes the utils un-importable in browsers without a bundler. // Once node.js 18 is deprecated, we can just drop the import. import { crypto } from './crypto.js'; +import { Cipher, concatBytes } from '../utils.js'; +import { number } from '../_assert.js'; /** * Secure PRNG. Uses `crypto.getRandomValues`, which defers to OS. @@ -16,32 +18,58 @@ export function randomBytes(bytesLength = 32): Uint8Array { throw new Error('crypto.getRandomValues must be defined'); } -function getWebcryptoSubtle() { +export function getWebcryptoSubtle() { if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) return crypto.subtle; throw new Error('crypto.subtle must be defined'); } -// Overridable -const BLOCK_LEN = 16; -const IV_BUF = new Uint8Array(BLOCK_LEN); -export const cryptoSubtleUtils = { - async aesEncrypt(key: Uint8Array, keyParams: any, cryptParams: any, plaintext: Uint8Array) { - const cr = getWebcryptoSubtle(); - const iKey = await cr.importKey('raw', key, keyParams, true, ['encrypt']); - const ciphertext = await cr.encrypt(cryptParams, iKey, plaintext); - return new Uint8Array(ciphertext); - }, - async aesDecrypt(key: Uint8Array, keyParams: any, cryptParams: any, ciphertext: Uint8Array) { - const cr = getWebcryptoSubtle(); - const iKey = await cr.importKey('raw', key, keyParams, true, ['decrypt']); - const plaintext = await cr.decrypt(cryptParams, iKey, ciphertext); - return new Uint8Array(plaintext); - }, - async aesEncryptBlock(msg: Uint8Array, key: Uint8Array): Promise { - if (key.length !== 16 && key.length !== 32) throw new Error('invalid key length'); - const keyParams = { name: 'AES-CBC', length: key.length * 8 }; - const cryptParams = { name: 'aes-cbc', iv: IV_BUF, counter: IV_BUF, length: 64 }; - const ciphertext = await cryptoSubtleUtils.aesEncrypt(key, keyParams, cryptParams, msg); - return ciphertext.subarray(0, 16); - }, +type RemoveNonceInner = ((...args: T) => Ret) extends ( + arg0: any, + arg1: any, + ...rest: infer R +) => any + ? (key: Uint8Array, ...args: R) => Ret + : never; + +type RemoveNonce any> = RemoveNonceInner, ReturnType>; +type CipherWithNonce = ((key: Uint8Array, nonce: Uint8Array, ...args: any[]) => Cipher) & { + nonceLength: number; }; + +// Uses CSPRG for nonce, nonce injected in ciphertext +export function managedNonce(fn: T): RemoveNonce { + number(fn.nonceLength); + return ((key: Uint8Array, ...args: any[]): any => ({ + encrypt: (plaintext: Uint8Array, ...argsEnc: any[]) => { + const { nonceLength } = fn; + const nonce = randomBytes(nonceLength); + const ciphertext = (fn(key, nonce, ...args).encrypt as any)(plaintext, ...argsEnc); + const out = concatBytes(nonce, ciphertext); + ciphertext.fill(0); + return out; + }, + decrypt: (ciphertext: Uint8Array, ...argsDec: any[]) => { + const { nonceLength } = fn; + const nonce = ciphertext.subarray(0, nonceLength); + const data = ciphertext.subarray(nonceLength); + return (fn(key, nonce, ...args).decrypt as any)(data, ...argsDec); + }, + })) as RemoveNonce; +} + +// // Type tests +// import { siv, gcm, ctr, ecb, cbc } from '../aes.js'; +// import { xsalsa20poly1305 } from '../salsa.js'; +// import { chacha20poly1305, xchacha20poly1305 } from '../chacha.js'; + +// const wsiv = managedNonce(siv); +// const wgcm = managedNonce(gcm); +// const wctr = managedNonce(ctr); +// const wcbc = managedNonce(cbc); +// const wsalsapoly = managedNonce(xsalsa20poly1305); +// const wchacha = managedNonce(chacha20poly1305); +// const wxchacha = managedNonce(xchacha20poly1305); + +// // should fail +// const wcbc2 = managedNonce(managedNonce(cbc)); +// const wecb = managedNonce(ecb); diff --git a/test/arx.test.js b/test/arx.test.js new file mode 100644 index 0000000..f1758bb --- /dev/null +++ b/test/arx.test.js @@ -0,0 +1,297 @@ +const { deepStrictEqual, throws } = require('assert'); +const { should, describe } = require('micro-should'); +const { hex, base64 } = require('@scure/base'); +const { salsa20, hsalsa, xsalsa20, xsalsa20poly1305 } = require('../salsa.js'); +const { + chacha20orig, + hchacha, + xchacha20, + chacha20poly1305, + xchacha20poly1305, +} = require('../chacha.js'); +const { poly1305 } = require('../_poly1305.js'); +const slow = require('../_micro.js'); +const tweetnacl_secretbox = require('./vectors/tweetnacl_secretbox.json'); +// Stablelib tests +const stable_salsa = require('./vectors/stablelib_salsa20.json'); +const stable_chacha = require('./vectors/stablelib_chacha20.json'); + +const stable_chacha_poly = require('./vectors/stablelib_chacha20poly1305.json'); +const stable_xchacha_poly = require('./vectors/stablelib_xchacha20poly1305.json'); +const stable_poly1305 = require('./vectors/stablelib_poly1305.json'); +// Wycheproof +const wycheproof_chacha20_poly1305 = require('./wycheproof/chacha20_poly1305_test.json'); +const wycheproof_xchacha20_poly1305 = require('./wycheproof/xchacha20_poly1305_test.json'); +// getKey for hsalsa/hchacha +const utils = require('../utils.js'); +const sigma16 = utils.utf8ToBytes('expand 16-byte k'); +const sigma32 = utils.utf8ToBytes('expand 32-byte k'); +const sigma16_32 = utils.u32(sigma16); +const sigma32_32 = utils.u32(sigma32); + +const getKey = (key) => { + if (key.length === 32) return { key, sigma: sigma32_32 }; + const k = new Uint8Array(32); + k.set(key); + k.set(key, 16); + return { key, sigma: sigma16_32 }; +}; + +describe('Salsa20', () => { + should('basic', () => { + for (const v of stable_salsa) { + { + const dst = salsa20(hex.decode(v.key), hex.decode(v.nonce), new Uint8Array(v.length)); + const res = new Uint8Array(64); + let i = 0; + while (i < dst.length) for (let j = 0; j < 64; j++) res[j] ^= dst[i++]; + deepStrictEqual(hex.encode(res), v.digest); + } + { + const dst = slow.salsa20(hex.decode(v.key), hex.decode(v.nonce), new Uint8Array(v.length)); + const res = new Uint8Array(64); + let i = 0; + while (i < dst.length) for (let j = 0; j < 64; j++) res[j] ^= dst[i++]; + deepStrictEqual(hex.encode(res), v.digest); + } + } + }); + should('hsalsa', () => { + const src = hex.decode('fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0'); + const good = 'c6cb53882782b5b86df1ab2ed9b810ec8a88c0a7f29211e693f0019fe0728858'; + const dst = new Uint8Array(32); + const { key, sigma } = getKey( + hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') + ); + deepStrictEqual(hex.encode(hsalsa(sigma, key, src, dst)), good); + deepStrictEqual(hex.encode(slow.hsalsa(sigma, key, src, dst)), good); + }); + should('xsalsa20', () => { + const key = hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'); + const nonce = hex.decode('fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8'); + const good = + '300885cce813d8cdbe05f89706f9d5557041e4fadc3ebc5db89c6ca60f7' + + '3ede4f91ff1f9521d3e9af058e037e7fd0601db9ccbd7a9f5ced151426f' + + 'de32fc544f4f95576e2614377049c258664845a93d5ff5dd479cfeb55c7' + + '579b60d419b8a8c03da3494993577b4597dcb658be52ab7'; + const dst = new Uint8Array(good.length / 2); + deepStrictEqual(hex.encode(xsalsa20(key, nonce, dst)), good); + const dst2 = new Uint8Array(good.length / 2); + deepStrictEqual(hex.encode(slow.xsalsa20(key, nonce, dst2)), good); + }); +}); + +describe('chacha', () => { + should('basic', () => { + for (const v of stable_chacha) { + const res = chacha20orig( + hex.decode(v.key), + hex.decode(v.nonce), + new Uint8Array(v.stream.length / 2) + ); + deepStrictEqual(hex.encode(res), v.stream); + const res2 = slow.chacha20orig( + hex.decode(v.key), + hex.decode(v.nonce), + new Uint8Array(v.stream.length / 2) + ); + deepStrictEqual(hex.encode(res2), v.stream); + } + }); + + // test taken from draft-arciszewski-xchacha-03 section 2.2.1 + // see https://tools.ietf.org/html/draft-arciszewski-xchacha-03#section-2.2.1 + should('hchacha', () => { + const { key, sigma } = getKey( + hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') + ); + const nonce = hex.decode('000000090000004a0000000031415927'); + const good = '82413b4227b27bfed30e42508a877d73a0f9e4d58a74a853c12ec41326d3ecdc'; + const subkey = hchacha(sigma, key, nonce.subarray(0, 16), new Uint8Array(32)); + deepStrictEqual(hex.encode(subkey), good); + const subkeySlow = slow.hchacha(sigma, key, nonce.subarray(0, 16), new Uint8Array(32)); + deepStrictEqual(hex.encode(subkeySlow), good); + }); + + // test taken from XChaCha20 TV1 in libsodium (line 93 in libsodium/test/default/xchacha20.c) + should('xchacha20/0', () => { + const key = hex.decode('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4'); + const nonce = hex.decode('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419'); + const good = 'c6e9758160083ac604ef90e712ce6e75d7797590744e0cf060f013739c'; + deepStrictEqual(hex.encode(xchacha20(key, nonce, new Uint8Array(good.length / 2))), good); + deepStrictEqual(hex.encode(slow.xchacha20(key, nonce, new Uint8Array(good.length / 2))), good); + }); + + // test taken from draft-arciszewski-xchacha-03 section A.3.2 + // see https://tools.ietf.org/html/draft-arciszewski-xchacha-03#appendix-A.3.2 + should('xchacha20/1', () => { + const key = hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'); + const plaintext = hex.decode( + '5468652064686f6c65202870726f6e6f756e6365642022646f6c652229206973' + + '20616c736f206b6e6f776e2061732074686520417369617469632077696c6420' + + '646f672c2072656420646f672c20616e642077686973746c696e6720646f672e' + + '2049742069732061626f7574207468652073697a65206f662061204765726d61' + + '6e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061' + + '206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c' + + '757369766520616e6420736b696c6c6564206a756d70657220697320636c6173' + + '736966696564207769746820776f6c7665732c20636f796f7465732c206a6163' + + '6b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d6963' + + '2066616d696c792043616e696461652e' + ); + const nonce = hex.decode('404142434445464748494a4b4c4d4e4f5051525354555658'); + const ciphertext = + '4559abba4e48c16102e8bb2c05e6947f' + + '50a786de162f9b0b7e592a9b53d0d4e9' + + '8d8d6410d540a1a6375b26d80dace4fa' + + 'b52384c731acbf16a5923c0c48d3575d' + + '4d0d2c673b666faa731061277701093a' + + '6bf7a158a8864292a41c48e3a9b4c0da' + + 'ece0f8d98d0d7e05b37a307bbb663331' + + '64ec9e1b24ea0d6c3ffddcec4f68e744' + + '3056193a03c810e11344ca06d8ed8a2b' + + 'fb1e8d48cfa6bc0eb4e2464b74814240' + + '7c9f431aee769960e15ba8b96890466e' + + 'f2457599852385c661f752ce20f9da0c' + + '09ab6b19df74e76a95967446f8d0fd41' + + '5e7bee2a12a114c20eb5292ae7a349ae' + + '577820d5520a1f3fb62a17ce6a7e68fa' + + '7c79111d8860920bc048ef43fe84486c' + + 'cb87c25f0ae045f0cce1e7989a9aa220' + + 'a28bdd4827e751a24a6d5c62d790a663' + + '93b93111c1a55dd7421a10184974c7c5'; + deepStrictEqual(hex.encode(xchacha20(key, nonce, plaintext)), ciphertext); + deepStrictEqual(hex.encode(slow.xchacha20(key, nonce, plaintext)), ciphertext); + }); +}); + +describe('poly1305', () => { + should('basic', () => { + for (const v of stable_poly1305) { + deepStrictEqual( + hex.encode(poly1305(hex.decode(v.data), hex.decode(v.key).subarray(0, 32))), + v.mac + ); + deepStrictEqual( + hex.encode(slow.poly1305(hex.decode(v.data), hex.decode(v.key).subarray(0, 32))), + v.mac, + 'slow' + ); + } + }); + + should('multiple updates', () => { + const key = new Uint8Array(32); + for (let i = 0; i < key.length; i++) key[i] = i; + const data = new Uint8Array(4 + 64 + 169); + for (let i = 0; i < data.length; i++) data[i] = i; + const d1 = data.subarray(0, 4); + const d2 = data.subarray(4, 4 + 64); + const d3 = data.subarray(4 + 64); + deepStrictEqual([d1, d2, d3].map(hex.encode).join(''), hex.encode(data)); + const r1 = poly1305.create(key).update(data).digest(); + const r2 = poly1305.create(key).update(d1).update(d2).update(d3).digest(); + deepStrictEqual(hex.encode(r1), hex.encode(r2)); + }); + + const t = (name, testVectors, cipher) => { + describe(name, () => { + for (let i = 0; i < testVectors.length; i++) { + const v = testVectors[i]; + should(`${i}`, () => { + const aad = v.aad ? hex.decode(v.aad) : undefined; + const c = cipher(hex.decode(v.key), hex.decode(v.nonce), aad); + const msg = hex.decode(v.msg); + const exp = hex.decode(v.result); + + // console.log('V', v); + deepStrictEqual(hex.encode(c.encrypt(msg)), v.result, 'encrypt'); + const plaintext = c.decrypt(exp); + deepStrictEqual(hex.encode(plaintext), v.msg, 'decrypt'); + const corrupt = exp.slice(); + corrupt[corrupt.length - 1] = 0; + throws(() => c.decrypt(corrupt)); + }); + } + }); + }; + t('Chacha20Poly1305', stable_chacha_poly, chacha20poly1305); + t('Xchacha20Poly1305', stable_xchacha_poly, xchacha20poly1305); + t('Chacha20Poly1305', stable_chacha_poly, slow.chacha20poly1305); + t('Xchacha20Poly1305', stable_xchacha_poly, slow.xchacha20poly1305); +}); + +should('tweetnacl secretbox compat', () => { + for (let i = 0; i < tweetnacl_secretbox.length; i++) { + const v = tweetnacl_secretbox[i]; + const [key, nonce, msg, exp] = v.map(base64.decode); + const c = xsalsa20poly1305(key, nonce); + deepStrictEqual(hex.encode(c.encrypt(msg)), hex.encode(exp), i); + deepStrictEqual(hex.encode(c.decrypt(exp)), hex.encode(msg), i); + const cSlow = slow.xsalsa20poly1305(key, nonce); + deepStrictEqual(hex.encode(cSlow.encrypt(msg)), hex.encode(exp), i); + deepStrictEqual(hex.encode(cSlow.decrypt(exp)), hex.encode(msg), i); + } +}); + +describe('handle byte offsets correctly', () => { + const VECTORS = { + chacha20poly1305: { stream: chacha20poly1305, nonceLen: 12, keyLen: 32 }, + xchacha20poly1305: { stream: xchacha20poly1305, nonceLen: 24, keyLen: 32 }, + xsalsa20poly1305: { stream: xsalsa20poly1305, nonceLen: 24, keyLen: 32 }, + }; + for (const name in VECTORS) { + const v = VECTORS[name]; + should(name, () => { + const sample = new Uint8Array(60).fill(1); + const data = new Uint8Array(sample.buffer, 1); + const key = new Uint8Array(v.keyLen).fill(2); + const nonce = new Uint8Array(v.nonceLen).fill(3); + const stream_c = v.stream(key, nonce); + const encrypted_c = stream_c.encrypt(data); + const decrypted_c = stream_c.decrypt(encrypted_c); + deepStrictEqual(decrypted_c, data); + // Key + nonce with offset + const keyOffset = new Uint8Array(v.keyLen + 1).fill(2).subarray(1); + const nonceOffset = new Uint8Array(v.nonceLen + 1).fill(3).subarray(1); + const streamOffset = v.stream(keyOffset, nonceOffset); + const encryptedOffset = stream_c.encrypt(data); + deepStrictEqual(encryptedOffset, encrypted_c); + const decryptedOffset = streamOffset.decrypt(encryptedOffset); + deepStrictEqual(decryptedOffset, data); + }); + } +}); + +describe('Wycheproof', () => { + const t = (name, vectors, cipher) => { + should(name, () => { + for (const group of vectors.testGroups) { + for (const t of group.tests) { + const ct = t.ct + t.tag; + const aad = t.aad ? hex.decode(t.aad) : undefined; + if (t.result !== 'invalid') { + const c = cipher(hex.decode(t.key), hex.decode(t.iv), aad); + const enc = c.encrypt(hex.decode(t.msg)); + deepStrictEqual(hex.encode(enc), ct); + const dec = c.decrypt(hex.decode(ct)); + deepStrictEqual(hex.encode(dec), t.msg); + } else { + throws(() => { + const c = cipher(hex.decode(t.key), hex.decode(t.iv), aad); + const enc = c.encrypt(hex.decode(t.msg)); + deepStrictEqual(hex.encode(enc), ct); + const dec = c.decrypt(hex.decode(ct)); + deepStrictEqual(hex.encode(dec), t.msg); + }); + } + } + } + }); + }; + t('wycheproof_chacha20_poly1305', wycheproof_chacha20_poly1305, chacha20poly1305); + t('wycheproof_xchacha20_poly1305', wycheproof_xchacha20_poly1305, xchacha20poly1305); + t('wycheproof_chacha20_poly1305', wycheproof_chacha20_poly1305, slow.chacha20poly1305); + t('wycheproof_xchacha20_poly1305', wycheproof_xchacha20_poly1305, slow.xchacha20poly1305); +}); + +if (require.main === module) should.run(); diff --git a/test/basic.test.js b/test/basic.test.js index 7f0b085..b63761d 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -1,299 +1,76 @@ -const { deepStrictEqual, throws } = require('assert'); +const { deepStrictEqual } = require('assert'); const { should, describe } = require('micro-should'); -const { hex, base64 } = require('@scure/base'); -const { salsa20, hsalsa, xsalsa20, xsalsa20poly1305 } = require('../salsa.js'); -const { - chacha20, - chacha20orig, - hchacha, - xchacha20, - chacha20poly1305, - xchacha20poly1305, -} = require('../chacha.js'); -const { poly1305 } = require('../_poly1305.js'); -const slow = require('../_micro.js'); -const tweetnacl_secretbox = require('./vectors/tweetnacl_secretbox.json'); -// Stablelib tests -const stable_salsa = require('./vectors/stablelib_salsa20.json'); -const stable_chacha = require('./vectors/stablelib_chacha20.json'); +const { hex } = require('@scure/base'); +const { managedNonce, randomBytes } = require('../webcrypto/utils.js'); -const stable_chacha_poly = require('./vectors/stablelib_chacha20poly1305.json'); -const stable_xchacha_poly = require('./vectors/stablelib_xchacha20poly1305.json'); -const stable_poly1305 = require('./vectors/stablelib_poly1305.json'); -// Wycheproof -const wycheproof_chacha20_poly1305 = require('./wycheproof/chacha20_poly1305_test.json'); -const wycheproof_xchacha20_poly1305 = require('./wycheproof/xchacha20_poly1305_test.json'); -const aes = require('../aes.js'); -// getKey for hsalsa/hchacha -const utils = require('../utils.js'); -const sigma16 = utils.utf8ToBytes('expand 16-byte k'); -const sigma32 = utils.utf8ToBytes('expand 32-byte k'); -const sigma16_32 = utils.u32(sigma16); -const sigma32_32 = utils.u32(sigma32); +const { siv, gcm, ctr, ecb, cbc } = require('../aes.js'); +const { xsalsa20poly1305 } = require('../salsa.js'); +const { chacha20poly1305, xchacha20poly1305 } = require('../chacha.js'); +const micro = require('../_micro.js'); -const getKey = (key) => { - if (key.length === 32) return { key, sigma: sigma32_32 }; - const k = new Uint8Array(32); - k.set(key); - k.set(key, 16); - return { key, sigma: sigma16_32 }; -}; - -describe('Salsa20', () => { - should('basic', () => { - for (const v of stable_salsa) { - { - const dst = salsa20(hex.decode(v.key), hex.decode(v.nonce), new Uint8Array(v.length)); - const res = new Uint8Array(64); - let i = 0; - while (i < dst.length) for (let j = 0; j < 64; j++) res[j] ^= dst[i++]; - deepStrictEqual(hex.encode(res), v.digest); - } - { - const dst = slow.salsa20(hex.decode(v.key), hex.decode(v.nonce), new Uint8Array(v.length)); - const res = new Uint8Array(64); - let i = 0; - while (i < dst.length) for (let j = 0; j < 64; j++) res[j] ^= dst[i++]; - deepStrictEqual(hex.encode(res), v.digest); - } - } - }); - should('hsalsa', () => { - const src = hex.decode('fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0'); - const good = 'c6cb53882782b5b86df1ab2ed9b810ec8a88c0a7f29211e693f0019fe0728858'; - const dst = new Uint8Array(32); - const { key, sigma } = getKey( - hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') - ); - deepStrictEqual(hex.encode(hsalsa(sigma, key, src, dst)), good); - deepStrictEqual(hex.encode(slow.hsalsa(sigma, key, src, dst)), good); - }); - should('xsalsa20', () => { - const key = hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'); - const nonce = hex.decode('fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8'); - const good = - '300885cce813d8cdbe05f89706f9d5557041e4fadc3ebc5db89c6ca60f7' + - '3ede4f91ff1f9521d3e9af058e037e7fd0601db9ccbd7a9f5ced151426f' + - 'de32fc544f4f95576e2614377049c258664845a93d5ff5dd479cfeb55c7' + - '579b60d419b8a8c03da3494993577b4597dcb658be52ab7'; - const dst = new Uint8Array(good.length / 2); - deepStrictEqual(hex.encode(xsalsa20(key, nonce, dst)), good); - const dst2 = new Uint8Array(good.length / 2); - deepStrictEqual(hex.encode(slow.xsalsa20(key, nonce, dst2)), good); - }); -}); - -describe('chacha', () => { - should('basic', () => { - for (const v of stable_chacha) { - const res = chacha20orig( - hex.decode(v.key), - hex.decode(v.nonce), - new Uint8Array(v.stream.length / 2) - ); - deepStrictEqual(hex.encode(res), v.stream); - const res2 = slow.chacha20orig( - hex.decode(v.key), - hex.decode(v.nonce), - new Uint8Array(v.stream.length / 2) - ); - deepStrictEqual(hex.encode(res2), v.stream); - } - }); +const CIPHERS = { + xsalsa20poly1305: { fn: xsalsa20poly1305, keyLen: 32, withNonce: true }, + chacha20poly1305: { fn: chacha20poly1305, keyLen: 32, withNonce: true }, + xchacha20poly1305: { fn: xchacha20poly1305, keyLen: 32, withNonce: true }, - // test taken from draft-arciszewski-xchacha-03 section 2.2.1 - // see https://tools.ietf.org/html/draft-arciszewski-xchacha-03#section-2.2.1 - should('hchacha', () => { - const { key, sigma } = getKey( - hex.decode('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') - ); - const nonce = hex.decode('000000090000004a0000000031415927'); - const good = '82413b4227b27bfed30e42508a877d73a0f9e4d58a74a853c12ec41326d3ecdc'; - const subkey = hchacha(sigma, key, nonce.subarray(0, 16), new Uint8Array(32)); - deepStrictEqual(hex.encode(subkey), good); - const subkeySlow = slow.hchacha(sigma, key, nonce.subarray(0, 16), new Uint8Array(32)); - deepStrictEqual(hex.encode(subkeySlow), good); - }); - - // test taken from XChaCha20 TV1 in libsodium (line 93 in libsodium/test/default/xchacha20.c) - should('xchacha20/0', () => { - const key = hex.decode('79c99798ac67300bbb2704c95c341e3245f3dcb21761b98e52ff45b24f304fc4'); - const nonce = hex.decode('b33ffd3096479bcfbc9aee49417688a0a2554f8d95389419'); - const good = 'c6e9758160083ac604ef90e712ce6e75d7797590744e0cf060f013739c'; - deepStrictEqual(hex.encode(xchacha20(key, nonce, new Uint8Array(good.length / 2))), good); - deepStrictEqual(hex.encode(slow.xchacha20(key, nonce, new Uint8Array(good.length / 2))), good); - }); - - // test taken from draft-arciszewski-xchacha-03 section A.3.2 - // see https://tools.ietf.org/html/draft-arciszewski-xchacha-03#appendix-A.3.2 - should('xchacha20/1', () => { - const key = hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'); - const plaintext = hex.decode( - '5468652064686f6c65202870726f6e6f756e6365642022646f6c652229206973' + - '20616c736f206b6e6f776e2061732074686520417369617469632077696c6420' + - '646f672c2072656420646f672c20616e642077686973746c696e6720646f672e' + - '2049742069732061626f7574207468652073697a65206f662061204765726d61' + - '6e20736865706865726420627574206c6f6f6b73206d6f7265206c696b652061' + - '206c6f6e672d6c656767656420666f782e205468697320686967686c7920656c' + - '757369766520616e6420736b696c6c6564206a756d70657220697320636c6173' + - '736966696564207769746820776f6c7665732c20636f796f7465732c206a6163' + - '6b616c732c20616e6420666f78657320696e20746865207461786f6e6f6d6963' + - '2066616d696c792043616e696461652e' - ); - const nonce = hex.decode('404142434445464748494a4b4c4d4e4f5051525354555658'); - const ciphertext = - '4559abba4e48c16102e8bb2c05e6947f' + - '50a786de162f9b0b7e592a9b53d0d4e9' + - '8d8d6410d540a1a6375b26d80dace4fa' + - 'b52384c731acbf16a5923c0c48d3575d' + - '4d0d2c673b666faa731061277701093a' + - '6bf7a158a8864292a41c48e3a9b4c0da' + - 'ece0f8d98d0d7e05b37a307bbb663331' + - '64ec9e1b24ea0d6c3ffddcec4f68e744' + - '3056193a03c810e11344ca06d8ed8a2b' + - 'fb1e8d48cfa6bc0eb4e2464b74814240' + - '7c9f431aee769960e15ba8b96890466e' + - 'f2457599852385c661f752ce20f9da0c' + - '09ab6b19df74e76a95967446f8d0fd41' + - '5e7bee2a12a114c20eb5292ae7a349ae' + - '577820d5520a1f3fb62a17ce6a7e68fa' + - '7c79111d8860920bc048ef43fe84486c' + - 'cb87c25f0ae045f0cce1e7989a9aa220' + - 'a28bdd4827e751a24a6d5c62d790a663' + - '93b93111c1a55dd7421a10184974c7c5'; - deepStrictEqual(hex.encode(xchacha20(key, nonce, plaintext)), ciphertext); - deepStrictEqual(hex.encode(slow.xchacha20(key, nonce, plaintext)), ciphertext); - }); -}); - -describe('poly1305', () => { - should('basic', () => { - for (const v of stable_poly1305) { - deepStrictEqual( - hex.encode(poly1305(hex.decode(v.data), hex.decode(v.key).subarray(0, 32))), - v.mac - ); - deepStrictEqual( - hex.encode(slow.poly1305(hex.decode(v.data), hex.decode(v.key).subarray(0, 32))), - v.mac, - 'slow' - ); - } - }); + micro_xsalsa20poly1305: { fn: micro.xsalsa20poly1305, keyLen: 32, withNonce: true }, + micro_chacha20poly1305: { fn: micro.chacha20poly1305, keyLen: 32, withNonce: true }, + micro_xchacha20poly1305: { fn: micro.xchacha20poly1305, keyLen: 32, withNonce: true }, +}; - should('multiple updates', () => { - const key = new Uint8Array(32); - for (let i = 0; i < key.length; i++) key[i] = i; - const data = new Uint8Array(4 + 64 + 169); - for (let i = 0; i < data.length; i++) data[i] = i; - const d1 = data.subarray(0, 4); - const d2 = data.subarray(4, 4 + 64); - const d3 = data.subarray(4 + 64); - deepStrictEqual([d1, d2, d3].map(hex.encode).join(''), hex.encode(data)); - const r1 = poly1305.create(key).update(data).digest(); - const r2 = poly1305.create(key).update(d1).update(d2).update(d3).digest(); - deepStrictEqual(hex.encode(r1), hex.encode(r2)); - }); +for (const [name, fn] of Object.entries({ ecb, cbc, ctr, gcm, siv })) { + for (const keyLen of [16, 24, 32]) { + CIPHERS[`${name}_${keyLen * 8}`] = { fn, keyLen, withNonce: name !== 'ecb' }; + } +} - const t = (name, testVectors, cipher) => { - describe(name, () => { - for (let i = 0; i < testVectors.length; i++) { - const v = testVectors[i]; - should(`${i}`, () => { - const aad = v.aad ? hex.decode(v.aad) : undefined; - const c = cipher(hex.decode(v.key), hex.decode(v.nonce), aad); - const msg = hex.decode(v.msg); - const exp = hex.decode(v.result); +for (const k in CIPHERS) { + const opts = CIPHERS[k]; + if (!opts.withNonce) continue; + CIPHERS[`${k}_managedNonce`] = { ...opts, fn: managedNonce(opts.fn), withNonce: false }; +} - // console.log('V', v); - deepStrictEqual(hex.encode(c.encrypt(msg)), v.result, 'encrypt'); - const plaintext = c.decrypt(exp); - deepStrictEqual(hex.encode(plaintext), v.msg, 'decrypt'); - const corrupt = exp.slice(); - corrupt[corrupt.length - 1] = 0; - throws(() => c.decrypt(corrupt)); - }); - } - }); - }; - t('Chacha20Poly1305', stable_chacha_poly, chacha20poly1305); - t('Xchacha20Poly1305', stable_xchacha_poly, xchacha20poly1305); - t('Chacha20Poly1305', stable_chacha_poly, slow.chacha20poly1305); - t('Xchacha20Poly1305', stable_xchacha_poly, slow.xchacha20poly1305); -}); +// Just to verify parameter passing works, should throw on round-trip test, but pass blockSize +// CIPHERS.test = { fn: managedNonce(cbc), args: [{ disablePadding: true }] }; -should('tweetnacl secretbox compat', () => { - for (let i = 0; i < tweetnacl_secretbox.length; i++) { - const v = tweetnacl_secretbox[i]; - const [key, nonce, msg, exp] = v.map(base64.decode); - const c = xsalsa20poly1305(key, nonce); - deepStrictEqual(hex.encode(c.encrypt(msg)), hex.encode(exp), i); - deepStrictEqual(hex.encode(c.decrypt(exp)), hex.encode(msg), i); - const cSlow = slow.xsalsa20poly1305(key, nonce); - deepStrictEqual(hex.encode(cSlow.encrypt(msg)), hex.encode(exp), i); - deepStrictEqual(hex.encode(cSlow.decrypt(exp)), hex.encode(msg), i); +const initCipher = (opts) => { + const { fn, keyLen, withNonce } = opts; + const args = opts.args || []; + const key = randomBytes(keyLen); + if (withNonce) { + const nonce = randomBytes(fn.nonceLength); + return fn(key, nonce, ...args); } -}); + return fn(key, ...args); +}; -describe('handle byte offsets correctly', () => { - const VECTORS = { - chacha20poly1305: { stream: chacha20poly1305, nonceLen: 12, keyLen: 32 }, - xchacha20poly1305: { stream: xchacha20poly1305, nonceLen: 24, keyLen: 32 }, - xsalsa20poly1305: { stream: xsalsa20poly1305, nonceLen: 24, keyLen: 32 }, - }; - for (const name in VECTORS) { - const v = VECTORS[name]; - should(name, () => { - const sample = new Uint8Array(60).fill(1); - const data = new Uint8Array(sample.buffer, 1); - const key = new Uint8Array(v.keyLen).fill(2); - const nonce = new Uint8Array(v.nonceLen).fill(3); - const stream_c = v.stream(key, nonce); - const encrypted_c = stream_c.encrypt(data); - const decrypted_c = stream_c.decrypt(encrypted_c); - deepStrictEqual(decrypted_c, data); - // Key + nonce with offset - const keyOffset = new Uint8Array(v.keyLen + 1).fill(2).subarray(1); - const nonceOffset = new Uint8Array(v.nonceLen + 1).fill(3).subarray(1); - const streamOffset = v.stream(keyOffset, nonceOffset); - const encryptedOffset = stream_c.encrypt(data); - deepStrictEqual(encryptedOffset, encrypted_c); - const decryptedOffset = streamOffset.decrypt(encryptedOffset); - deepStrictEqual(decryptedOffset, data); +describe('Basic', () => { + for (const k in CIPHERS) { + const opts = CIPHERS[k]; + should(`${k}: blockSize`, () => { + const c = initCipher(opts); + const msg = new Uint8Array(opts.fn.blockSize).fill(12); + deepStrictEqual(c.decrypt(c.encrypt(msg.slice())), msg); }); - } -}); -describe('Wycheproof', () => { - const t = (name, vectors, cipher) => { - should(name, () => { - for (const group of vectors.testGroups) { - for (const t of group.tests) { - const ct = t.ct + t.tag; - const aad = t.aad ? hex.decode(t.aad) : undefined; - if (t.result !== 'invalid') { - const c = cipher(hex.decode(t.key), hex.decode(t.iv), aad); - const enc = c.encrypt(hex.decode(t.msg)); - deepStrictEqual(hex.encode(enc), ct); - const dec = c.decrypt(hex.decode(ct)); - deepStrictEqual(hex.encode(dec), t.msg); - } else { - throws(() => { - const c = cipher(hex.decode(t.key), hex.decode(t.iv), aad); - const enc = c.encrypt(hex.decode(t.msg)); - deepStrictEqual(hex.encode(enc), ct); - const dec = c.decrypt(hex.decode(ct)); - deepStrictEqual(hex.encode(dec), t.msg); - }); - } - } + should(`${k}: round-trip`, () => { + const c = initCipher(opts); + // slice, so cipher has no way to corrupt msg + const msg = new Uint8Array(2).fill(12); + deepStrictEqual(c.decrypt(c.encrypt(msg.slice())), msg); + const msg2 = new Uint8Array(2048).fill(255); + deepStrictEqual(c.decrypt(c.encrypt(msg2.slice())), msg2); + const msg3 = new Uint8Array(256); + deepStrictEqual(c.decrypt(c.encrypt(msg3.slice())), msg3); + }); + should(`${k}: different sizes`, () => { + const c = initCipher(opts); + for (let i = 0; i < 2048; i++) { + const msg = new Uint8Array(i).fill(i); + deepStrictEqual(c.decrypt(c.encrypt(msg.slice())), msg); } }); - }; - t('wycheproof_chacha20_poly1305', wycheproof_chacha20_poly1305, chacha20poly1305); - t('wycheproof_xchacha20_poly1305', wycheproof_xchacha20_poly1305, xchacha20poly1305); - t('wycheproof_chacha20_poly1305', wycheproof_chacha20_poly1305, slow.chacha20poly1305); - t('wycheproof_xchacha20_poly1305', wycheproof_xchacha20_poly1305, slow.xchacha20poly1305); + } }); if (require.main === module) should.run(); diff --git a/test/ff1.test.js b/test/ff1.test.js index 7a6b48f..36f138b 100644 --- a/test/ff1.test.js +++ b/test/ff1.test.js @@ -1,9 +1,6 @@ -const { webcrypto } = require('node:crypto'); -if (!globalThis.crypto) globalThis.crypto = webcrypto; - const assert = require('assert'); const { should } = require('micro-should'); -const { FF1, BinaryFF1 } = require('../webcrypto/ff1.js'); +const { FF1, BinaryFF1 } = require('../ff1.js'); const v = require('./vectors/ff1.json'); const BIN_VECTORS = v.v; // @ts-ignore @@ -71,45 +68,45 @@ const VECTORS = [ }, ]; -should('FF1: simple test', async () => { +should('FF1: simple test', () => { const bytes = new Uint8Array([ 156, 161, 238, 80, 84, 230, 40, 147, 212, 166, 85, 71, 189, 19, 216, 222, 239, 239, 247, 244, 254, 223, 161, 182, 178, 156, 92, 134, 113, 32, 54, 74, ]); const ff1 = BinaryFF1(bytes); - let res = await ff1.encrypt([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + let res = ff1.encrypt([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); assert.deepStrictEqual(res, new Uint8Array([59, 246, 250, 31, 131, 191, 69, 99, 200, 167, 19])); }); for (let i = 0; i < VECTORS.length; i++) { const v = VECTORS[i]; const ff1 = FF1(v.radix, v.key, v.tweak); - should(`NIST vector (${i}): encrypt`, async () => { - assert.deepStrictEqual(await ff1.encrypt(v.X), v.AB); + should(`NIST vector (${i}): encrypt`, () => { + assert.deepStrictEqual(ff1.encrypt(v.X), v.AB); }); - should(`NIST vector (${i}): decrypt`, async () => { - assert.deepStrictEqual(await ff1.decrypt(v.AB), v.X); + should(`NIST vector (${i}): decrypt`, () => { + assert.deepStrictEqual(ff1.decrypt(v.AB), v.X); }); } -should(`Binary FF1 encrypt`, async () => { +should(`Binary FF1 encrypt`, () => { for (let i = 0; i < BIN_VECTORS.length; i++) { const v = BIN_VECTORS[i]; const ff1 = BinaryFF1(fromHex(v.key)); // minLen is 2 by spec if (v.data.length < 2) continue; - const res = await ff1.encrypt(fromHex(v.data)); + const res = ff1.encrypt(fromHex(v.data)); assert.deepStrictEqual(res, fromHex(v.exp), i); } }); -should(`Binary FF1 decrypt`, async () => { +should(`Binary FF1 decrypt`, () => { for (let i = 0; i < BIN_VECTORS.length; i++) { const v = BIN_VECTORS[i]; const ff1 = BinaryFF1(fromHex(v.key)); // minLen is 2 by spec if (v.data.length < 2) continue; - const res = await ff1.decrypt(fromHex(v.exp)); + const res = ff1.decrypt(fromHex(v.exp)); assert.deepStrictEqual(res, fromHex(v.data), i); } }); diff --git a/test/index.js b/test/index.js index 9b8698d..88ccaa3 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ const { should } = require('micro-should'); require('./basic.test.js'); +require('./arx.test.js'); require('./polyval.test.js'); require('./aes.test.js'); require('./ff1.test.js'); diff --git a/tsconfig.esm.json b/tsconfig.esm.json index 483fe52..450b6f4 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -7,7 +7,9 @@ "moduleResolution": "bundler", "baseUrl": ".", "paths": { - "@noble/ciphers/webcrypto/crypto": ["src/webcrypto/crypto"] + "@noble/ciphers/webcrypto/crypto": [ + "src/webcrypto/crypto" + ] }, "sourceMap": true, "allowSyntheticDefaultImports": false, @@ -19,6 +21,11 @@ "noUnusedLocals": true, "noUnusedParameters": true, }, - "include": ["src"], - "exclude": ["node_modules", "lib"] -} + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "lib" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 171cd5e..64434c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,12 +2,17 @@ "compilerOptions": { "outDir": ".", "target": "es2020", - "lib": ["es2020", "dom"], // Set explicitly to remove DOM + "lib": [ + "es2020", + "dom" + ], // Set explicitly to remove DOM "module": "commonjs", "moduleResolution": "node", "baseUrl": ".", "paths": { - "@noble/ciphers/webcrypto/crypto": [ "src/webcrypto/crypto" ] + "@noble/ciphers/webcrypto/crypto": [ + "src/webcrypto/crypto" + ] }, "sourceMap": true, "declaration": true, @@ -22,9 +27,11 @@ "noUnusedLocals": true, "noUnusedParameters": true, }, - "include": ["src"], + "include": [ + "src" + ], "exclude": [ "node_modules", "*.d.ts" ], -} +} \ No newline at end of file