diff --git a/README.md b/README.md index 2043f68..29085ff 100644 --- a/README.md +++ b/README.md @@ -42,24 +42,36 @@ If you don't like NPM, a standalone ```js // import * from '@noble/ciphers'; // Error // Use sub-imports for tree-shaking, to ensure small size of your apps -import { xsalsa20_poly1305, secretbox } from '@noble/ciphers/salsa'; -import { chacha20_poly1305 } from '@noble/ciphers/chacha'; -import { randomBytes } from '@noble/ciphers/webcrypto/utils'; +import { xsalsa20_poly1305 } from '@noble/ciphers/salsa'; import { utf8ToBytes } from '@noble/ciphers/utils'; +import { randomBytes } from '@noble/ciphers/webcrypto/utils'; const key = randomBytes(32); +const nonce = randomBytes(24); const data = utf8ToBytes('hello, noble'); // strings must be converted to Uint8Array -const nonce24 = randomBytes(24); -const stream_s = xsalsa20_poly1305(key, nonce24); +// XSalsa20 +const stream_s = xsalsa20_poly1305(key, nonce); const encrypted_s = stream_s.encrypt(data); stream_s.decrypt(encrypted_s); // === data +// XChaCha +import { xchacha20_poly1305 } from '@noble/ciphers/salsa'; +const stream_xc = xchacha20_poly1305(key, nonce); +const encrypted_xc = stream_xc.encrypt(data); +stream_xc.decrypt(encrypted_xc); // === data + +// ChaCha +import { chacha20_poly1305 } from '@noble/ciphers/chacha'; const nonce12 = randomBytes(12); const stream_c = chacha20_poly1305(key, nonce12); const encrypted_c = stream_c.encrypt(data); stream_c.decrypt(encrypted_c); // === data +``` + +##### AES usage +``` import { aes_128_gcm, aes_128_ctr, aes_128_cbc, aes_256_gcm, aes_256_ctr, aes_256_cbc @@ -314,57 +326,55 @@ The library is experimental. Use at your own risk. ## Speed +To summarize, noble is the fastest JS implementation. + Benchmark results on Apple M2 with node v20: ``` -encrypt (32B) -├─salsa x 1,210,653 ops/sec @ 826ns/op -├─chacha x 1,440,922 ops/sec @ 694ns/op -├─xsalsa x 846,023 ops/sec @ 1μs/op -├─xchacha x 842,459 ops/sec @ 1μs/op -├─xsalsa20_poly1305 x 562,746 ops/sec @ 1μs/op -├─chacha20_poly1305 x 468,603 ops/sec @ 2μs/op -└─xchacha20poly1305 x 311,623 ops/sec @ 3μs/op - encrypt (64B) -├─salsa x 1,310,615 ops/sec @ 763ns/op -├─chacha x 1,577,287 ops/sec @ 634ns/op -├─xsalsa x 864,304 ops/sec @ 1μs/op -├─xchacha x 862,068 ops/sec @ 1μs/op -├─xsalsa20_poly1305 x 481,000 ops/sec @ 2μs/op -├─chacha20_poly1305 x 446,627 ops/sec @ 2μs/op -└─xchacha20poly1305 x 302,480 ops/sec @ 3μs/op - +├─xsalsa20_poly1305 x 484,966 ops/sec @ 2μs/op +├─chacha20_poly1305 x 442,282 ops/sec @ 2μs/op +└─xchacha20poly1305 x 300,842 ops/sec @ 3μs/op encrypt (1KB) -├─salsa x 356,506 ops/sec @ 2μs/op -├─chacha x 380,952 ops/sec @ 2μs/op -├─xsalsa x 312,891 ops/sec @ 3μs/op -├─xchacha x 318,674 ops/sec @ 3μs/op -├─xsalsa20_poly1305 x 143,864 ops/sec @ 6μs/op -├─chacha20_poly1305 x 141,703 ops/sec @ 7μs/op -└─xchacha20poly1305 x 122,895 ops/sec @ 8μs/op - +├─xsalsa20_poly1305 x 143,905 ops/sec @ 6μs/op +├─chacha20_poly1305 x 141,663 ops/sec @ 7μs/op +└─xchacha20poly1305 x 122,639 ops/sec @ 8μs/op encrypt (8KB) -├─salsa x 56,170 ops/sec @ 17μs/op -├─chacha x 57,997 ops/sec @ 17μs/op -├─xsalsa x 54,758 ops/sec @ 18μs/op -├─xchacha x 56,085 ops/sec @ 17μs/op -├─xsalsa20_poly1305 x 23,203 ops/sec @ 43μs/op -├─chacha20_poly1305 x 23,482 ops/sec @ 42μs/op -└─xchacha20poly1305 x 22,900 ops/sec @ 43μs/op - +├─xsalsa20_poly1305 x 23,373 ops/sec @ 42μs/op +├─chacha20_poly1305 x 23,683 ops/sec @ 42μs/op +└─xchacha20poly1305 x 23,066 ops/sec @ 43μs/op encrypt (1MB) -├─salsa x 462 ops/sec @ 2ms/op -├─chacha x 473 ops/sec @ 2ms/op -├─xsalsa x 463 ops/sec @ 2ms/op -├─xchacha x 474 ops/sec @ 2ms/op -├─xsalsa20_poly1305 x 190 ops/sec @ 5ms/op -├─chacha20_poly1305 x 193 ops/sec @ 5ms/op -└─xchacha20poly1305 x 192 ops/sec @ 5ms/op +├─xsalsa20_poly1305 x 193 ops/sec @ 5ms/op +├─chacha20_poly1305 x 196 ops/sec @ 5ms/op +└─xchacha20poly1305 x 195 ops/sec @ 5ms/op +``` + +Unauthenticated encryption: +``` +encrypt (64B) +├─salsa x 1,272,264 ops/sec @ 786ns/op +├─chacha x 1,526,717 ops/sec @ 655ns/op +├─xsalsa x 847,457 ops/sec @ 1μs/op +└─xchacha x 848,896 ops/sec @ 1μs/op +encrypt (1KB) +├─salsa x 355,492 ops/sec @ 2μs/op +├─chacha x 377,358 ops/sec @ 2μs/op +├─xsalsa x 311,915 ops/sec @ 3μs/op +└─xchacha x 315,457 ops/sec @ 3μs/op +encrypt (8KB) +├─salsa x 56,063 ops/sec @ 17μs/op +├─chacha x 57,359 ops/sec @ 17μs/op +├─xsalsa x 54,848 ops/sec @ 18μs/op +└─xchacha x 55,475 ops/sec @ 18μs/op +encrypt (1MB) +├─salsa x 465 ops/sec @ 2ms/op +├─chacha x 474 ops/sec @ 2ms/op +├─xsalsa x 466 ops/sec @ 2ms/op +└─xchacha x 476 ops/sec @ 2ms/op ``` -Compare to other implementations (slow is `_micro.ts`): +Compare to other implementations: ``` xsalsa20_poly1305 (encrypt, 1MB) diff --git a/benchmark/aead.js b/benchmark/aead.js new file mode 100644 index 0000000..0a8ccc2 --- /dev/null +++ b/benchmark/aead.js @@ -0,0 +1,180 @@ +import { deepStrictEqual } from 'assert'; +import { compare, utils as butils } from 'micro-bmark'; +import { createCipheriv, createDecipheriv } from 'node:crypto'; + +import { concatBytes } from '@noble/ciphers/utils'; +import { xchacha20_poly1305, chacha20_poly1305 } from '@noble/ciphers/chacha'; +import { xsalsa20_poly1305 } from '@noble/ciphers/salsa'; +import * as micro from '@noble/ciphers/_micro'; + +import { ChaCha20Poly1305 as StableChachaPoly } from '@stablelib/chacha20poly1305'; +import { XChaCha20Poly1305 as StableXchachaPoly } from '@stablelib/xchacha20poly1305'; +import { default as tweetnacl } from 'tweetnacl'; // secretbox = xsalsa20-poly1305. +import { + ChaCha20Poly1305 as ChsfChachaPoly, + newInstance as chainsafe_init_wasm, +} from '@chainsafe/as-chacha20poly1305'; + +const ONLY_NOBLE = process.argv[2] === 'noble'; +const buf = (n) => new Uint8Array(n).fill(n); +// buffer title, sample count, data +const buffers = { + '32B': [500000, buf(32)], + '64B': [500000, buf(64)], + '1KB': [150000, buf(1024)], + '8KB': [20000, buf(1024 * 8)], + '1MB': [500, buf(1024 * 1024)], +}; + +let chainsafe_chacha_poly; + +export const CIPHERS = { + xsalsa20_poly1305: { + opts: { key: buf(32), nonce: buf(24) }, + tweetnacl: { + encrypt: (buf, opts) => tweetnacl.secretbox(buf, opts.nonce, opts.key), + decrypt: (buf, opts) => tweetnacl.secretbox.open(buf, opts.nonce, opts.key), + }, + noble: { + encrypt: (buf, opts) => xsalsa20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => xsalsa20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + micro: { + encrypt: (buf, opts) => micro.xsalsa20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => micro.xsalsa20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + }, + chacha20_poly1305: { + opts: { key: buf(32), nonce: buf(12) }, + node: { + encrypt: (buf, opts) => { + const c = createCipheriv('chacha20-poly1305', opts.key, opts.nonce); + const res = []; + res.push(c.update(buf)); + res.push(c.final()); + res.push(c.getAuthTag()); + return concatBytes(...res.map((i) => Uint8Array.from(i))); + }, + decrypt: (buf, opts) => { + const ciphertext = buf.slice(0, -16); + const authTag = buf.slice(-16); + const decipher = createDecipheriv('chacha20-poly1305', opts.key, opts.nonce); + decipher.setAuthTag(authTag); + return concatBytes( + ...[decipher.update(ciphertext), decipher.final()].map((i) => Uint8Array.from(i)) + ); + }, + }, + stable: { + encrypt: (buf, opts) => new StableChachaPoly(opts.key).seal(opts.nonce, buf), + decrypt: (buf, opts) => new StableChachaPoly(opts.key).open(opts.nonce, buf), + }, + chainsafe: { + encrypt: (buf, opts) => { + return chainsafe_chacha_poly.seal(opts.key, opts.nonce, buf); + }, + decrypt: (buf, opts) => { + return chainsafe_chacha_poly.open(opts.key, opts.nonce, buf); + }, + }, + noble: { + encrypt: (buf, opts) => chacha20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => chacha20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + micro: { + encrypt: (buf, opts) => micro.chacha20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => micro.chacha20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + }, + xchacha20poly1305: { + opts: { key: buf(32), nonce: buf(24) }, + stable: { + encrypt: (buf, opts) => new StableXchachaPoly(opts.key).seal(opts.nonce, buf), + decrypt: (buf, opts) => new StableXchachaPoly(opts.key).open(opts.nonce, buf), + }, + noble: { + encrypt: (buf, opts) => xchacha20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => xchacha20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + micro: { + encrypt: (buf, opts) => micro.xchacha20_poly1305(opts.key, opts.nonce).encrypt(buf), + decrypt: (buf, opts) => micro.xchacha20_poly1305(opts.key, opts.nonce).decrypt(buf), + }, + }, +}; + +async function validate() { + // Verify that things we bench actually work + const bufs = [...Object.entries(buffers).map((i) => i[1][1])]; + // Verify different buffer sizes + for (let i = 0; i < 2048; i++) bufs.push(buf(i)); + // Verify different subarrays positions + const b2 = buf(2048); + //for (let i = 0; i < 2048; i++) bufs.push(b2.subarray(i)); + for (const buf of bufs) { + const b = buf.slice(); + // ciphers + for (let [k, libs] of Object.entries(CIPHERS)) { + let encrypted; + for (const [lib, fn] of Object.entries(libs)) { + if (lib === 'opts') continue; + if (encrypted === undefined) encrypted = await fn.encrypt(buf, libs.opts); + else { + const cur = await fn.encrypt(buf, libs.opts); + deepStrictEqual(encrypted, cur, `encrypt verify (${lib})`); + } + deepStrictEqual(buf, b, `encrypt mutates buffer (${lib})`); + const res = await fn.decrypt(encrypted, libs.opts); + deepStrictEqual(res, buf, `decrypt verify (${lib})`); + } + } + } + console.log('VALIDATED'); +} + +export const main = () => + (async () => { + const ctx = chainsafe_init_wasm(); + chainsafe_chacha_poly = new ChsfChachaPoly(ctx); + await validate(); + if (ONLY_NOBLE) { + // Benchmark different noble-ciphers + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const c = Object.entries(CIPHERS) + .map(([k, lib]) => [k, lib.noble, lib.opts]) + .filter(([k, noble, _]) => !!noble); + await compare( + `encrypt (${size})`, + samples, + Object.fromEntries(c.map(([k, noble, opts]) => [k, () => noble.encrypt(buf, opts)])) + ); + } + return; + } + // Benchmark against other libraries + for (let [k, libs] of Object.entries(CIPHERS)) { + console.log(`==== ${k} ====`); + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const l = Object.entries(libs).filter(([lib, _]) => lib !== 'opts'); + await compare( + `${k} (encrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.encrypt(buf, libs.opts)])) + ); + const encrypted = await l[0][1].encrypt(buf, libs.opts); + await compare( + `${k} (decrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.decrypt(encrypted, libs.opts)])) + ); + } + } + // Log current RAM + butils.logMem(); + })(); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/benchmark/aes.js b/benchmark/aes.js new file mode 100644 index 0000000..d6f5a49 --- /dev/null +++ b/benchmark/aes.js @@ -0,0 +1,163 @@ +import { deepStrictEqual } from 'assert'; +import { compare, utils as butils } from 'micro-bmark'; +import { createCipheriv, createDecipheriv } from 'node:crypto'; + +import { aes_256_gcm } from '@noble/ciphers/webcrypto/aes'; +// import { aes_256_gcm_siv } from '@noble/ciphers/webcrypto/siv'; +import { concatBytes } from '@noble/ciphers/utils'; + +const ONLY_NOBLE = process.argv[2] === 'noble'; +const buf = (n) => new Uint8Array(n).fill(n); + +// Works for gcm only? +const nodeGCM = (name) => { + return { + encrypt: (buf, opts) => { + const res = [opts.iv]; + const c = createCipheriv(name, opts.key, opts.iv); + if (opts.aad) c.setAAD(opts.aad); + res.push(c.update(buf)); + res.push(c.final()); + res.push(c.getAuthTag()); + return concatBytes(...res.map((i) => Uint8Array.from(i))); + }, + decrypt: (buf, opts) => { + const ciphertext = buf.slice(12, -16); + const authTag = buf.slice(-16); + const decipher = createDecipheriv(name, opts.key, opts.iv); + if (opts.aad) c.setAAD(opts.aad); + decipher.setAuthTag(authTag); + return concatBytes( + ...[decipher.update(ciphertext), decipher.final()].map((i) => Uint8Array.from(i)) + ); + }, + }; +}; + +const cipherSame = (fn) => ({ encrypt: fn, decrypt: fn }); + +export const CIPHERS = { + // TODO: why this is so slow? + 'gcm-256': { + opts: { key: buf(32), iv: buf(12) }, + node: nodeGCM('aes-256-gcm'), + noble: { + encrypt: (buf, opts) => aes_256_gcm.encrypt(opts.key, buf, opts.iv), + decrypt: (buf, opts) => aes_256_gcm.decrypt(opts.key, buf, opts.iv), + }, + }, + 'gcm-siv-128': { + opts: { key: buf(16), aad: buf(0), nonce: buf(12) }, + noble: { + encrypt: async (buf, opts) => + await (await aes_256_gcm_siv(opts.key, opts.nonce, opts.aad)).encrypt(buf), + decrypt: async (buf, opts) => + await (await aes_256_gcm_siv(opts.key, opts.nonce, opts.aad)).decrypt(buf), + }, + }, + 'gcm-siv-256': { + opts: { key: buf(32), aad: buf(0), nonce: buf(12) }, + noble: { + encrypt: async (buf, opts) => + await (await aes_256_gcm_siv(opts.key, opts.nonce, opts.aad)).encrypt(buf), + decrypt: async (buf, opts) => + await (await aes_256_gcm_siv(opts.key, opts.nonce, opts.aad)).decrypt(buf), + }, + }, +}; + +// buffer title, sample count, data +const buffers = { + '32B': [2000000, buf(32)], + '64B': [1000000, buf(64)], + '1KB': [66667, buf(1024)], + '8KB': [8333, buf(1024 * 8)], + '1MB': [524, buf(1024 * 1024)], +}; + +async function validate() { + // Verify that things we bench actually work + const bufs = [...Object.entries(buffers).map((i) => i[1][1])]; + // Verify different buffer sizes + for (let i = 0; i < 2048; i++) bufs.push(buf(i)); + // Verify different subarrays positions + const b2 = buf(2048); + //for (let i = 0; i < 2048; i++) bufs.push(b2.subarray(i)); + for (const buf of bufs) { + const b = buf.slice(); + // ciphers + for (let [k, libs] of Object.entries(CIPHERS)) { + let encrypted; + for (const [lib, fn] of Object.entries(libs)) { + if (lib === 'opts') continue; + if (encrypted === undefined) encrypted = await fn.encrypt(buf, libs.opts); + else { + const cur = await fn.encrypt(buf, libs.opts); + deepStrictEqual(encrypted, cur, `encrypt verify (${lib})`); + } + deepStrictEqual(buf, b, `encrypt mutates buffer (${lib})`); + const res = await fn.decrypt(encrypted, libs.opts); + deepStrictEqual(res, buf, `decrypt verify (${lib})`); + } + } + } + console.log('VALIDATED'); +} + +export const main = () => + (async () => { + await validate(); + if (ONLY_NOBLE) { + // Benchmark different noble-ciphers + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const c = Object.entries(CIPHERS) + .map(([k, lib]) => [k, lib.noble, lib.opts]) + .filter(([k, noble, _]) => !!noble); + await compare( + `encrypt (${size})`, + samples, + Object.fromEntries(c.map(([k, noble, opts]) => [k, () => noble.encrypt(buf, opts)])) + ); + } + return; + } + // Benchmark against other libraries + for (let [k, libs] of Object.entries(HASHES)) { + for (const [size, [samples, buf]] of Object.entries(buffers)) { + await compare( + `${k} (${size})`, + samples, + Object.fromEntries( + Object.entries(libs) + .filter(([lib, _]) => lib !== 'opts') + .map(([lib, fn]) => [lib, () => fn(buf, libs.opts)]) + ) + ); + } + } + for (let [k, libs] of Object.entries(CIPHERS)) { + console.log(`==== ${k} ====`); + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const l = Object.entries(libs).filter(([lib, _]) => lib !== 'opts'); + await compare( + `${k} (encrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.encrypt(buf, libs.opts)])) + ); + const encrypted = await l[0][1].encrypt(buf, libs.opts); + await compare( + `${k} (decrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.decrypt(encrypted, libs.opts)])) + ); + } + } + // Log current RAM + butils.logMem(); + })(); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/benchmark/ciphers.js b/benchmark/ciphers.js new file mode 100644 index 0000000..8e48d22 --- /dev/null +++ b/benchmark/ciphers.js @@ -0,0 +1,152 @@ +import { deepStrictEqual } from 'assert'; +import { compare, utils as butils } from 'micro-bmark'; +import { createCipheriv, createDecipheriv } from 'node:crypto'; + +import { concatBytes } from '@noble/ciphers/utils'; +import { salsa20, xsalsa20 } from '@noble/ciphers/salsa'; +import { chacha20, xchacha20 } from '@noble/ciphers/chacha'; +import * as micro from '@noble/ciphers/_micro'; + +import { streamXOR as stableSalsa } from '@stablelib/salsa20'; +import { streamXOR as stableXSalsa } from '@stablelib/xsalsa20'; +import { streamXOR as stableChacha } from '@stablelib/chacha'; +import { streamXOR as stableXchacha } from '@stablelib/xchacha20'; + +const ONLY_NOBLE = process.argv[2] === 'noble'; +const buf = (n) => new Uint8Array(n).fill(n); + +// Non-authenticated ciphers. aead.js contains authenticated ones + +// buffer title, sample count, data +const buffers = { + '32B': [1500000, buf(32)], + '64B': [1500000, buf(64)], + '1KB': [300000, buf(1024)], + '8KB': [50000, buf(1024 * 8)], + '1MB': [300, buf(1024 * 1024)], +}; + +const cipherSame = (fn) => ({ encrypt: fn, decrypt: fn }); + +export const CIPHERS = { + salsa: { + opts: { key: buf(32), nonce: buf(8) }, + stablelib: cipherSame((buf, opts) => + stableSalsa(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) + ), + noble: cipherSame((buf, opts) => salsa20(opts.key, opts.nonce, buf)), + micro: cipherSame((buf, opts) => micro.salsa20(opts.key, opts.nonce, buf)), + }, + chacha: { + opts: { key: buf(32), nonce: buf(12), nonce16: concatBytes(new Uint8Array(4), buf(12)) }, + node: { + encrypt: (buf, opts) => { + const c = createCipheriv('chacha20', opts.key, opts.nonce16); + const res = c.update(buf); + c.final(); + return Uint8Array.from(res); + }, + decrypt: (buf, opts) => { + const decipher = createDecipheriv('chacha20', opts.key, opts.nonce16); + const res = decipher.update(buf); + decipher.final(); + return Uint8Array.from(res); + }, + }, + stablelib: cipherSame((buf, opts) => + stableChacha(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) + ), + noble: cipherSame((buf, opts) => chacha20(opts.key, opts.nonce, buf)), + micro: cipherSame((buf, opts) => micro.chacha20(opts.key, opts.nonce, buf)), + }, + xsalsa: { + opts: { key: buf(32), nonce: buf(24) }, + stablelib: cipherSame((buf, opts) => + stableXSalsa(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) + ), + noble: cipherSame((buf, opts) => xsalsa20(opts.key, opts.nonce, buf)), + micro: cipherSame((buf, opts) => micro.xsalsa20(opts.key, opts.nonce, buf)), + }, + xchacha: { + opts: { key: buf(32), nonce: buf(24) }, + stablelib: cipherSame((buf, opts) => + stableXchacha(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) + ), + noble: cipherSame((buf, opts) => xchacha20(opts.key, opts.nonce, buf)), + micro: cipherSame((buf, opts) => micro.xchacha20(opts.key, opts.nonce, buf)), + }, +}; + +async function validate() { + // Verify that things we bench actually work + const bufs = [...Object.entries(buffers).map((i) => i[1][1])]; + // Verify different buffer sizes + for (let i = 0; i < 2048; i++) bufs.push(buf(i)); + // Verify different subarrays positions + const b2 = buf(2048); + //for (let i = 0; i < 2048; i++) bufs.push(b2.subarray(i)); + for (const buf of bufs) { + const b = buf.slice(); + // ciphers + for (let [k, libs] of Object.entries(CIPHERS)) { + let encrypted; + for (const [lib, fn] of Object.entries(libs)) { + if (lib === 'opts') continue; + if (encrypted === undefined) encrypted = await fn.encrypt(buf, libs.opts); + else { + const cur = await fn.encrypt(buf, libs.opts); + deepStrictEqual(encrypted, cur, `encrypt verify (${lib})`); + } + deepStrictEqual(buf, b, `encrypt mutates buffer (${lib})`); + const res = await fn.decrypt(encrypted, libs.opts); + deepStrictEqual(res, buf, `decrypt verify (${lib})`); + } + } + } + console.log('VALIDATED'); +} + +export const main = () => + (async () => { + await validate(); + if (ONLY_NOBLE) { + // Benchmark different noble-ciphers + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const c = Object.entries(CIPHERS) + .map(([k, lib]) => [k, lib.noble, lib.opts]) + .filter(([k, noble, _]) => !!noble); + await compare( + `encrypt (${size})`, + samples, + Object.fromEntries(c.map(([k, noble, opts]) => [k, () => noble.encrypt(buf, opts)])) + ); + } + return; + } + // Benchmark against other libraries + for (let [k, libs] of Object.entries(CIPHERS)) { + console.log(`==== ${k} ====`); + for (const [size, [samples, buf]] of Object.entries(buffers)) { + const l = Object.entries(libs).filter(([lib, _]) => lib !== 'opts'); + await compare( + `${k} (encrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.encrypt(buf, libs.opts)])) + ); + const encrypted = await l[0][1].encrypt(buf, libs.opts); + await compare( + `${k} (decrypt, ${size})`, + samples, + Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.decrypt(encrypted, libs.opts)])) + ); + } + } + // Log current RAM + butils.logMem(); + })(); + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/benchmark/index.js b/benchmark/index.js deleted file mode 100644 index 7a0e6e9..0000000 --- a/benchmark/index.js +++ /dev/null @@ -1,328 +0,0 @@ -import { deepStrictEqual } from 'assert'; -import { run, mark, compare, utils as butils } from 'micro-bmark'; -import * as crypto from 'node:crypto'; -// import { AES as siv } from '../esm/webcrypto/siv.js'; -// import * as gcm from '../esm/webcrypto/aes.js'; -import * as utils from '../esm/utils.js'; -import { salsa20, xsalsa20, xsalsa20_poly1305 } from '../esm/salsa.js'; -import { chacha20, xchacha20, xchacha20_poly1305, chacha20_poly1305 } from '../esm/chacha.js'; -import { poly1305 } from '../esm/_poly1305.js'; -import * as slow from '../esm/_micro.js'; -// StableLib -import * as stableSalsa from '@stablelib/salsa20'; -import * as stableXSalsa from '@stablelib/xsalsa20'; -import * as stableChacha from '@stablelib/chacha'; -import * as stableXchacha from '@stablelib/xchacha20'; -import { ChaCha20Poly1305 as StableChachaPoly } from '@stablelib/chacha20poly1305'; -import { XChaCha20Poly1305 as StableXchachaPoly } from '@stablelib/xchacha20poly1305'; -import { oneTimeAuth as stablePoly1305 } from '@stablelib/poly1305'; -import { default as tweetnacl } from 'tweetnacl'; // secretbox = xsalsa20-poly1305. -import { - ChaCha20Poly1305 as ChainsafeChachaPoly, - newInstance as chainsafe_init_wasm, -} from '@chainsafe/as-chacha20poly1305'; - -const ONLY_NOBLE = process.argv[2] === 'noble'; -const buf = (n) => new Uint8Array(n).fill(n); - -let chainsafe_chacha_poly; - -// Works for gcm only? -const nodeGCM = (name) => { - return { - encrypt: (buf, opts) => { - const res = [opts.iv]; - const c = crypto.createCipheriv(name, opts.key, opts.iv); - if (opts.aad) c.setAAD(opts.aad); - res.push(c.update(buf)); - res.push(c.final()); - res.push(c.getAuthTag()); - return utils.concatBytes(...res.map((i) => Uint8Array.from(i))); - }, - decrypt: (buf, opts) => { - const ciphertext = buf.slice(12, -16); - const authTag = buf.slice(-16); - const decipher = crypto.createDecipheriv(name, opts.key, opts.iv); - if (opts.aad) c.setAAD(opts.aad); - decipher.setAuthTag(authTag); - return utils.concatBytes( - ...[decipher.update(ciphertext), decipher.final()].map((i) => Uint8Array.from(i)) - ); - }, - }; -}; - -const cipherSame = (fn) => ({ encrypt: fn, decrypt: fn }); - -export const CIPHERS = { - salsa: { - opts: { key: buf(32), nonce: buf(8) }, - stablelib: cipherSame((buf, opts) => - stableSalsa.streamXOR(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) - ), - noble: cipherSame((buf, opts) => salsa20(opts.key, opts.nonce, buf)), - micro: cipherSame((buf, opts) => slow.salsa20(opts.key, opts.nonce, buf)), - }, - chacha: { - opts: { key: buf(32), nonce: buf(12), nonce16: utils.concatBytes(new Uint8Array(4), buf(12)) }, - node: { - encrypt: (buf, opts) => { - const c = crypto.createCipheriv('chacha20', opts.key, opts.nonce16); - const res = c.update(buf); - c.final(); - return Uint8Array.from(res); - }, - decrypt: (buf, opts) => { - const decipher = crypto.createDecipheriv('chacha20', opts.key, opts.nonce16); - const res = decipher.update(buf); - decipher.final(); - return Uint8Array.from(res); - }, - }, - stablelib: cipherSame((buf, opts) => - stableChacha.streamXOR(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) - ), - noble: cipherSame((buf, opts) => chacha20(opts.key, opts.nonce, buf)), - micro: cipherSame((buf, opts) => slow.chacha20(opts.key, opts.nonce, buf)), - }, - xsalsa: { - opts: { key: buf(32), nonce: buf(24) }, - stablelib: cipherSame((buf, opts) => - stableXSalsa.streamXOR(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) - ), - noble: cipherSame((buf, opts) => xsalsa20(opts.key, opts.nonce, buf)), - micro: cipherSame((buf, opts) => slow.xsalsa20(opts.key, opts.nonce, buf)), - }, - xchacha: { - opts: { key: buf(32), nonce: buf(24) }, - stablelib: cipherSame((buf, opts) => - stableXchacha.streamXOR(opts.key, opts.nonce, buf, new Uint8Array(buf.length)) - ), - noble: cipherSame((buf, opts) => xchacha20(opts.key, opts.nonce, buf)), - micro: cipherSame((buf, opts) => slow.xchacha20(opts.key, opts.nonce, buf)), - }, - xsalsa20_poly1305: { - opts: { key: buf(32), nonce: buf(24) }, - tweetnacl: { - encrypt: (buf, opts) => tweetnacl.secretbox(buf, opts.nonce, opts.key), - decrypt: (buf, opts) => tweetnacl.secretbox.open(buf, opts.nonce, opts.key), - }, - noble: { - encrypt: (buf, opts) => xsalsa20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => xsalsa20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - micro: { - encrypt: (buf, opts) => slow.xsalsa20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => slow.xsalsa20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - }, - chacha20_poly1305: { - opts: { key: buf(32), nonce: buf(12) }, - node: { - encrypt: (buf, opts) => { - const c = crypto.createCipheriv('chacha20-poly1305', opts.key, opts.nonce); - const res = []; - res.push(c.update(buf)); - res.push(c.final()); - res.push(c.getAuthTag()); - return utils.concatBytes(...res.map((i) => Uint8Array.from(i))); - }, - decrypt: (buf, opts) => { - const ciphertext = buf.slice(0, -16); - const authTag = buf.slice(-16); - const decipher = crypto.createDecipheriv('chacha20-poly1305', opts.key, opts.nonce); - decipher.setAuthTag(authTag); - return utils.concatBytes( - ...[decipher.update(ciphertext), decipher.final()].map((i) => Uint8Array.from(i)) - ); - }, - }, - stable: { - encrypt: (buf, opts) => new StableChachaPoly(opts.key).seal(opts.nonce, buf), - decrypt: (buf, opts) => new StableChachaPoly(opts.key).open(opts.nonce, buf), - }, - chainsafe: { - encrypt: (buf, opts) => { - return chainsafe_chacha_poly.seal(opts.key, opts.nonce, buf); - }, - decrypt: (buf, opts) => { - return chainsafe_chacha_poly.open(opts.key, opts.nonce, buf); - }, - }, - noble: { - encrypt: (buf, opts) => chacha20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => chacha20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - micro: { - encrypt: (buf, opts) => slow.chacha20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => slow.chacha20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - }, - xchacha20poly1305: { - opts: { key: buf(32), nonce: buf(24) }, - stable: { - encrypt: (buf, opts) => new StableXchachaPoly(opts.key).seal(opts.nonce, buf), - decrypt: (buf, opts) => new StableXchachaPoly(opts.key).open(opts.nonce, buf), - }, - noble: { - encrypt: (buf, opts) => xchacha20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => xchacha20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - micro: { - encrypt: (buf, opts) => slow.xchacha20_poly1305(opts.key, opts.nonce).encrypt(buf), - decrypt: (buf, opts) => slow.xchacha20_poly1305(opts.key, opts.nonce).decrypt(buf), - }, - }, - // TODO: why this is so slow? - // 'gcm-256': { - // opts: { key: buf(32), iv: buf(12) }, - // node: nodeGCM('aes-256-gcm'), - // noble: { - // encrypt: (buf, opts) => gcm.encrypt(opts.key, buf, opts.iv), - // decrypt: (buf, opts) => gcm.decrypt(opts.key, buf, opts.iv), - // }, - // }, - // 'gcm-siv-128': { - // opts: { key: buf(16), aad: buf(0), nonce: buf(12) }, - // noble: { - // encrypt: async (buf, opts) => await (await siv(opts.key, opts.nonce, opts.aad)).encrypt(buf), - // decrypt: async (buf, opts) => await (await siv(opts.key, opts.nonce, opts.aad)).decrypt(buf), - // }, - // }, - // 'gcm-siv-256': { - // opts: { key: buf(32), aad: buf(0), nonce: buf(12) }, - // noble: { - // encrypt: async (buf, opts) => await (await siv(opts.key, opts.nonce, opts.aad)).encrypt(buf), - // decrypt: async (buf, opts) => await (await siv(opts.key, opts.nonce, opts.aad)).decrypt(buf), - // }, - // }, -}; - -const HASHES = { - poly1305: { - opts: { key: buf(32) }, - stable: (buf, opts) => stablePoly1305(opts.key, buf), - // function crypto_onetimeauth(out, outpos, m, mpos, n, k) { - tweetnacl: (buf, opts) => { - // Such awesome API! - const res = new Uint8Array(16); - tweetnacl.lowlevel.crypto_onetimeauth(res, 0, buf, 0, buf.length, opts.key); - return res; - }, - noble: (buf, opts) => poly1305(buf, opts.key), - micro: (buf, opts) => slow.poly1305(buf, opts.key), - }, -}; - -// buffer title, sample count, data -const buffers = { - '32B': [2000000, buf(32)], - '64B': [1000000, buf(64)], - '1KB': [66667, buf(1024)], - '8KB': [8333, buf(1024 * 8)], - '1MB': [524, buf(1024 * 1024)], -}; - -async function validate() { - // Verify that things we bench actually work - const bufs = [...Object.entries(buffers).map((i) => i[1][1])]; - // Verify different buffer sizes - for (let i = 0; i < 2048; i++) bufs.push(buf(i)); - // Verify different subarrays positions - const b2 = buf(2048); - //for (let i = 0; i < 2048; i++) bufs.push(b2.subarray(i)); - for (const buf of bufs) { - const b = buf.slice(); - // hashes - for (let [k, libs] of Object.entries(HASHES)) { - let value; - for (const [lib, fn] of Object.entries(libs)) { - if (lib === 'opts') continue; - if (value === undefined) value = fn(buf, libs.opts); - else { - const cur = fn(buf, libs.opts); - deepStrictEqual(value, cur, `hash verify (${lib})`); - } - deepStrictEqual(buf, b, `hash mutates buffer (${lib})`); - } - } - // ciphers - for (let [k, libs] of Object.entries(CIPHERS)) { - let encrypted; - for (const [lib, fn] of Object.entries(libs)) { - if (lib === 'opts') continue; - if (encrypted === undefined) encrypted = await fn.encrypt(buf, libs.opts); - else { - const cur = await fn.encrypt(buf, libs.opts); - deepStrictEqual(encrypted, cur, `encrypt verify (${lib})`); - } - deepStrictEqual(buf, b, `encrypt mutates buffer (${lib})`); - const res = await fn.decrypt(encrypted, libs.opts); - deepStrictEqual(res, buf, `decrypt verify (${lib})`); - } - } - } - console.log('VALIDATED'); -} - -export const main = () => - run(async () => { - if (!ONLY_NOBLE) { - const ctx = chainsafe_init_wasm(); - chainsafe_chacha_poly = new ChainsafeChachaPoly(ctx); - } - await validate(); - if (ONLY_NOBLE) { - // Benchmark different noble-ciphers - for (const [size, [samples, buf]] of Object.entries(buffers)) { - const c = Object.entries(CIPHERS) - .map(([k, lib]) => [k, lib.noble, lib.opts]) - .filter(([k, noble, _]) => !!noble); - await compare( - `encrypt (${size})`, - samples, - Object.fromEntries(c.map(([k, noble, opts]) => [k, () => noble.encrypt(buf, opts)])) - ); - } - return; - } - // Benchmark against other libraries - for (let [k, libs] of Object.entries(HASHES)) { - for (const [size, [samples, buf]] of Object.entries(buffers)) { - await compare( - `${k} (${size})`, - samples, - Object.fromEntries( - Object.entries(libs) - .filter(([lib, _]) => lib !== 'opts') - .map(([lib, fn]) => [lib, () => fn(buf, libs.opts)]) - ) - ); - } - } - for (let [k, libs] of Object.entries(CIPHERS)) { - console.log(`==== ${k} ====`); - for (const [size, [samples, buf]] of Object.entries(buffers)) { - const l = Object.entries(libs).filter(([lib, _]) => lib !== 'opts'); - await compare( - `${k} (encrypt, ${size})`, - samples, - Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.encrypt(buf, libs.opts)])) - ); - const encrypted = await l[0][1].encrypt(buf, libs.opts); - await compare( - `${k} (decrypt, ${size})`, - samples, - Object.fromEntries(l.map(([lib, fn]) => [lib, () => fn.decrypt(encrypted, libs.opts)])) - ); - } - } - // Log current RAM - butils.logMem(); - }); - -// ESM is broken. -import url from 'url'; -if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { - main(); -} diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index a28a1ce..01ebd51 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -9,25 +9,38 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "@devtomio/sodium": "^0.3.0", - "@stablelib/chacha": "^1.0.1", - "@stablelib/chacha20poly1305": "^1.0.1", - "@stablelib/salsa20": "^1.0.2", - "@stablelib/xchacha20": "^1.0.1", - "@stablelib/xchacha20poly1305": "^1.0.1", - "@stablelib/xsalsa20": "^1.0.2", - "libsodium-wrappers": "^0.7.11", - "tweetnacl": "^1.0.3" - }, + "@chainsafe/as-chacha20poly1305": "0.1.0", + "@devtomio/sodium": "0.3.0", + "@noble/ciphers": "file:..", + "@stablelib/chacha": "1.0.1", + "@stablelib/chacha20poly1305": "1.0.1", + "@stablelib/salsa20": "1.0.2", + "@stablelib/xchacha20": "1.0.1", + "@stablelib/xchacha20poly1305": "1.0.1", + "@stablelib/xsalsa20": "1.0.2", + "libsodium-wrappers": "0.7.11", + "tweetnacl": "1.0.3" + } + }, + "..": { + "version": "0.1.4", + "license": "MIT", "devDependencies": { - "@chainsafe/as-chacha20poly1305": "^0.1.0" + "@scure/base": "1.1.1", + "fast-check": "3.0.0", + "micro-bmark": "0.3.1", + "micro-should": "0.4.0", + "prettier": "2.8.4", + "typescript": "5.0.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", - "integrity": "sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==", - "dev": true + "integrity": "sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew==" }, "node_modules/@devtomio/sodium": { "version": "0.3.0", @@ -175,6 +188,10 @@ "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.1.0.tgz", "integrity": "sha512-XQr74QaLeMiqhStEhLn1im9EOMnkypp7MZOwQhGzqp2Weu5eQJbpPxWxixxlYRKWPOmJjsk6qYfYH9kq43yc2w==" }, + "node_modules/@noble/ciphers": { + "resolved": "..", + "link": true + }, "node_modules/@node-rs/helper": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.3.3.tgz", diff --git a/benchmark/package.json b/benchmark/package.json index 908ee7c..30bc3d2 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -12,8 +12,9 @@ "author": "", "license": "MIT", "dependencies": { - "@devtomio/sodium": "0.3.0", "@chainsafe/as-chacha20poly1305": "0.1.0", + "@devtomio/sodium": "0.3.0", + "@noble/ciphers": "file:..", "@stablelib/chacha": "1.0.1", "@stablelib/chacha20poly1305": "1.0.1", "@stablelib/salsa20": "1.0.2", diff --git a/benchmark/poly.js b/benchmark/poly.js new file mode 100644 index 0000000..83d3f86 --- /dev/null +++ b/benchmark/poly.js @@ -0,0 +1,86 @@ +import { deepStrictEqual } from 'assert'; +import { compare, utils as butils } from 'micro-bmark'; +import { poly1305 } from '@noble/ciphers/_poly1305'; +import * as micro from '@noble/ciphers/_micro'; +import { oneTimeAuth as stablePoly1305 } from '@stablelib/poly1305'; +import { default as tweetnacl } from 'tweetnacl'; // secretbox = xsalsa20-poly1305. + +const ONLY_NOBLE = process.argv[2] === 'noble'; +const buf = (n) => new Uint8Array(n).fill(n); +const HASHES = { + poly1305: { + opts: { key: buf(32) }, + stable: (buf, opts) => stablePoly1305(opts.key, buf), + // function crypto_onetimeauth(out, outpos, m, mpos, n, k) { + tweetnacl: (buf, opts) => { + // Such awesome API! + const res = new Uint8Array(16); + tweetnacl.lowlevel.crypto_onetimeauth(res, 0, buf, 0, buf.length, opts.key); + return res; + }, + noble: (buf, opts) => poly1305(buf, opts.key), + micro: (buf, opts) => micro.poly1305(buf, opts.key), + }, +}; + +// buffer title, sample count, data +const buffers = { + '32B': [2000000, buf(32)], + '64B': [1000000, buf(64)], + '1KB': [66667, buf(1024)], + '8KB': [8333, buf(1024 * 8)], + '1MB': [524, buf(1024 * 1024)], +}; + +async function validate() { + // Verify that things we bench actually work + const bufs = [...Object.entries(buffers).map((i) => i[1][1])]; + // Verify different buffer sizes + for (let i = 0; i < 2048; i++) bufs.push(buf(i)); + // Verify different subarrays positions + const b2 = buf(2048); + //for (let i = 0; i < 2048; i++) bufs.push(b2.subarray(i)); + for (const buf of bufs) { + const b = buf.slice(); + // hashes + for (let [k, libs] of Object.entries(HASHES)) { + let value; + for (const [lib, fn] of Object.entries(libs)) { + if (lib === 'opts') continue; + if (value === undefined) value = fn(buf, libs.opts); + else { + const cur = fn(buf, libs.opts); + deepStrictEqual(value, cur, `hash verify (${lib})`); + } + deepStrictEqual(buf, b, `hash mutates buffer (${lib})`); + } + } + } + console.log('VALIDATED'); +} + +export const main = async () => { + await validate(); + // Benchmark against other libraries + for (let [k, libs] of Object.entries(HASHES)) { + for (const [size, [samples, buf]] of Object.entries(buffers)) { + await compare( + `${k} (${size})`, + samples, + Object.fromEntries( + Object.entries(libs) + .filter(([lib, _]) => lib !== 'opts') + .map(([lib, fn]) => [lib, () => fn(buf, libs.opts)]) + ) + ); + } + } + // Log current RAM + butils.logMem(); +}; + +// ESM is broken. +import url from 'url'; +if (import.meta.url === url.pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/package.json b/package.json index 5f9a85a..d5b5462 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "bench:install": "cd benchmark && npm install && cd ../../", "build": "npm run build:clean; tsc && tsc -p tsconfig.esm.json", "build:release": "cd build; npm i; npm run build", - "build:clean": "rm -r *.{js,d.ts,js.map,d.ts.map} esm/*.{js,d.ts,js.map,d.ts.map} webcrypto esm/webcrypto 2> /dev/null", + "build:clean": "rm *.{js,d.ts,js.map,d.ts.map} esm/*.{js,d.ts,js.map,d.ts.map} 2> /dev/null; rm -r esm/webcrypto 2> /dev/null", "lint": "prettier --check 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'", "format": "prettier --write 'src/**/*.{js,ts}' 'test/**/*.{js,ts,mjs}'", "test": "node test/index.js"