From ea2f2d2b88084a19230b8034823581be9395b8fc Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Thu, 27 Jun 2024 10:15:06 +0000 Subject: [PATCH] Refactor package. Add support for unaligned byte arrays. Fix typescript paths --- README.md | 77 +++++++++--------- benchmark/package-lock.json | 52 +++++-------- benchmark/package.json | 2 +- build/package-lock.json | 7 +- esm/package.json | 1 - package.json | 37 ++++----- src/_arx.ts | 14 ++-- src/_micro.ts | 8 +- src/_poly1305.ts | 1 + src/_polyval.ts | 4 +- src/aes.ts | 150 +++++++++++++++++++++++++----------- src/chacha.ts | 7 +- src/crypto.ts | 14 +--- src/cryptoNode.ts | 14 +--- src/salsa.ts | 44 +++++++---- src/utils.ts | 18 ++++- src/webcrypto.ts | 19 +++-- test/basic.test.js | 66 ++++++++++++---- test/ff1.test.js | 80 +++++++++---------- test/index.js | 1 + test/utils.js | 7 ++ test/utils.test.js | 35 ++++----- tsconfig.esm.json | 18 ++++- 23 files changed, 391 insertions(+), 285 deletions(-) diff --git a/README.md b/README.md index ebb8b25..403452c 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,11 @@ const data_ = chacha.decrypt(ciphertext); #### Use same array for input and output +This allows re-use Uint8Array between encryption/decryption calls (works if all plaintext or ciphertext are same size). + +**_NOTE_**: some ciphers don't support unaligned (`byteOffset % 4 !== 0`) Uint8Array as destination, since it will significantly +decrease performance and this optimization will become pointless. + ```js import { chacha20poly1305 } from '@noble/ciphers/chacha'; import { utf8ToBytes } from '@noble/ciphers/utils'; @@ -497,54 +502,54 @@ Benchmark results on Apple M2 with node v20: ``` encrypt (64B) -├─xsalsa20poly1305 x 485,672 ops/sec @ 2μs/op -├─chacha20poly1305 x 466,200 ops/sec @ 2μs/op -├─xchacha20poly1305 x 312,500 ops/sec @ 3μs/op -├─aes-256-gcm x 151,057 ops/sec @ 6μs/op -└─aes-256-gcm-siv x 124,984 ops/sec @ 8μs/op +├─xsalsa20poly1305 x 485,908 ops/sec @ 2μs/op +├─chacha20poly1305 x 419,639 ops/sec @ 2μs/op +├─xchacha20poly1305 x 335,232 ops/sec @ 2μs/op +├─aes-256-gcm x 143,595 ops/sec @ 6μs/op +└─aes-256-gcm-siv x 120,743 ops/sec @ 8μs/op encrypt (1KB) -├─xsalsa20poly1305 x 146,477 ops/sec @ 6μs/op -├─chacha20poly1305 x 145,518 ops/sec @ 6μs/op -├─xchacha20poly1305 x 126,119 ops/sec @ 7μs/op -├─aes-256-gcm x 43,207 ops/sec @ 23μs/op -└─aes-256-gcm-siv x 39,363 ops/sec @ 25μs/op +├─xsalsa20poly1305 x 135,924 ops/sec @ 7μs/op +├─chacha20poly1305 x 134,843 ops/sec @ 7μs/op +├─xchacha20poly1305 x 124,626 ops/sec @ 8μs/op +├─aes-256-gcm x 39,588 ops/sec @ 25μs/op +└─aes-256-gcm-siv x 36,989 ops/sec @ 27μs/op encrypt (8KB) -├─xsalsa20poly1305 x 23,773 ops/sec @ 42μs/op -├─chacha20poly1305 x 24,134 ops/sec @ 41μs/op -├─xchacha20poly1305 x 23,520 ops/sec @ 42μs/op -├─aes-256-gcm x 8,420 ops/sec @ 118μs/op -└─aes-256-gcm-siv x 8,126 ops/sec @ 123μs/op +├─xsalsa20poly1305 x 22,178 ops/sec @ 45μs/op +├─chacha20poly1305 x 22,691 ops/sec @ 44μs/op +├─xchacha20poly1305 x 22,463 ops/sec @ 44μs/op +├─aes-256-gcm x 8,082 ops/sec @ 123μs/op +└─aes-256-gcm-siv x 2,376 ops/sec @ 420μs/op encrypt (1MB) -├─xsalsa20poly1305 x 195 ops/sec @ 5ms/op -├─chacha20poly1305 x 199 ops/sec @ 5ms/op -├─xchacha20poly1305 x 198 ops/sec @ 5ms/op -├─aes-256-gcm x 76 ops/sec @ 13ms/op -└─aes-256-gcm-siv x 78 ops/sec @ 12ms/op +├─xsalsa20poly1305 x 171 ops/sec @ 5ms/op +├─chacha20poly1305 x 186 ops/sec @ 5ms/op +├─xchacha20poly1305 x 189 ops/sec @ 5ms/op +├─aes-256-gcm x 73 ops/sec @ 13ms/op +└─aes-256-gcm-siv x 77 ops/sec @ 12ms/op ``` Unauthenticated encryption: ``` encrypt (64B) -├─salsa x 1,287,001 ops/sec @ 777ns/op -├─chacha x 1,555,209 ops/sec @ 643ns/op -├─xsalsa x 938,086 ops/sec @ 1μs/op -└─xchacha x 920,810 ops/sec @ 1μs/op +├─salsa x 1,245,330 ops/sec @ 803ns/op +├─chacha x 1,468,428 ops/sec @ 681ns/op +├─xsalsa x 995,024 ops/sec @ 1μs/op +└─xchacha x 1,026,694 ops/sec @ 974ns/op encrypt (1KB) -├─salsa x 353,107 ops/sec @ 2μs/op -├─chacha x 377,216 ops/sec @ 2μs/op -├─xsalsa x 331,674 ops/sec @ 3μs/op -└─xchacha x 336,247 ops/sec @ 2μs/op +├─salsa x 349,283 ops/sec @ 2μs/op +├─chacha x 369,822 ops/sec @ 2μs/op +├─xsalsa x 326,370 ops/sec @ 3μs/op +└─xchacha x 334,001 ops/sec @ 2μs/op encrypt (8KB) -├─salsa x 57,084 ops/sec @ 17μs/op -├─chacha x 59,520 ops/sec @ 16μs/op -├─xsalsa x 57,097 ops/sec @ 17μs/op -└─xchacha x 58,278 ops/sec @ 17μs/op +├─salsa x 55,050 ops/sec @ 18μs/op +├─chacha x 56,474 ops/sec @ 17μs/op +├─xsalsa x 54,068 ops/sec @ 18μs/op +└─xchacha x 55,469 ops/sec @ 18μs/op encrypt (1MB) -├─salsa x 479 ops/sec @ 2ms/op -├─chacha x 491 ops/sec @ 2ms/op -├─xsalsa x 483 ops/sec @ 2ms/op -└─xchacha x 492 ops/sec @ 2ms/op +├─salsa x 449 ops/sec @ 2ms/op +├─chacha x 459 ops/sec @ 2ms/op +├─xsalsa x 448 ops/sec @ 2ms/op +└─xchacha x 459 ops/sec @ 2ms/op AES encrypt (64B) diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 5582ddf..7aa250c 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@chainsafe/as-chacha20poly1305": "0.1.0", "@devtomio/sodium": "0.3.0", - "@noble/ciphers": "file:..", + "@noble/ciphers": "file:noble-ciphers-0.6.0.tgz", "@stablelib/aes": "^1.0.1", "@stablelib/chacha": "1.0.1", "@stablelib/chacha20poly1305": "1.0.1", @@ -28,22 +28,6 @@ "tweetnacl": "1.0.3" } }, - "..": { - "name": "@noble/ciphers", - "version": "0.4.1", - "license": "MIT", - "devDependencies": { - "@scure/base": "1.1.3", - "fast-check": "3.0.0", - "micro-bmark": "0.3.1", - "micro-should": "0.4.0", - "prettier": "3.1.1", - "typescript": "5.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@chainsafe/as-chacha20poly1305": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz", @@ -191,20 +175,24 @@ ] }, "node_modules/@napi-rs/triples": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", - "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.2.0.tgz", + "integrity": "sha512-HAPjR3bnCsdXBsATpDIP5WCrw0JcACwhhrwIAQhiR46n+jm+a2F8kBsfseAuWtSyQ+H3Yebt2k43B5dy+04yMA==" }, "node_modules/@noble/ciphers": { - "resolved": "..", - "link": true + "version": "0.6.0", + "resolved": "file:noble-ciphers-0.6.0.tgz", + "integrity": "sha512-HuRELzNicCu/cuGBKX6iGNFHiZUmXRXIRUK7Ay+UO2UCfF08urWrIMdW/ImbN2JCSGEjCxqurjZxF5tMwqWKTw==", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@node-rs/helper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.3.3.tgz", - "integrity": "sha512-p4OdfQObGN9YFy5WZaGwlPYICQSe7xZYyXB0sxREmvj1HzGKp5bPg2PlfgfMZEfnjIA882B9ZrnagYzZihIwjA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.6.0.tgz", + "integrity": "sha512-2OTh/tokcLA1qom1zuCJm2gQzaZljCCbtX1YCrwRVd/toz7KxaDRFeLTAPwhs8m9hWgzrBn5rShRm6IaZofCPw==", "dependencies": { - "@napi-rs/triples": "^1.1.0" + "@napi-rs/triples": "^1.2.0" } }, "node_modules/@stablelib/aead": { @@ -373,9 +361,9 @@ "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" }, "node_modules/libsodium": { - "version": "0.7.11", - "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.11.tgz", - "integrity": "sha512-WPfJ7sS53I2s4iM58QxY3Inb83/6mjlYgcmZs7DJsvDlnmVUwNinBCi5vBT43P6bHRy01O4zsMU2CoVR6xJ40A==" + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.13.tgz", + "integrity": "sha512-mK8ju0fnrKXXfleL53vtp9xiPq5hKM0zbDQtcxQIsSmxNgSxqCj6R7Hl9PkrNe2j29T4yoDaF7DJLK9/i5iWUw==" }, "node_modules/libsodium-wrappers": { "version": "0.7.11", @@ -386,9 +374,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tweetnacl": { "version": "1.0.3", diff --git a/benchmark/package.json b/benchmark/package.json index e1e37b3..7d8a03e 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -14,7 +14,7 @@ "dependencies": { "@chainsafe/as-chacha20poly1305": "0.1.0", "@devtomio/sodium": "0.3.0", - "@noble/ciphers": "file:..", + "@noble/ciphers": "file:noble-ciphers-0.6.0.tgz", "@stablelib/aes": "^1.0.1", "@stablelib/chacha": "1.0.1", "@stablelib/chacha20poly1305": "1.0.1", diff --git a/build/package-lock.json b/build/package-lock.json index c799706..5bd3b9d 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -13,7 +13,8 @@ } }, "..": { - "version": "0.5.1", + "name": "@noble/ciphers", + "version": "0.6.0", "dev": true, "license": "MIT", "devDependencies": { @@ -22,8 +23,8 @@ "fast-check": "3.0.0", "micro-bmark": "0.3.1", "micro-should": "0.4.0", - "prettier": "3.1.1", - "typescript": "5.3.2" + "prettier": "3.3.2", + "typescript": "5.5.2" }, "funding": { "url": "https://paulmillr.com/funding/" diff --git a/esm/package.json b/esm/package.json index 623827d..f42e46b 100644 --- a/esm/package.json +++ b/esm/package.json @@ -5,7 +5,6 @@ "node:crypto": false }, "node": { - "./crypto.js": "./esm/cryptoNode.js", "./crypto": "./esm/cryptoNode.js" } } diff --git a/package.json b/package.json index 88a44df..1f14ce0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@noble/ciphers", - "version": "0.5.3", + "version": "0.6.0", "description": "Auditable & minimal JS implementation of Salsa20, ChaCha and AES", "files": [ "esm", @@ -28,7 +28,6 @@ "url": "git+https://github.com/paulmillr/noble-ciphers.git" }, "license": "MIT", - "sideEffects": false, "devDependencies": { "@paulmillr/jsbt": "0.1.0", "@scure/base": "1.1.3", @@ -41,24 +40,20 @@ "main": "index.js", "exports": { ".": { - "types": "./index.d.ts", "import": "./esm/index.js", - "default": "./index.js" + "require": "./index.js" }, "./_micro": { - "types": "./_micro.d.ts", "import": "./esm/_micro.js", - "default": "./_micro.js" + "require": "./_micro.js" }, "./_poly1305": { - "types": "./_poly1305.d.ts", "import": "./esm/_poly1305.js", - "default": "./_poly1305.js" + "require": "./_poly1305.js" }, "./_polyval": { - "types": "./_polyval.d.ts", "import": "./esm/_polyval.js", - "default": "./_polyval.js" + "require": "./_polyval.js" }, "./crypto": { "types": "./crypto.d.ts", @@ -70,41 +65,35 @@ "default": "./crypto.js" }, "./aes": { - "types": "./aes.d.ts", "import": "./esm/aes.js", - "default": "./aes.js" + "require": "./aes.js" }, "./chacha": { - "types": "./chacha.d.ts", "import": "./esm/chacha.js", - "default": "./chacha.js" + "require": "./chacha.js" }, "./salsa": { - "types": "./salsa.d.ts", "import": "./esm/salsa.js", - "default": "./salsa.js" + "require": "./salsa.js" }, "./ff1": { - "types": "./ff1.d.ts", "import": "./esm/ff1.js", - "default": "./ff1.js" + "require": "./ff1.js" }, "./utils": { - "types": "./utils.d.ts", "import": "./esm/utils.js", - "default": "./utils.js" + "require": "./utils.js" }, "./index": { - "types": "./index.d.ts", "import": "./esm/index.js", - "default": "./index.js" + "require": "./index.js" }, "./webcrypto": { - "types": "./webcrypto.d.ts", "import": "./esm/webcrypto.js", - "default": "./webcrypto.js" + "require": "./webcrypto.js" } }, + "sideEffects": false, "browser": { "node:crypto": false, "./crypto": "./crypto.js" diff --git a/src/_arx.ts b/src/_arx.ts index f182246..4b920bf 100644 --- a/src/_arx.ts +++ b/src/_arx.ts @@ -1,6 +1,6 @@ // Basic utils for ARX (add-rotate-xor) salsa and chacha ciphers. import { number as anumber, bytes as abytes, bool as abool } from './_assert.js'; -import { XorStream, checkOpts, u32 } from './utils.js'; +import { XorStream, checkOpts, u32, copyBytes } from './utils.js'; /* RFC8439 requires multi-step cipher stream, where @@ -149,7 +149,7 @@ export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream { abytes(nonce); abytes(data); const len = data.length; - if (!output) output = new Uint8Array(len); + if (output === undefined) output = new Uint8Array(len); abytes(output); anumber(counter); if (counter < 0 || counter >= MAX_COUNTER) throw new Error('arx: counter overflow'); @@ -164,8 +164,7 @@ export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream { k: Uint8Array, sigma: Uint32Array; if (l === 32) { - k = key.slice(); - toClean.push(k); + toClean.push((k = copyBytes(key))); sigma = sigma32_32; } else if (l === 16 && allowShortKeys) { k = new Uint8Array(32); @@ -184,10 +183,7 @@ export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream { // xsalsa20: 24 (16 -> hsalsa, 8 -> old nonce) // xchacha20: 24 (16 -> hchacha, 8 -> old nonce) // Align nonce to 4 bytes - if (!isAligned32(nonce)) { - nonce = nonce.slice(); - toClean.push(nonce); - } + if (!isAligned32(nonce)) toClean.push((nonce = copyBytes(nonce))); const k32 = u32(k); // hsalsa & hchacha: handle extended nonce @@ -211,7 +207,7 @@ export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream { } const n32 = u32(nonce); runCipher(core, sigma, k32, n32, data, output, counter, rounds); - while (toClean.length > 0) toClean.pop()!.fill(0); + for (const i of toClean) i.fill(0); return output; }; } diff --git a/src/_micro.ts b/src/_micro.ts index 77e42ee..e19b0d5 100644 --- a/src/_micro.ts +++ b/src/_micro.ts @@ -250,7 +250,7 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( abytes(key); abytes(nonce); return { - encrypt: (plaintext: Uint8Array) => { + encrypt(plaintext: Uint8Array) { abytes(plaintext); const m = concatBytes(new Uint8Array(32), plaintext); const c = xsalsa20(key, nonce, m); @@ -259,7 +259,7 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( const tag = poly1305(data, authKey); return concatBytes(tag, data); }, - decrypt: (ciphertext: Uint8Array) => { + decrypt(ciphertext: Uint8Array) { abytes(ciphertext); if (ciphertext.length < 16) throw new Error('encrypted data must be at least 16 bytes'); const c = concatBytes(new Uint8Array(16), ciphertext); @@ -288,13 +288,13 @@ export const _poly1305_aead = abytes(key, keyLength); abytes(nonce); return { - encrypt: (plaintext: Uint8Array) => { + encrypt(plaintext: Uint8Array) { abytes(plaintext); const res = fn(key, nonce, plaintext, undefined, 1); const tag = computeTag(fn, key, nonce, res, AAD); return concatBytes(res, tag); }, - decrypt: (ciphertext: Uint8Array) => { + decrypt(ciphertext: Uint8Array) { abytes(ciphertext); if (ciphertext.length < tagLength) throw new Error(`encrypted data must be at least ${tagLength} bytes`); diff --git a/src/_poly1305.ts b/src/_poly1305.ts index 7376cb9..7005175 100644 --- a/src/_poly1305.ts +++ b/src/_poly1305.ts @@ -214,6 +214,7 @@ class Poly1305 implements Hash { f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0; h[i] = f & 0xffff; } + g.fill(0); } update(data: Input): this { aexists(this); diff --git a/src/_polyval.ts b/src/_polyval.ts index 9738685..f8bd548 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, copyBytes } from './utils.js'; import { bytes as abytes, exists as aexists, output as aoutput } from './_assert.js'; // GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV. @@ -182,7 +182,7 @@ class GHASH implements Hash { class Polyval extends GHASH { constructor(key: Input, expectedLength?: number) { key = toBytes(key); - const ghKey = _toGHASHKey(key.slice()); + const ghKey = _toGHASHKey(copyBytes(key)); super(ghKey, expectedLength); ghKey.fill(0); } diff --git a/src/aes.ts b/src/aes.ts index fe78b25..d37999f 100644 --- a/src/aes.ts +++ b/src/aes.ts @@ -1,7 +1,15 @@ // prettier-ignore import { - wrapCipher, Cipher, CipherWithOutput, - createView, setBigUint64, equalBytes, u32, u8, + wrapCipher, + Cipher, + CipherWithOutput, + createView, + setBigUint64, + equalBytes, + u32, + u8, + isAligned32, + copyBytes, } from './utils.js'; import { ghash, polyval } from './_polyval.js'; import { bytes as abytes } from './_assert.js'; @@ -43,7 +51,7 @@ function mul(a: number, b: number) { // AES S-box is generated using finite field inversion, // an affine transform, and xor of a constant 0x63. const sbox = /* @__PURE__ */ (() => { - let t = new Uint8Array(256); + const t = new Uint8Array(256); for (let i = 0, x = 1; i < 256; i++, x ^= mul2(x)) t[i] = x; const box = new Uint8Array(256); box[0] = 0x63; // first elm @@ -52,6 +60,7 @@ const sbox = /* @__PURE__ */ (() => { x |= x << 8; box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff; } + t.fill(0); return box; })(); @@ -107,6 +116,8 @@ export function expandKeyLE(key: Uint8Array): Uint32Array { if (![16, 24, 32].includes(len)) throw new Error(`aes: wrong key size: should be 16, 24 or 32, got: ${len}`); const { sbox2 } = tableEncoding; + const toClean = []; + if (!isAligned32(key)) toClean.push((key = copyBytes(key))); const k32 = u32(key); const Nk = k32.length; const subByte = (n: number) => applySbox(sbox2, n, n, n, n); @@ -119,6 +130,7 @@ export function expandKeyLE(key: Uint8Array): Uint32Array { else if (Nk > 6 && i % Nk === 4) t = subByte(t); xk[i] = xk[i - Nk] ^ t; } + for (const i of toClean) i.fill(0); return xk; } @@ -205,10 +217,11 @@ function decrypt(xk: Uint32Array, s0: number, s1: number, s2: number, s3: number } function getDst(len: number, dst?: Uint8Array) { - if (!dst) return new Uint8Array(len); + if (dst === undefined) return new Uint8Array(len); abytes(dst); if (dst.length < len) throw new Error(`aes: wrong destination length, expected at least ${len}, got: ${dst.length}`); + if (!isAligned32(dst)) throw new Error('unaligned dst'); return dst; } @@ -246,6 +259,7 @@ function ctrCounter(xk: Uint32Array, nonce: Uint8Array, src: Uint8Array, dst?: U const b32 = new Uint32Array([s0, s1, s2, s3]); const buf = u8(b32); for (let i = start, pos = 0; i < srcLen; i++, pos++) dst[i] = src[i] ^ buf[pos]; + b32.fill(0); } return dst; } @@ -289,6 +303,7 @@ function ctr32( const b32 = new Uint32Array([s0, s1, s2, s3]); const buf = u8(b32); for (let i = start, pos = 0; i < srcLen; i++, pos++) dst[i] = src[i] ^ buf[pos]; + b32.fill(0); } return dst; } @@ -303,11 +318,17 @@ export const ctr = wrapCipher( abytes(key); abytes(nonce, BLOCK_SIZE); function processCtr(buf: Uint8Array, dst?: Uint8Array) { + abytes(buf); + if (dst !== undefined) { + abytes(dst); + if (!isAligned32(dst)) throw new Error('unaligned destination'); + } const xk = expandKeyLE(key); - const n = nonce.slice(); + const n = copyBytes(nonce); // align + avoid changing + const toClean = [xk, n]; + if (!isAligned32(buf)) toClean.push((buf = copyBytes(buf))); const out = ctrCounter(xk, n, buf, dst); - xk.fill(0); - n.fill(0); + for (const i of toClean) i.fill(0); return out; } return { @@ -327,10 +348,12 @@ function validateBlockDecrypt(data: Uint8Array) { } function validateBlockEncrypt(plaintext: Uint8Array, pcks5: boolean, dst?: Uint8Array) { + abytes(plaintext); let outLen = plaintext.length; const remaining = outLen % BLOCK_SIZE; if (!pcks5 && remaining !== 0) throw new Error('aec/(cbc-ecb): unpadded plaintext with disabled padding'); + if (!isAligned32(plaintext)) plaintext = copyBytes(plaintext); const b = u32(plaintext); if (pcks5) { let left = BLOCK_SIZE - remaining; @@ -375,8 +398,7 @@ export const ecb = wrapCipher( abytes(key); const pcks5 = !opts.disablePadding; return { - encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => { - abytes(plaintext); + encrypt(plaintext: Uint8Array, dst?: Uint8Array) { const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); const xk = expandKeyLE(key); let i = 0; @@ -392,17 +414,19 @@ export const ecb = wrapCipher( xk.fill(0); return _out; }, - decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => { + decrypt(ciphertext: Uint8Array, dst?: Uint8Array) { validateBlockDecrypt(ciphertext); const xk = expandKeyDecLE(key); const out = getDst(ciphertext.length, dst); + const toClean: (Uint8Array | Uint32Array)[] = [xk]; + if (!isAligned32(ciphertext)) toClean.push((ciphertext = copyBytes(ciphertext))); 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); + for (const i of toClean) i.fill(0); return validatePCKS(out, pcks5); }, }; @@ -420,10 +444,13 @@ export const cbc = wrapCipher( abytes(iv, 16); const pcks5 = !opts.disablePadding; return { - encrypt: (plaintext: Uint8Array, dst?: Uint8Array) => { + encrypt(plaintext: Uint8Array, dst?: Uint8Array) { const xk = expandKeyLE(key); const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); - const n32 = u32(iv); + let _iv = iv; + const toClean: (Uint8Array | Uint32Array)[] = [xk]; + if (!isAligned32(_iv)) toClean.push((_iv = copyBytes(_iv))); + const n32 = u32(_iv); // prettier-ignore let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; let i = 0; @@ -438,14 +465,18 @@ export const cbc = wrapCipher( ({ 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); + for (const i of toClean) i.fill(0); return _out; }, - decrypt: (ciphertext: Uint8Array, dst?: Uint8Array) => { + decrypt(ciphertext: Uint8Array, dst?: Uint8Array) { validateBlockDecrypt(ciphertext); const xk = expandKeyDecLE(key); - const n32 = u32(iv); + let _iv = iv; + const toClean: (Uint8Array | Uint32Array)[] = [xk]; + if (!isAligned32(_iv)) toClean.push((_iv = copyBytes(_iv))); + const n32 = u32(_iv); const out = getDst(ciphertext.length, dst); + if (!isAligned32(ciphertext)) toClean.push((ciphertext = copyBytes(ciphertext))); const b = u32(ciphertext); const o = u32(out); // prettier-ignore @@ -457,7 +488,7 @@ export const cbc = wrapCipher( 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); + for (const i of toClean) i.fill(0); return validatePCKS(out, pcks5); }, }; @@ -474,13 +505,18 @@ export const cfb = wrapCipher( abytes(key); abytes(iv, 16); function processCfb(src: Uint8Array, isEncrypt: boolean, dst?: Uint8Array) { - const xk = expandKeyLE(key); + abytes(src); const srcLen = src.length; dst = getDst(srcLen, dst); + const xk = expandKeyLE(key); + let _iv = iv; + const toClean: (Uint8Array | Uint32Array)[] = [xk]; + if (!isAligned32(_iv)) toClean.push((_iv = copyBytes(_iv))); + if (!isAligned32(src)) toClean.push((src = copyBytes(src))); const src32 = u32(src); const dst32 = u32(dst); const next32 = isEncrypt ? dst32 : src32; - const n32 = u32(iv); + const n32 = u32(_iv); // prettier-ignore let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; for (let i = 0; i + 4 <= src32.length; ) { @@ -499,7 +535,7 @@ export const cfb = wrapCipher( for (let i = start, pos = 0; i < srcLen; i++, pos++) dst[i] = src[i] ^ buf[pos]; buf.fill(0); } - xk.fill(0); + for (const i of toClean) i.fill(0); return dst; } return { @@ -525,7 +561,9 @@ function computeTag( if (AAD) setBigUint64(view, 0, BigInt(AAD.length * 8), isLE); setBigUint64(view, 8, BigInt(data.length * 8), isLE); h.update(num); - return h.digest(); + const res = h.digest(); + num.fill(0); + return res; } /** @@ -536,7 +574,9 @@ function computeTag( export const gcm = wrapCipher( { blockSize: 16, nonceLength: 12, tagLength: 16 }, function gcm(key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher { + abytes(key); abytes(nonce); + if (AAD !== undefined) abytes(AAD); // 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; @@ -559,35 +599,41 @@ export const gcm = wrapCipher( 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 g = ghash.create(authKey).update(nonce).update(nonceLen); + g.digestInto(counter); // digestInto doesn't trigger '.destroy' + g.destroy(); } const tagMask = ctr32(xk, false, counter, EMPTY_BLOCK); return { xk, authKey, counter, tagMask }; } return { - encrypt: (plaintext: Uint8Array) => { + encrypt(plaintext: Uint8Array) { abytes(plaintext); const { xk, authKey, counter, tagMask } = deriveKeys(); const out = new Uint8Array(plaintext.length + tagLength); + const toClean: (Uint8Array | Uint32Array)[] = [xk, authKey, counter, tagMask]; + if (!isAligned32(plaintext)) toClean.push((plaintext = copyBytes(plaintext))); ctr32(xk, false, counter, plaintext, out); const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength)); + toClean.push(tag); out.set(tag, plaintext.length); - xk.fill(0); + for (const i of toClean) i.fill(0); return out; }, - decrypt: (ciphertext: Uint8Array) => { + decrypt(ciphertext: Uint8Array) { abytes(ciphertext); if (ciphertext.length < tagLength) throw new Error(`aes/gcm: ciphertext less than tagLen (${tagLength})`); const { xk, authKey, counter, tagMask } = deriveKeys(); + const toClean: (Uint8Array | Uint32Array)[] = [xk, authKey, tagMask, counter]; + if (!isAligned32(ciphertext)) toClean.push((ciphertext = copyBytes(ciphertext))); const data = ciphertext.subarray(0, -tagLength); const passedTag = ciphertext.subarray(-tagLength); const tag = _computeTag(authKey, tagMask, data); + toClean.push(tag); 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); + for (const i of toClean) i.fill(0); return out; }, }; @@ -614,20 +660,21 @@ export const siv = wrapCipher( const PLAIN_LIMIT = limit('plaintext', 0, 2 ** 36); const NONCE_LIMIT = limit('nonce', 12, 12); const CIPHER_LIMIT = limit('ciphertext', 16, 2 ** 36 + 16); + abytes(key, 16, 24, 32); abytes(nonce); NONCE_LIMIT(nonce.length); - if (AAD) { + if (AAD !== undefined) { abytes(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 encKey = new Uint8Array(key.length); const authKey = new Uint8Array(16); - const n32 = u32(nonce); + const toClean: (Uint8Array | Uint32Array)[] = [xk, encKey]; + let _nonce = nonce; + if (!isAligned32(_nonce)) toClean.push((_nonce = copyBytes(_nonce))); + const n32 = u32(_nonce); // prettier-ignore let s0 = 0, s1 = n32[0], s2 = n32[1], s3 = n32[2]; let counter = 0; @@ -641,8 +688,10 @@ export const siv = wrapCipher( s0 = ++counter; // increment counter inside state } } - xk.fill(0); - return { authKey, encKey: expandKeyLE(encKey) }; + const res = { authKey, encKey: expandKeyLE(encKey) }; + // Cleanup + for (const i of toClean) i.fill(0); + return res; } function _computeTag(encKey: Uint32Array, authKey: Uint8Array, data: Uint8Array) { const tag = computeTag(polyval, true, authKey, data, AAD); @@ -661,33 +710,44 @@ export const siv = wrapCipher( } // actual decrypt/encrypt of message. function processSiv(encKey: Uint32Array, tag: Uint8Array, input: Uint8Array) { - let block = tag.slice(); + let block = copyBytes(tag); block[15] |= 0x80; // Force highest bit - return ctr32(encKey, true, block, input); + const res = ctr32(encKey, true, block, input); + // Cleanup + block.fill(0); + return res; } return { - encrypt: (plaintext: Uint8Array) => { + encrypt(plaintext: Uint8Array) { abytes(plaintext); PLAIN_LIMIT(plaintext.length); const { encKey, authKey } = deriveKeys(); const tag = _computeTag(encKey, authKey, plaintext); + const toClean: (Uint8Array | Uint32Array)[] = [encKey, authKey, tag]; + if (!isAligned32(plaintext)) toClean.push((plaintext = copyBytes(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); + // Cleanup + for (const i of toClean) i.fill(0); return out; }, - decrypt: (ciphertext: Uint8Array) => { + decrypt(ciphertext: Uint8Array) { abytes(ciphertext); CIPHER_LIMIT(ciphertext.length); const tag = ciphertext.subarray(-tagLength); const { encKey, authKey } = deriveKeys(); + const toClean: (Uint8Array | Uint32Array)[] = [encKey, authKey]; + if (!isAligned32(ciphertext)) toClean.push((ciphertext = copyBytes(ciphertext))); 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'); + toClean.push(expectedTag); + if (!equalBytes(tag, expectedTag)) { + for (const i of toClean) i.fill(0); + throw new Error('invalid polyval tag'); + } + // Cleanup + for (const i of toClean) i.fill(0); return plaintext; }, }; diff --git a/src/chacha.ts b/src/chacha.ts index bcd9ce6..eebc5aa 100644 --- a/src/chacha.ts +++ b/src/chacha.ts @@ -214,6 +214,7 @@ function computeTag( h.update(num); const res = h.digest(); authKey.fill(0); + num.fill(0); return res; } @@ -233,7 +234,7 @@ export const _poly1305_aead = abytes(key, 32); abytes(nonce); return { - encrypt: (plaintext: Uint8Array, output?: Uint8Array) => { + encrypt(plaintext: Uint8Array, output?: Uint8Array) { const plength = plaintext.length; const clength = plength + tagLength; if (output) { @@ -244,9 +245,10 @@ export const _poly1305_aead = xorStream(key, nonce, plaintext, output, 1); const tag = computeTag(xorStream, key, nonce, output.subarray(0, -tagLength), AAD); output.set(tag, plength); // append tag + tag.fill(0); return output; }, - decrypt: (ciphertext: Uint8Array, output?: Uint8Array) => { + decrypt(ciphertext: Uint8Array, output?: Uint8Array) { const clength = ciphertext.length; const plength = clength - tagLength; if (clength < tagLength) @@ -261,6 +263,7 @@ export const _poly1305_aead = const tag = computeTag(xorStream, key, nonce, data, AAD); if (!equalBytes(passedTag, tag)) throw new Error('invalid tag'); xorStream(key, nonce, data, output, 1); + tag.fill(0); return output; }, }; diff --git a/src/crypto.ts b/src/crypto.ts index 2ada69a..eaea6be 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -1,15 +1,5 @@ // We use WebCrypto aka globalThis.crypto, which exists in browsers and node.js 16+. // See utils.ts for details. declare const globalThis: Record | undefined; -const cr = typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; - -export function randomBytes(bytesLength = 32): Uint8Array { - if (cr && typeof cr.getRandomValues === 'function') - return cr.getRandomValues(new Uint8Array(bytesLength)); - throw new Error('crypto.getRandomValues must be defined'); -} - -export function getWebcryptoSubtle() { - if (cr && typeof cr.subtle === 'object' && cr.subtle != null) return cr.subtle; - throw new Error('crypto.subtle must be defined'); -} +export const crypto = + typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; diff --git a/src/cryptoNode.ts b/src/cryptoNode.ts index 772f3b3..6fdba9e 100644 --- a/src/cryptoNode.ts +++ b/src/cryptoNode.ts @@ -3,15 +3,5 @@ // The file will throw on node.js 14 and earlier. // @ts-ignore import * as nc from 'node:crypto'; -const cr = nc && typeof nc === 'object' && 'webcrypto' in nc ? (nc.webcrypto as any) : undefined; - -export function randomBytes(bytesLength = 32): Uint8Array { - if (cr && typeof cr.getRandomValues === 'function') - return cr.getRandomValues(new Uint8Array(bytesLength)); - throw new Error('crypto.getRandomValues must be defined'); -} - -export function getWebcryptoSubtle() { - if (cr && typeof cr.subtle === 'object' && cr.subtle != null) return cr.subtle; - throw new Error('crypto.subtle must be defined'); -} +export const crypto = + nc && typeof nc === 'object' && 'webcrypto' in nc ? (nc.webcrypto as any) : undefined; diff --git a/src/salsa.ts b/src/salsa.ts index 5cf5b92..852d965 100644 --- a/src/salsa.ts +++ b/src/salsa.ts @@ -124,7 +124,7 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( abytes(key, 32); abytes(nonce, 24); return { - encrypt: (plaintext: Uint8Array, output?: Uint8Array) => { + encrypt(plaintext: Uint8Array, output?: Uint8Array) { abytes(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) @@ -141,29 +141,39 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( // Clean auth key, even though JS provides no guarantees about memory cleaning output.set(tag, tagLength); output.subarray(0, tagLength).fill(0); + // Cleanup + tag.fill(0); return output.subarray(tagLength); }, - decrypt: (ciphertext: Uint8Array) => { + decrypt(ciphertext: Uint8Array, output?: Uint8Array) { abytes(ciphertext); - const clength = ciphertext.length; - if (clength < tagLength) throw new Error('encrypted data should be at least 16 bytes'); + if (ciphertext.length < tagLength) + throw new Error('encrypted data should be at least 16 bytes'); + const clength = ciphertext.length + 32; // 32 is authKey length + if (output) { + abytes(output, clength); + } else { + output = new Uint8Array(clength); + } // Create new ciphertext array: - // auth tag auth tag from ciphertext ciphertext - // [bytes 0..16] [bytes 16..32] [bytes 32..] + // tmp part auth tag ciphertext + // [bytes 0..32] [bytes 32..48] [bytes 48..] // 16 instead of 32, because we already have 16 byte tag - const ciphertext_ = new Uint8Array(clength + tagLength); // alloc - ciphertext_.set(ciphertext, tagLength); + output.set(ciphertext, 32); // 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); + // Here we use first 32 bytes for authKey + const authKeyBuf = output.subarray(0, 32); + authKeyBuf.fill(0); + const authKey = xsalsa20(key, nonce, authKeyBuf, authKeyBuf); + const tag = poly1305(output.subarray(32 + tagLength), authKey); // alloc + if (!equalBytes(output.subarray(32, 48), tag)) throw new Error('invalid tag'); + // NOTE: first 32 bytes skipped (used for authKey) + xsalsa20(key, nonce, output.subarray(16), output.subarray(16)); + // Cleanup + output.subarray(0, 32 + 16).fill(0); + tag.fill(0); + return output.subarray(32 + 16); }, }; } diff --git a/src/utils.ts b/src/utils.ts index 08d0aff..b068d15 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -85,7 +85,7 @@ export function numberToBytesBE(n: number | bigint, len: number): Uint8Array { // There is no setImmediate in browser and setTimeout is slow. // call of async fn will return Promise, which will be fullfiled only on // next scheduler queue processing step and this is exactly what we need. -export const nextTick = async () => {}; +export const nextTick = async () => { }; // Returns control to thread each 'tick' ms to avoid blocking export async function asyncLoop(iters: number, tick: number, cb: (i: number) => void) { @@ -128,7 +128,7 @@ export type Input = Uint8Array | string; */ export function toBytes(data: Input): Uint8Array { if (typeof data === 'string') data = utf8ToBytes(data); - else if (isBytes(data)) data = data.slice(); + else if (isBytes(data)) data = copyBytes(data); else throw new Error(`Uint8Array expected, got ${typeof data}`); return data; } @@ -251,3 +251,17 @@ export function u64Lengths(ciphertext: Uint8Array, AAD?: Uint8Array) { setBigUint64(view, 8, BigInt(ciphertext.length), true); return num; } + +// Is byte array aligned to 4 byte offset (u32)? +export function isAligned32(bytes: Uint8Array) { + return bytes.byteOffset % 4 === 0; +} + +// copy bytes to new u8a (aligned). Because Buffer.slice is broken. +export function copyBytes(bytes: Uint8Array) { + return Uint8Array.from(bytes); +} + +export function clean(bytes: Uint8Array) { + bytes.fill(0); +} diff --git a/src/webcrypto.ts b/src/webcrypto.ts index c4a3ccb..96e0d7e 100644 --- a/src/webcrypto.ts +++ b/src/webcrypto.ts @@ -6,14 +6,23 @@ // Once node.js 18 is deprecated, we can just drop the import. // // Use full path so that Node.js can rewrite it to `cryptoNode.js`. -import { randomBytes, getWebcryptoSubtle } from '@noble/ciphers/crypto'; +import { crypto } from '@noble/ciphers/crypto'; import { AsyncCipher, Cipher, concatBytes } from './utils.js'; import { number, bytes as abytes } from './_assert.js'; /** * Secure PRNG. Uses `crypto.getRandomValues`, which defers to OS. */ -export { randomBytes, getWebcryptoSubtle }; +export function randomBytes(bytesLength = 32): Uint8Array { + if (crypto && typeof crypto.getRandomValues === 'function') + return crypto.getRandomValues(new Uint8Array(bytesLength)); + throw new Error('crypto.getRandomValues must be defined'); +} + +export function getWebcryptoSubtle() { + if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) return crypto.subtle; + throw new Error('crypto.subtle must be defined'); +} type RemoveNonceInner = ((...args: T) => Ret) extends ( arg0: any, @@ -32,7 +41,7 @@ type CipherWithNonce = ((key: Uint8Array, nonce: Uint8Array, ...args: any[]) => export function managedNonce(fn: T): RemoveNonce { number(fn.nonceLength); return ((key: Uint8Array, ...args: any[]): any => ({ - encrypt: (plaintext: Uint8Array, ...argsEnc: any[]) => { + encrypt(plaintext: Uint8Array, ...argsEnc: any[]) { const { nonceLength } = fn; const nonce = randomBytes(nonceLength); const ciphertext = (fn(key, nonce, ...args).encrypt as any)(plaintext, ...argsEnc); @@ -40,7 +49,7 @@ export function managedNonce(fn: T): RemoveNonce { ciphertext.fill(0); return out; }, - decrypt: (ciphertext: Uint8Array, ...argsDec: any[]) => { + decrypt(ciphertext: Uint8Array, ...argsDec: any[]) { const { nonceLength } = fn; const nonce = ciphertext.subarray(0, nonceLength); const data = ciphertext.subarray(nonceLength); @@ -122,4 +131,4 @@ export const gcm = generate(mode.GCM); // // should fail // const wcbc2 = managedNonce(managedNonce(cbc)); -// const wecb = managedNonce(ecb); +// const wctr = managedNonce(ctr); diff --git a/test/basic.test.js b/test/basic.test.js index 66872c0..6e4b286 100644 --- a/test/basic.test.js +++ b/test/basic.test.js @@ -2,16 +2,17 @@ const { deepStrictEqual } = require('assert'); const { should, describe } = require('micro-should'); const { hex } = require('@scure/base'); const { managedNonce, randomBytes } = require('../webcrypto.js'); - const { siv, gcm, ctr, ecb, cbc, cfb } = require('../aes.js'); const { xsalsa20poly1305 } = require('../salsa.js'); const { chacha20poly1305, xchacha20poly1305 } = require('../chacha.js'); +const { unalign } = require('./utils.js'); +const { BinaryFF1 } = require('../ff1.js'); const micro = require('../_micro.js'); const CIPHERS = { xsalsa20poly1305: { fn: xsalsa20poly1305, keyLen: 32, withNonce: true }, - chacha20poly1305: { fn: chacha20poly1305, keyLen: 32, withNonce: true }, - xchacha20poly1305: { fn: xchacha20poly1305, keyLen: 32, withNonce: true }, + chacha20poly1305: { fn: chacha20poly1305, keyLen: 32, withNonce: true, withDST: true }, + xchacha20poly1305: { fn: xchacha20poly1305, keyLen: 32, withNonce: true, withDST: true }, micro_xsalsa20poly1305: { fn: micro.xsalsa20poly1305, keyLen: 32, withNonce: true }, micro_chacha20poly1305: { fn: micro.chacha20poly1305, keyLen: 32, withNonce: true }, @@ -37,39 +38,72 @@ 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); + const nonce = randomBytes(fn.nonceLength); + const c = withNonce ? fn(key, nonce, ...args) : fn(key, ...args); + return { c, key, nonce, copy: { key: key.slice(), nonce: nonce.slice() } }; }; describe('Basic', () => { for (const k in CIPHERS) { const opts = CIPHERS[k]; should(`${k}: blockSize`, () => { - const c = initCipher(opts); + const { c, key, nonce, copy } = initCipher(opts); const msg = new Uint8Array(opts.fn.blockSize).fill(12); - deepStrictEqual(c.decrypt(c.encrypt(msg.slice())), msg); + const msgCopy = msg.slice(); + deepStrictEqual(c.decrypt(c.encrypt(msgCopy)), msg); + deepStrictEqual(msg, msgCopy); + // Verify that key/nonce is not modified + deepStrictEqual(key, copy.key); + deepStrictEqual(nonce, copy.nonce); }); should(`${k}: round-trip`, () => { - const c = initCipher(opts); + const { c, key, nonce, copy } = 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 msgCopy = msg.slice(); + deepStrictEqual(c.decrypt(c.encrypt(msgCopy)), msg); + deepStrictEqual(msg, msgCopy); + const msg2 = new Uint8Array(2048).fill(255); - deepStrictEqual(c.decrypt(c.encrypt(msg2.slice())), msg2); + const msg2Copy = msg2.slice(); + deepStrictEqual(c.decrypt(c.encrypt(msg2)), msg2); + deepStrictEqual(msg2, msg2Copy); + const msg3 = new Uint8Array(256); - deepStrictEqual(c.decrypt(c.encrypt(msg3.slice())), msg3); + const msg3Copy = msg3.slice(); + deepStrictEqual(c.decrypt(c.encrypt(msg3Copy)), msg3); + deepStrictEqual(msg3, msg3Copy); + + // Verify that key/nonce is not modified + deepStrictEqual(key, copy.key); + deepStrictEqual(nonce, copy.nonce); }); should(`${k}: different sizes`, () => { - const c = initCipher(opts); + const { c, key, nonce, copy } = initCipher(opts); for (let i = 0; i < 2048; i++) { const msg = new Uint8Array(i).fill(i); - deepStrictEqual(c.decrypt(c.encrypt(msg.slice())), msg); + const msgCopy = msg.slice(); + deepStrictEqual(c.decrypt(c.encrypt(msg)), msg); + deepStrictEqual(msg, msgCopy); } + // Verify that key/nonce is not modified + deepStrictEqual(key, copy.key); + deepStrictEqual(nonce, copy.nonce); }); + for (let i = 0; i < 8; i++) { + should(`${k} (unalign ${i})`, () => { + const { fn, keyLen } = opts; + const key = unalign(randomBytes(keyLen), i); + const nonce = unalign(randomBytes(fn.nonceLength), i); + const AAD = unalign(randomBytes(64), i); + const msg = unalign(new Uint8Array(2048).fill(255), i); + const cipher = fn(key, nonce, AAD); + const encrypted = unalign(cipher.encrypt(msg), i); + const decrypted = cipher.decrypt(encrypted); + deepStrictEqual(decrypted, msg); + }); + } } }); diff --git a/test/ff1.test.js b/test/ff1.test.js index 36f138b..4f0893d 100644 --- a/test/ff1.test.js +++ b/test/ff1.test.js @@ -1,5 +1,5 @@ -const assert = require('assert'); -const { should } = require('micro-should'); +const { deepStrictEqual } = require('assert'); +const { describe, should } = require('micro-should'); const { FF1, BinaryFF1 } = require('../ff1.js'); const v = require('./vectors/ff1.json'); const BIN_VECTORS = v.v; @@ -68,47 +68,49 @@ const VECTORS = [ }, ]; -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 = 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`, () => { - assert.deepStrictEqual(ff1.encrypt(v.X), v.AB); +describe('FF1', () => { + 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 = ff1.encrypt([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + deepStrictEqual(res, new Uint8Array([59, 246, 250, 31, 131, 191, 69, 99, 200, 167, 19])); }); - should(`NIST vector (${i}): decrypt`, () => { - assert.deepStrictEqual(ff1.decrypt(v.AB), v.X); - }); -} -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 = ff1.encrypt(fromHex(v.data)); - assert.deepStrictEqual(res, fromHex(v.exp), i); + 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`, () => { + deepStrictEqual(ff1.encrypt(v.X), v.AB); + }); + should(`NIST vector (${i}): decrypt`, () => { + deepStrictEqual(ff1.decrypt(v.AB), v.X); + }); } -}); -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 = ff1.decrypt(fromHex(v.exp)); - assert.deepStrictEqual(res, fromHex(v.data), i); - } + 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 = ff1.encrypt(fromHex(v.data)); + deepStrictEqual(res, fromHex(v.exp), i); + } + }); + + 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 = ff1.decrypt(fromHex(v.exp)); + deepStrictEqual(res, fromHex(v.data), i); + } + }); }); if (require.main === module) should.run(); diff --git a/test/index.js b/test/index.js index 88ccaa3..a25ce73 100644 --- a/test/index.js +++ b/test/index.js @@ -4,5 +4,6 @@ require('./arx.test.js'); require('./polyval.test.js'); require('./aes.test.js'); require('./ff1.test.js'); +require('./utils.test.js'); if (require.main === module) should.run(); diff --git a/test/utils.js b/test/utils.js index ab737d1..037254c 100644 --- a/test/utils.js +++ b/test/utils.js @@ -109,6 +109,12 @@ const pattern = (toByte, len) => Uint8Array.from({ length: len }, (i, j) => j % const jsonGZ = (path) => JSON.parse(zlib.gunzipSync(fs.readFileSync(`${__dirname}/${path}`))); +const unalign = (arr, len) => { + const n = new Uint8Array(arr.length + len); + n.set(arr, len); + return n.subarray(len); +}; + module.exports = { utf8ToBytes, hexToBytes, @@ -129,4 +135,5 @@ module.exports = { times, pattern, jsonGZ, + unalign, }; diff --git a/test/utils.test.js b/test/utils.test.js index 86b1d5a..a66642f 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,24 +1,21 @@ -const assert = require('assert'); -const { should } = require('micro-should'); -const { optional, integer, gen } = require('./generator'); +const { deepStrictEqual, throws } = require('assert'); +const { describe, should } = require('micro-should'); +const utils = require('./utils.js'); // Here goes test for tests... -should(`Test generator`, () => { - assert.deepStrictEqual( - gen({ - N: integer(0, 5), - b: integer(2, 7), - c: optional(integer(5, 10)), - }), - [ - { N: 0, b: 2, c: undefined }, - { N: 4, b: 3, c: 9 }, - { N: 3, b: 4, c: 8 }, - { N: 2, b: 5, c: 7 }, - { N: 1, b: 6, c: 6 }, - { N: 0, b: 2, c: 5 }, - ] - ); +describe('Tests', () => { + should('Unalign', () => { + const arr = new Uint8Array([1, 2, 3]); + for (let i = 0; i < 16; i++) { + const tmp = utils.unalign(arr, i); + deepStrictEqual(tmp, arr); + deepStrictEqual(tmp.byteOffset, i); + // check that it doesn't modify original + tmp[1] = 9; + deepStrictEqual(tmp, new Uint8Array([1, 9, 3])); + deepStrictEqual(arr, new Uint8Array([1, 2, 3])); + } + }); }); if (require.main === module) should.run(); diff --git a/tsconfig.esm.json b/tsconfig.esm.json index e205140..3e3b532 100644 --- a/tsconfig.esm.json +++ b/tsconfig.esm.json @@ -4,9 +4,19 @@ "outDir": "esm", "baseUrl": ".", "paths": { - "@noble/ciphers/crypto": ["src/crypto.ts"] - } + "@noble/ciphers/crypto": [ + "src/crypto.ts" + ] + }, + "declaration": true, + "declarationMap": true }, - "include": ["index.ts", "src"], - "exclude": ["node_modules", "lib"] + "include": [ + "index.ts", + "src" + ], + "exclude": [ + "node_modules", + "lib" + ] }