Skip to content

Commit

Permalink
Improve benchmarks
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmillr committed Jul 18, 2023
1 parent 48ef205 commit 2139e8d
Show file tree
Hide file tree
Showing 9 changed files with 670 additions and 389 deletions.
102 changes: 56 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
180 changes: 180 additions & 0 deletions benchmark/aead.js
Original file line number Diff line number Diff line change
@@ -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();
}
Loading

0 comments on commit 2139e8d

Please sign in to comment.