diff --git a/README.md b/README.md index 403452c..c2942f4 100644 --- a/README.md +++ b/README.md @@ -518,7 +518,7 @@ encrypt (8KB) ├─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 +└─aes-256-gcm-siv x 7,907 ops/sec @ 126μs/op encrypt (1MB) ├─xsalsa20poly1305 x 171 ops/sec @ 5ms/op ├─chacha20poly1305 x 186 ops/sec @ 5ms/op diff --git a/package-lock.json b/package-lock.json index f7d8681..5f879b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@noble/ciphers", - "version": "0.5.3", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@noble/ciphers", - "version": "0.5.3", + "version": "0.6.0", "license": "MIT", "devDependencies": { "@paulmillr/jsbt": "0.1.0", diff --git a/src/_arx.ts b/src/_arx.ts index 4b920bf..c6a653e 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, copyBytes } from './utils.js'; +import { bool as abool, bytes as abytes, number as anumber } from './_assert.js'; +import { XorStream, checkOpts, clean, copyBytes, u32 } from './utils.js'; /* RFC8439 requires multi-step cipher stream, where @@ -207,7 +207,7 @@ export function createCipher(core: CipherCoreFn, opts: CipherOpts): XorStream { } const n32 = u32(nonce); runCipher(core, sigma, k32, n32, data, output, counter, rounds); - for (const i of toClean) i.fill(0); + clean(...toClean) return output; }; } diff --git a/src/_micro.ts b/src/_micro.ts index e19b0d5..e33afc9 100644 --- a/src/_micro.ts +++ b/src/_micro.ts @@ -1,11 +1,14 @@ /*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) */ // prettier-ignore -import { - Cipher, XorStream, createView, setBigUint64, wrapCipher, - bytesToHex, concatBytes, equalBytes, hexToNumber, numberToBytesBE, -} from './utils.js'; import { createCipher, rotl } from './_arx.js'; import { bytes as abytes } from './_assert.js'; +import { + Cipher, XorStream, + bytesToHex, concatBytes, + createView, + equalBytes, hexToNumber, numberToBytesBE, + setBigUint64, wrapCipher, +} from './utils.js'; /* noble-ciphers-micro: more auditable, but slower version of salsa20, chacha & poly1305. @@ -73,7 +76,7 @@ function salsaCore( const y = new Uint32Array([ s[0], k[0], k[1], k[2], // "expa" Key Key Key k[3], s[1], n[0], n[1], // Key "nd 3" Nonce Nonce - cnt, 0 , s[2], k[4], // Pos. Pos. "2-by" Key + cnt, 0, s[2], k[4], // Pos. Pos. "2-by" Key k[5], k[6], k[7], s[3], // Key Key Key "te k" ]); const x = y.slice(); @@ -91,10 +94,10 @@ export function hsalsa(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint ]); salsaRound(x, 20); let oi = 0; - o32[oi++] = x[0]; o32[oi++] = x[5]; + o32[oi++] = x[0]; o32[oi++] = x[5]; o32[oi++] = x[10]; o32[oi++] = x[15]; - o32[oi++] = x[6]; o32[oi++] = x[7]; - o32[oi++] = x[8]; o32[oi++] = x[9]; + o32[oi++] = x[6]; o32[oi++] = x[7]; + o32[oi++] = x[8]; o32[oi++] = x[9]; } function chachaCore( @@ -110,7 +113,7 @@ function chachaCore( s[0], s[1], s[2], s[3], // "expa" "nd 3" "2-by" "te k" k[0], k[1], k[2], k[3], // Key Key Key Key k[4], k[5], k[6], k[7], // Key Key Key Key - cnt, n[0], n[1], n[2], // Counter Counter Nonce Nonce + cnt, n[0], n[1], n[2], // Counter Counter Nonce Nonce ]); const x = y.slice(); chachaRound(x, rounds); @@ -127,8 +130,8 @@ export function hchacha(s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uin ]); chachaRound(x, 20); let oi = 0; - o32[oi++] = x[0]; o32[oi++] = x[1]; - o32[oi++] = x[2]; o32[oi++] = x[3]; + o32[oi++] = x[0]; o32[oi++] = x[1]; + o32[oi++] = x[2]; o32[oi++] = x[3]; o32[oi++] = x[12]; o32[oi++] = x[13]; o32[oi++] = x[14]; o32[oi++] = x[15]; } @@ -282,30 +285,30 @@ export function secretbox(key: Uint8Array, nonce: Uint8Array) { export const _poly1305_aead = (fn: XorStream) => - (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => { - const tagLength = 16; - const keyLength = 32; - abytes(key, keyLength); - abytes(nonce); - return { - 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) { - abytes(ciphertext); - if (ciphertext.length < tagLength) - throw new Error(`encrypted data must be at least ${tagLength} bytes`); - const passedTag = ciphertext.subarray(-tagLength); - const data = ciphertext.subarray(0, -tagLength); - const tag = computeTag(fn, key, nonce, data, AAD); - if (!equalBytes(passedTag, tag)) throw new Error('invalid poly1305 tag'); - return fn(key, nonce, data, undefined, 1); - }, + (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): Cipher => { + const tagLength = 16; + const keyLength = 32; + abytes(key, keyLength); + abytes(nonce); + return { + 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) { + abytes(ciphertext); + if (ciphertext.length < tagLength) + throw new Error(`encrypted data must be at least ${tagLength} bytes`); + const passedTag = ciphertext.subarray(-tagLength); + const data = ciphertext.subarray(0, -tagLength); + const tag = computeTag(fn, key, nonce, data, AAD); + if (!equalBytes(passedTag, tag)) throw new Error('invalid poly1305 tag'); + return fn(key, nonce, data, undefined, 1); + }, + }; }; - }; /** * chacha20-poly1305 12-byte-nonce chacha. diff --git a/src/_poly1305.ts b/src/_poly1305.ts index 7005175..d45277b 100644 --- a/src/_poly1305.ts +++ b/src/_poly1305.ts @@ -1,5 +1,5 @@ -import { exists as aexists, bytes as abytes, output as aoutput } from './_assert.js'; -import { Input, toBytes, Hash } from './utils.js'; +import { bytes as abytes, exists as aexists, output as aoutput } from './_assert.js'; +import { Hash, Input, clean, toBytes } from './utils.js'; // Poly1305 is a fast and parallel secret-key message-authentication code. // https://cr.yp.to/mac.html, https://cr.yp.to/mac/poly1305-20050329.pdf @@ -214,7 +214,7 @@ class Poly1305 implements Hash { f = (((h[i] + pad[i]) | 0) + (f >>> 16)) | 0; h[i] = f & 0xffff; } - g.fill(0); + clean(g); } update(data: Input): this { aexists(this); @@ -222,7 +222,7 @@ class Poly1305 implements Hash { data = toBytes(data); const len = data.length; - for (let pos = 0; pos < len; ) { + for (let pos = 0; pos < len;) { const take = Math.min(blockLen - this.pos, len - pos); // Fast path: we have at least one block in input if (take === blockLen) { @@ -240,10 +240,7 @@ class Poly1305 implements Hash { return this; } destroy() { - this.h.fill(0); - this.r.fill(0); - this.buffer.fill(0); - this.pad.fill(0); + clean(this.h, this.r, this.buffer, this.pad); } digestInto(out: Uint8Array) { aexists(this); @@ -253,7 +250,6 @@ class Poly1305 implements Hash { let { pos } = this; if (pos) { buffer[pos++] = 1; - // buffer.subarray(pos).fill(0); for (; pos < 16; pos++) buffer[pos] = 0; this.process(buffer, 0, true); } diff --git a/src/_polyval.ts b/src/_polyval.ts index f8bd548..2cd89f0 100644 --- a/src/_polyval.ts +++ b/src/_polyval.ts @@ -1,5 +1,5 @@ -import { createView, toBytes, Input, Hash, u32, copyBytes } from './utils.js'; import { bytes as abytes, exists as aexists, output as aoutput } from './_assert.js'; +import { clean, copyBytes, createView, Hash, Input, toBytes, u32 } from './utils.js'; // GHash from AES-GCM and its little-endian "mirror image" Polyval from AES-SIV. // Implemented in terms of GHash with conversion function for keys @@ -148,7 +148,7 @@ class GHASH implements Hash { if (left) { ZEROS16.set(data.subarray(blocks * BLOCK_SIZE)); this._updateBlock(ZEROS32[0], ZEROS32[1], ZEROS32[2], ZEROS32[3]); - ZEROS32.fill(0); // clean tmp buffer + clean(ZEROS32); // clean tmp buffer } return this; } @@ -184,7 +184,7 @@ class Polyval extends GHASH { key = toBytes(key); const ghKey = _toGHASHKey(copyBytes(key)); super(ghKey, expectedLength); - ghKey.fill(0); + clean(ghKey); } update(data: Input): this { data = toBytes(data); @@ -208,7 +208,7 @@ class Polyval extends GHASH { swapLE(ZEROS32[1]), swapLE(ZEROS32[0]) ); - ZEROS32.fill(0); // clean tmp buffer + clean(ZEROS32); } return this; } diff --git a/src/aes.ts b/src/aes.ts index d37999f..e615085 100644 --- a/src/aes.ts +++ b/src/aes.ts @@ -1,18 +1,19 @@ // prettier-ignore +import { bytes as abytes } from './_assert.js'; +import { ghash, polyval } from './_polyval.js'; import { - wrapCipher, Cipher, CipherWithOutput, + clean, + copyBytes, createView, - setBigUint64, equalBytes, + isAligned32, + setBigUint64, u32, u8, - isAligned32, - copyBytes, + wrapCipher, } from './utils.js'; -import { ghash, polyval } from './_polyval.js'; -import { bytes as abytes } from './_assert.js'; /* AES (Advanced Encryption Standard) aka Rijndael block cipher. @@ -60,7 +61,7 @@ const sbox = /* @__PURE__ */ (() => { x |= x << 8; box[t[i]] = (x ^ (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63) & 0xff; } - t.fill(0); + clean(t); return box; })(); @@ -130,7 +131,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); + clean(...toClean) return xk; } @@ -144,7 +145,7 @@ export function expandKeyDecLE(key: Uint8Array): Uint32Array { for (let i = 0; i < Nk; i += 4) { for (let j = 0; j < 4; j++) xk[i + j] = encKey[Nk - i - 4 + j]; } - encKey.fill(0); + clean(encKey); // apply InvMixColumn except first & last round for (let i = 4; i < Nk - 4; i++) { const x = xk[i]; @@ -259,7 +260,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); + clean(b32); } return dst; } @@ -303,7 +304,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); + clean(b32); } return dst; } @@ -328,7 +329,7 @@ export const ctr = wrapCipher( const toClean = [xk, n]; if (!isAligned32(buf)) toClean.push((buf = copyBytes(buf))); const out = ctrCounter(xk, n, buf, dst); - for (const i of toClean) i.fill(0); + clean(...toClean) return out; } return { @@ -402,7 +403,7 @@ export const ecb = wrapCipher( const { b, o, out: _out } = validateBlockEncrypt(plaintext, pcks5, dst); const xk = expandKeyLE(key); let i = 0; - for (; i + 4 <= b.length; ) { + 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); } @@ -411,7 +412,7 @@ export const ecb = wrapCipher( 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); + clean(xk); return _out; }, decrypt(ciphertext: Uint8Array, dst?: Uint8Array) { @@ -422,11 +423,11 @@ export const ecb = wrapCipher( if (!isAligned32(ciphertext)) toClean.push((ciphertext = copyBytes(ciphertext))); const b = u32(ciphertext); const o = u32(out); - for (let i = 0; i + 4 <= b.length; ) { + 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); } - for (const i of toClean) i.fill(0); + clean(...toClean) return validatePCKS(out, pcks5); }, }; @@ -454,7 +455,7 @@ export const cbc = wrapCipher( // prettier-ignore let s0 = n32[0], s1 = n32[1], s2 = n32[2], s3 = n32[3]; let i = 0; - for (; i + 4 <= b.length; ) { + 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); @@ -465,7 +466,7 @@ 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); } - for (const i of toClean) i.fill(0); + clean(...toClean) return _out; }, decrypt(ciphertext: Uint8Array, dst?: Uint8Array) { @@ -481,14 +482,14 @@ export const cbc = wrapCipher( 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; ) { + 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); } - for (const i of toClean) i.fill(0); + clean(...toClean) return validatePCKS(out, pcks5); }, }; @@ -519,7 +520,7 @@ export const cfb = wrapCipher( 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; ) { + for (let i = 0; i + 4 <= src32.length;) { const { s0: e0, s1: e1, s2: e2, s3: e3 } = encrypt(xk, s0, s1, s2, s3); dst32[i + 0] = src32[i + 0] ^ e0; dst32[i + 1] = src32[i + 1] ^ e1; @@ -533,9 +534,9 @@ export const cfb = wrapCipher( ({ s0, s1, s2, s3 } = encrypt(xk, s0, s1, s2, s3)); const buf = u8(new Uint32Array([s0, s1, s2, s3])); for (let i = start, pos = 0; i < srcLen; i++, pos++) dst[i] = src[i] ^ buf[pos]; - buf.fill(0); + clean(buf); } - for (const i of toClean) i.fill(0); + clean(...toClean) return dst; } return { @@ -562,7 +563,7 @@ function computeTag( setBigUint64(view, 8, BigInt(data.length * 8), isLE); h.update(num); const res = h.digest(); - num.fill(0); + clean(num); return res; } @@ -617,7 +618,7 @@ export const gcm = wrapCipher( const tag = _computeTag(authKey, tagMask, out.subarray(0, out.length - tagLength)); toClean.push(tag); out.set(tag, plaintext.length); - for (const i of toClean) i.fill(0); + clean(...toClean) return out; }, decrypt(ciphertext: Uint8Array) { @@ -633,7 +634,7 @@ export const gcm = wrapCipher( toClean.push(tag); if (!equalBytes(tag, passedTag)) throw new Error('aes/gcm: invalid ghash tag'); const out = ctr32(xk, false, counter, data); - for (const i of toClean) i.fill(0); + clean(...toClean) return out; }, }; @@ -690,7 +691,7 @@ export const siv = wrapCipher( } const res = { authKey, encKey: expandKeyLE(encKey) }; // Cleanup - for (const i of toClean) i.fill(0); + clean(...toClean) return res; } function _computeTag(encKey: Uint32Array, authKey: Uint8Array, data: Uint8Array) { @@ -714,7 +715,7 @@ export const siv = wrapCipher( block[15] |= 0x80; // Force highest bit const res = ctr32(encKey, true, block, input); // Cleanup - block.fill(0); + clean(block); return res; } return { @@ -729,7 +730,7 @@ export const siv = wrapCipher( out.set(tag, plaintext.length); out.set(processSiv(encKey, tag, plaintext)); // Cleanup - for (const i of toClean) i.fill(0); + clean(...toClean) return out; }, decrypt(ciphertext: Uint8Array) { @@ -743,11 +744,11 @@ export const siv = wrapCipher( const expectedTag = _computeTag(encKey, authKey, plaintext); toClean.push(expectedTag); if (!equalBytes(tag, expectedTag)) { - for (const i of toClean) i.fill(0); + clean(...toClean) throw new Error('invalid polyval tag'); } // Cleanup - for (const i of toClean) i.fill(0); + clean(...toClean) return plaintext; }, }; diff --git a/src/chacha.ts b/src/chacha.ts index eebc5aa..18223c1 100644 --- a/src/chacha.ts +++ b/src/chacha.ts @@ -1,10 +1,13 @@ // prettier-ignore -import { - wrapCipher, CipherWithOutput, XorStream, createView, equalBytes, setBigUint64, -} from './utils.js'; -import { poly1305 } from './_poly1305.js'; import { createCipher, rotl } from './_arx.js'; import { bytes as abytes } from './_assert.js'; +import { poly1305 } from './_poly1305.js'; +import { + CipherWithOutput, XorStream, + clean, + createView, equalBytes, setBigUint64, + wrapCipher, +} from './utils.js'; // ChaCha20 stream cipher was released in 2008. ChaCha aims to increase // the diffusion per round, but had slightly less cryptanalysis. @@ -18,14 +21,14 @@ function chachaCore( s: Uint32Array, k: Uint32Array, n: Uint32Array, out: Uint32Array, cnt: number, rounds = 20 ): void { let y00 = s[0], y01 = s[1], y02 = s[2], y03 = s[3], // "expa" "nd 3" "2-by" "te k" - y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key - y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key - y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce + y04 = k[0], y05 = k[1], y06 = k[2], y07 = k[3], // Key Key Key Key + y08 = k[4], y09 = k[5], y10 = k[6], y11 = k[7], // Key Key Key Key + y12 = cnt, y13 = n[0], y14 = n[1], y15 = n[2]; // Counter Counter Nonce Nonce // Save state to temporary variables let x00 = y00, x01 = y01, x02 = y02, x03 = y03, - x04 = y04, x05 = y05, x06 = y06, x07 = y07, - x08 = y08, x09 = y09, x10 = y10, x11 = y11, - x12 = y12, x13 = y13, x14 = y14, x15 = y15; + x04 = y04, x05 = y05, x06 = y06, x07 = y07, + x08 = y08, x09 = y09, x10 = y10, x11 = y11, + x12 = y12, x13 = y13, x14 = y14, x15 = y15; for (let r = 0; r < rounds; r += 2) { x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16); x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12); @@ -39,7 +42,7 @@ function chachaCore( x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 16); x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 12); - x02 = (x02 + x06) | 0; x14 = rotl(x14 ^x02, 8); + x02 = (x02 + x06) | 0; x14 = rotl(x14 ^ x02, 8); x10 = (x10 + x14) | 0; x06 = rotl(x06 ^ x10, 7); x03 = (x03 + x07) | 0; x15 = rotl(x15 ^ x03, 16); @@ -89,9 +92,9 @@ export function hchacha( s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array ) { let x00 = s[0], x01 = s[1], x02 = s[2], x03 = s[3], - x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], - x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], - x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3]; + x04 = k[0], x05 = k[1], x06 = k[2], x07 = k[3], + x08 = k[4], x09 = k[5], x10 = k[6], x11 = k[7], + x12 = i[0], x13 = i[1], x14 = i[2], x15 = i[3]; for (let r = 0; r < 20; r += 2) { x00 = (x00 + x04) | 0; x12 = rotl(x12 ^ x00, 16); x08 = (x08 + x12) | 0; x04 = rotl(x04 ^ x08, 12); @@ -213,8 +216,7 @@ function computeTag( setBigUint64(view, 8, BigInt(data.length), true); h.update(num); const res = h.digest(); - authKey.fill(0); - num.fill(0); + clean(authKey, num); return res; } @@ -229,45 +231,45 @@ function computeTag( */ export const _poly1305_aead = (xorStream: XorStream) => - (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): CipherWithOutput => { - const tagLength = 16; - abytes(key, 32); - abytes(nonce); - return { - encrypt(plaintext: Uint8Array, output?: Uint8Array) { - const plength = plaintext.length; - const clength = plength + tagLength; - if (output) { - abytes(output, clength); - } else { - output = new Uint8Array(clength); - } - 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) { - const clength = ciphertext.length; - const plength = clength - tagLength; - if (clength < tagLength) - throw new Error(`encrypted data must be at least ${tagLength} bytes`); - if (output) { - abytes(output, plength); - } else { - output = new Uint8Array(plength); - } - const data = ciphertext.subarray(0, -tagLength); - const passedTag = ciphertext.subarray(-tagLength); - 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; - }, + (key: Uint8Array, nonce: Uint8Array, AAD?: Uint8Array): CipherWithOutput => { + const tagLength = 16; + abytes(key, 32); + abytes(nonce); + return { + encrypt(plaintext: Uint8Array, output?: Uint8Array) { + const plength = plaintext.length; + const clength = plength + tagLength; + if (output) { + abytes(output, clength); + } else { + output = new Uint8Array(clength); + } + xorStream(key, nonce, plaintext, output, 1); + const tag = computeTag(xorStream, key, nonce, output.subarray(0, -tagLength), AAD); + output.set(tag, plength); // append tag + clean(tag); + return output; + }, + decrypt(ciphertext: Uint8Array, output?: Uint8Array) { + const clength = ciphertext.length; + const plength = clength - tagLength; + if (clength < tagLength) + throw new Error(`encrypted data must be at least ${tagLength} bytes`); + if (output) { + abytes(output, plength); + } else { + output = new Uint8Array(plength); + } + const data = ciphertext.subarray(0, -tagLength); + const passedTag = ciphertext.subarray(-tagLength); + const tag = computeTag(xorStream, key, nonce, data, AAD); + if (!equalBytes(passedTag, tag)) throw new Error('invalid tag'); + xorStream(key, nonce, data, output, 1); + clean(tag); + return output; + }, + }; }; - }; /** * ChaCha20-Poly1305 from RFC 8439. diff --git a/src/ff1.ts b/src/ff1.ts index 1e3c579..788d595 100644 --- a/src/ff1.ts +++ b/src/ff1.ts @@ -1,5 +1,5 @@ -import { Cipher, bytesToNumberBE, numberToBytesBE } from './utils.js'; import { unsafe } from './aes.js'; +import { Cipher, bytesToNumberBE, clean, numberToBytesBE } from './utils.js'; // NOTE: no point in inlining encrypt instead of encryptBlock, since BigInt stuff will be slow const { expandKeyLE, encryptBlock } = unsafe; @@ -45,7 +45,7 @@ function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: number[] // Q = T || [0](−t−b−1) mod 16 || [i]1 || [NUMradix(B)]b. const PQ = new Uint8Array(P.length + tweak.length + padding + 1 + b); PQ.set(P); - P.fill(0); + clean(P); PQ.set(tweak, P.length); const xk = expandKeyLE(key); const round = (A: number[], B: number[], i: number, decrypt = false) => { @@ -80,8 +80,7 @@ function getRound(radix: number, key: Uint8Array, tweak: Uint8Array, x: number[] return [A, B]; }; const destroy = () => { - xk.fill(0); - PQ.fill(0); + clean(xk, PQ); }; return { u, round, destroy }; } diff --git a/src/salsa.ts b/src/salsa.ts index 852d965..a9d5d63 100644 --- a/src/salsa.ts +++ b/src/salsa.ts @@ -1,7 +1,7 @@ -import { bytes as abytes } from './_assert.js'; import { createCipher, rotl } from './_arx.js'; +import { bytes as abytes } from './_assert.js'; import { poly1305 } from './_poly1305.js'; -import { wrapCipher, Cipher, equalBytes } from './utils.js'; +import { Cipher, clean, equalBytes, wrapCipher } from './utils.js'; // Salsa20 stream cipher was released in 2005. // Salsa's goal was to implement AES replacement that does not rely on S-Boxes, @@ -17,30 +17,30 @@ function salsaCore( ): void { // Based on https://cr.yp.to/salsa20.html let y00 = s[0], y01 = k[0], y02 = k[1], y03 = k[2], // "expa" Key Key Key - y04 = k[3], y05 = s[1], y06 = n[0], y07 = n[1], // Key "nd 3" Nonce Nonce - y08 = cnt, y09 = 0 , y10 = s[2], y11 = k[4], // Pos. Pos. "2-by" Key - y12 = k[5], y13 = k[6], y14 = k[7], y15 = s[3]; // Key Key Key "te k" + y04 = k[3], y05 = s[1], y06 = n[0], y07 = n[1], // Key "nd 3" Nonce Nonce + y08 = cnt, y09 = 0, y10 = s[2], y11 = k[4], // Pos. Pos. "2-by" Key + y12 = k[5], y13 = k[6], y14 = k[7], y15 = s[3]; // Key Key Key "te k" // Save state to temporary variables let x00 = y00, x01 = y01, x02 = y02, x03 = y03, - x04 = y04, x05 = y05, x06 = y06, x07 = y07, - x08 = y08, x09 = y09, x10 = y10, x11 = y11, - x12 = y12, x13 = y13, x14 = y14, x15 = y15; + x04 = y04, x05 = y05, x06 = y06, x07 = y07, + x08 = y08, x09 = y09, x10 = y10, x11 = y11, + x12 = y12, x13 = y13, x14 = y14, x15 = y15; for (let r = 0; r < rounds; r += 2) { - x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9); + x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9); x12 ^= rotl(x08 + x04 | 0, 13); x00 ^= rotl(x12 + x08 | 0, 18); - x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9); + x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9); x01 ^= rotl(x13 + x09 | 0, 13); x05 ^= rotl(x01 + x13 | 0, 18); - x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9); + x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9); x06 ^= rotl(x02 + x14 | 0, 13); x10 ^= rotl(x06 + x02 | 0, 18); - x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9); + x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9); x11 ^= rotl(x07 + x03 | 0, 13); x15 ^= rotl(x11 + x07 | 0, 18); - x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9); + x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9); x03 ^= rotl(x02 + x01 | 0, 13); x00 ^= rotl(x03 + x02 | 0, 18); - x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9); + x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9); x04 ^= rotl(x07 + x06 | 0, 13); x05 ^= rotl(x04 + x07 | 0, 18); - x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9); + x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9); x09 ^= rotl(x08 + x11 | 0, 13); x10 ^= rotl(x09 + x08 | 0, 18); - x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9); + x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9); x14 ^= rotl(x13 + x12 | 0, 13); x15 ^= rotl(x14 + x13 | 0, 18); } // Write output @@ -66,25 +66,25 @@ export function hsalsa( s: Uint32Array, k: Uint32Array, i: Uint32Array, o32: Uint32Array ) { let x00 = s[0], x01 = k[0], x02 = k[1], x03 = k[2], - x04 = k[3], x05 = s[1], x06 = i[0], x07 = i[1], - x08 = i[2], x09 = i[3], x10 = s[2], x11 = k[4], - x12 = k[5], x13 = k[6], x14 = k[7], x15 = s[3]; + x04 = k[3], x05 = s[1], x06 = i[0], x07 = i[1], + x08 = i[2], x09 = i[3], x10 = s[2], x11 = k[4], + x12 = k[5], x13 = k[6], x14 = k[7], x15 = s[3]; for (let r = 0; r < 20; r += 2) { - x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9); + x04 ^= rotl(x00 + x12 | 0, 7); x08 ^= rotl(x04 + x00 | 0, 9); x12 ^= rotl(x08 + x04 | 0, 13); x00 ^= rotl(x12 + x08 | 0, 18); - x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9); + x09 ^= rotl(x05 + x01 | 0, 7); x13 ^= rotl(x09 + x05 | 0, 9); x01 ^= rotl(x13 + x09 | 0, 13); x05 ^= rotl(x01 + x13 | 0, 18); - x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9); + x14 ^= rotl(x10 + x06 | 0, 7); x02 ^= rotl(x14 + x10 | 0, 9); x06 ^= rotl(x02 + x14 | 0, 13); x10 ^= rotl(x06 + x02 | 0, 18); - x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9); + x03 ^= rotl(x15 + x11 | 0, 7); x07 ^= rotl(x03 + x15 | 0, 9); x11 ^= rotl(x07 + x03 | 0, 13); x15 ^= rotl(x11 + x07 | 0, 18); - x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9); + x01 ^= rotl(x00 + x03 | 0, 7); x02 ^= rotl(x01 + x00 | 0, 9); x03 ^= rotl(x02 + x01 | 0, 13); x00 ^= rotl(x03 + x02 | 0, 18); - x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9); + x06 ^= rotl(x05 + x04 | 0, 7); x07 ^= rotl(x06 + x05 | 0, 9); x04 ^= rotl(x07 + x06 | 0, 13); x05 ^= rotl(x04 + x07 | 0, 18); - x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9); + x11 ^= rotl(x10 + x09 | 0, 7); x08 ^= rotl(x11 + x10 | 0, 9); x09 ^= rotl(x08 + x11 | 0, 13); x10 ^= rotl(x09 + x08 | 0, 18); - x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9); + x12 ^= rotl(x15 + x14 | 0, 7); x13 ^= rotl(x12 + x15 | 0, 9); x14 ^= rotl(x13 + x12 | 0, 13); x15 ^= rotl(x14 + x13 | 0, 18); } let oi = 0; @@ -140,9 +140,7 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( 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); - // Cleanup - tag.fill(0); + clean(output.subarray(0, tagLength), tag); return output.subarray(tagLength); }, decrypt(ciphertext: Uint8Array, output?: Uint8Array) { @@ -164,15 +162,14 @@ export const xsalsa20poly1305 = /* @__PURE__ */ wrapCipher( // Separate call to calculate authkey, since first bytes contains tag // Here we use first 32 bytes for authKey const authKeyBuf = output.subarray(0, 32); - authKeyBuf.fill(0); + clean(authKeyBuf); 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); + clean(output.subarray(0, 32 + 16), tag); return output.subarray(32 + 16); }, }; diff --git a/src/utils.ts b/src/utils.ts index b068d15..903970f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -262,6 +262,8 @@ export function copyBytes(bytes: Uint8Array) { return Uint8Array.from(bytes); } -export function clean(bytes: Uint8Array) { - bytes.fill(0); +export function clean(...arrays: TypedArray[]) { + for (let i = 0; i < arrays.length; i++) { + arrays[i].fill(0); + } } diff --git a/src/webcrypto.ts b/src/webcrypto.ts index 96e0d7e..b7f9468 100644 --- a/src/webcrypto.ts +++ b/src/webcrypto.ts @@ -7,8 +7,8 @@ // // Use full path so that Node.js can rewrite it to `cryptoNode.js`. import { crypto } from '@noble/ciphers/crypto'; +import { bytes as abytes, number } from './_assert.js'; import { AsyncCipher, Cipher, concatBytes } from './utils.js'; -import { number, bytes as abytes } from './_assert.js'; /** * Secure PRNG. Uses `crypto.getRandomValues`, which defers to OS.