noble-ciphers
Auditable & minimal JS implementation of Salsa20, ChaCha, Poly1305 & AES-SIV
- 🔒 Auditable
- 🔻 Tree-shaking-friendly: use only what's necessary, other code won't be included
- 🏎 Ultra-fast, hand-optimized for caveats of JS engines
- 🔍 Unique tests ensure correctness: property-based, cross-library and Wycheproof vectors
- 💼 AES: GCM (Galois Counter Mode), SIV (Nonce Misuse-Resistant encryption)
- 💃 Salsa20, ChaCha, XSalsa20, XChaCha, Poly1305, ChaCha8, ChaCha12
- ✍️ FF1 format-preserving encryption
- 🧂 Compatible with NaCl / libsodium secretbox
- 🪶 Just 500 lines / 4KB gzipped for Salsa + ChaCha + Poly build
_slow
file contains minimal, readable implementation of algorithms.
Other files contain unrolled loops, which are fast, but less auditable.
This library belongs to noble crypto
noble-crypto — high-security, easily auditable set of contained cryptographic libraries and tools.
- No dependencies, protection against supply chain attacks
- Auditable TypeScript / JS code
- Supported on all major platforms
- Releases are signed with PGP keys and built transparently with NPM provenance
- Check out homepage & all libraries:
ciphers,
curves
(4kb versions secp256k1,
ed25519),
hashes
Usage
npm install @noble/ciphers
We support all major platforms and runtimes.
For Deno, ensure to use
npm specifier.
For React Native, you may need a
polyfill for crypto.getRandomValues.
If you don't like NPM, a standalone
noble-ciphers.js is also available.
import { xsalsa20_poly1305, secretbox } from '@noble/ciphers/salsa';
import { chacha20_poly1305 } from '@noble/ciphers/chacha';
import { randomBytes } from '@noble/ciphers/webcrypto/utils';
import { utf8ToBytes } from '@noble/ciphers/utils';
const key = randomBytes(32);
const data = utf8ToBytes('hello, noble');
const nonce24 = randomBytes(24);
const stream_s = xsalsa20_poly1305(key, nonce24);
const encrypted_s = stream_s.encrypt(data);
stream_s.decrypt(encrypted_s);
const nonce12 = randomBytes(12);
const stream_c = chacha20_poly1305(key, nonce12);
const encrypted_c = stream_c.encrypt(data);
stream_c.decrypt(encrypted_c);
import {
aes_128_gcm, aes_128_ctr, aes_128_cbc,
aes_256_gcm, aes_256_ctr, aes_256_cbc
} from '@noble/ciphers/webcrypto/aes';
const stream_aes = aes_256_gcm(key, nonce);
const ciphertext_aes = await stream_aes.encrypt(data);
const plaintext_aes = await stream_a.decrypt(ciphertext_aes);
import { aes_256_gcm_siv } from '@noble/ciphers/webcrypto/siv';
import { FF1, BinaryFF1 } from '@noble/ciphers/webcrypto/ff1';
import * as ciphers from '@noble/ciphers/_slow';
How to encrypt properly
- Use unpredictable, random key; don't re-use keys between different protocols
- Use new nonce every time and don't repeat it
- Be aware of rules for cryptographic key wear-out and encryption limits
- Prefer authenticated encryption, with MACs like poly1305, GCM, hmac
Most ciphers need a key and a nonce (aka initialization vector / IV) to encrypt a data:
ciphertext = encrypt(plaintext, key, nonce)
Repeating (key, nonce) pair with different plaintexts would allow an attacker to decrypt it:
ciphertext_a = encrypt(plaintext_a, key, nonce)
ciphertext_b = encrypt(plaintext_b, key, nonce)
stream_diff = xor(ciphertext_a, ciphertext_b) # Break encryption
So, you can't repeat nonces. One way of doing so is using counters:
for i in 0..:
ciphertext[i] = encrypt(plaintexts[i], key, i)
Another is generating random nonce every time:
for i in 0..:
rand_nonces[i] = random()
ciphertext[i] = encrypt(plaintexts[i], key, rand_nonces[i])
Counters are OK, but it's not always possible to store current counter value:
e.g. in decentralized, unsyncable systems.
Randomness is OK, but there's a catch:
ChaCha20 and AES-GCM use 96-bit / 12-byte nonces, which implies
higher chance of collision. In the example above,
random()
can collide and produce repeating nonce.
To safely use random nonces, utilize XSalsa20 or XChaCha:
they increased nonce length to 192-bit, minimizing a chance of collision.
AES-SIV is also fine. In situations where you can't use eXtended-nonce
algorithms, key rotation is advised. hkdf would work great for this case.
Encryption limits
A "protected message" would mean a probability of 2**-50
that a passive attacker
successfully distinguishes the ciphertext outputs of the AEAD scheme from the outputs
of a random function. See RFC draft for details.
- Max message size:
- AES-GCM: ~68GB,
2**36-256
- Salsa, ChaCha, XSalsa, XChaCha: ~256GB,
2**38-64
- Max amount of protected messages, under same key:
- AES-GCM:
2**32.5
- Salsa, ChaCha:
2**46
, but only integrity is affected, not confidentiality - XSalsa, XChaCha:
2**72
- Max amount of protected messages, across all keys:
- AES-GCM:
2**69/B
where B is max blocks encrypted by a key. Meaning
2**59
for 1KB, 2**49
for 1MB, 2**39
for 1GB - Salsa, ChaCha, XSalsa, XChaCha:
2**100
Salsa
import { xsalsa20_poly1305, secretbox } from '@noble/ciphers/salsa';
import { utf8ToBytes } from '@noble/ciphers/utils';
import { randomBytes } from '@noble/ciphers/webcrypto/utils';
const key = randomBytes(32);
const data = utf8ToBytes('hello, noble');
const nonce = randomBytes(24);
const stream_x = xsalsa20_poly1305(key, nonce);
const ciphertext = stream_x.encrypt(data);
const plaintext = stream_x.decrypt(ciphertext);
const box = secretbox(key, nonce);
const ciphertext = box.seal(plaintext);
const plaintext = box.open(ciphertext);
import { salsa20, xsalsa20 } from '@noble/ciphers/salsa';
const nonce12 = randomBytes(12);
const encrypted_s = salsa20(key, nonce12, data);
const encrypted_xs = xsalsa20(key, nonce, data);
Salsa20 stream cipher (website,
PDF,
wiki) was released in 2005.
Salsa's goal was to implement AES replacement that does not rely on S-Boxes,
which are hard to implement in a constant-time manner.
Salsa20 is usually faster than AES, a big deal on slow, budget mobile phones.
XSalsa20, extended-nonce
variant was released in 2008. It switched nonces from 96-bit to 192-bit,
and became safe to be picked at random.
Nacl / Libsodium popularized term "secretbox", a simple black-box
authenticated encryption. Secretbox is just xsalsa20-poly1305. We provide the
alias and corresponding seal / open methods.
ChaCha
import { chacha20_poly1305, xchacha20_poly1305 } from '@noble/ciphers/chacha';
import { utf8ToBytes } from '@noble/ciphers/utils';
import { randomBytes } from '@noble/ciphers/webcrypto/utils';
const key = randomBytes(32);
const data = utf8ToBytes('hello, noble');
const nonce12 = randomBytes(12);
const stream_c = chacha20_poly1305(key, nonce12);
const ciphertext_c = stream_c.encrypt(data);
const plaintext_c = stream_c.decrypt(ciphertext_c);
const nonce24 = randomBytes(24);
const stream_xc = xchacha20_poly1305(key, nonce24);
const ciphertext_xc = stream_xc.encrypt(data);
const plaintext_xc = stream_xc.decrypt(ciphertext_xc);
import { chacha20, xchacha20, chacha8, chacha12 } from '@noble/ciphers/chacha';
const ciphertext_pc = chacha20(key, nonce12, data);
const ciphertext_pxc = xchacha20(key, nonce24, data);
const ciphertext_8 = chacha8(key, nonce12, data);
const ciphertext_12 = chacha12(key, nonce12, data);
ChaCha20 stream cipher (website,
PDF,
wiki,
blog post) was released
in 2008. ChaCha aims to increase the diffusion per round, but had slightly less
cryptanalysis. It was standardized in
RFC 8439 and is now used in TLS 1.3.
XChaCha20 (draft RFC)
extended-nonce variant is also provided. Similar to XSalsa, it's safe to use with
randomly-generated nonces.
Poly1305
Poly1305 (website,
PDF,
wiki,
blog post)
is a fast and parallel secret-key message-authentication code suitable for
a wide variety of applications. It was standardized in
RFC 8439 and is now used in TLS 1.3.
Poly1305 is polynomial-evaluation MAC, which is not perfect for every situation:
just like GCM, it lacks Random Key Robustness: the tags can be forged, and can't
be used in PAKE schemes. See
invisible salamanders attack.
To combat invisible salamanders, hash(key)
can be included in ciphertext,
however, this would violate ciphertext indistinguishability:
an attacker would know which key was used - so HKDF(key, i)
could be used instead.
Even though poly1305 can be imported separately from the library, we suggest
using chacha-poly or xsalsa-poly.
AES
import {
aes_128_gcm, aes_128_ctr, aes_128_cbc,
aes_256_gcm, aes_256_ctr, aes_256_cbc
} from '@noble/ciphers/webcrypto/aes';
for (let cipher of [aes_256_gcm, aes_256_ctr, aes_256_cbc]) {
const stream_new = cipher(key, nonce);
const ciphertext_new = await stream_new.encrypt(plaintext);
const plaintext_new = await stream_new.decrypt(ciphertext);
}
import { aes_256_gcm_siv } from '@noble/ciphers/webcrypto/siv';
const stream_siv = aes_256_gcm_siv(key, nonce)
await stream_siv.encrypt(plaintext, AAD);
AES (wiki)
is a variant of Rijndael block cipher, standardized by NIST.
We don't implement AES in pure JS for now: instead, we wrap WebCrypto built-in
and provide an improved, simple API. There is a simple reason for this:
webcrypto API is terrible: different block modes require different params.
Optional AES-GCM-SIV
(synthetic initialization vector) nonce-misuse-resistant mode is also provided.
How AES works
cipher = encrypt(block, key)
. Data is split into 128-bit blocks. Encrypted in 10/12/14 rounds (128/192/256bit). Every round does:
- S-box, table substitution
- Shift rows, cyclic shift left of all rows of data array
- Mix columns, multiplying every column by fixed polynomial
- Add round key, round_key xor i-th column of array
For non-deterministic (not ECB) schemes, initialization vector (IV) is mixed to block/key;
and each new round either depends on previous block's key, or on some counter.
Block modes
We only expose GCM & SIV for now.
- ECB — simple deterministic replacement. Dangerous: always map x to y. See AES Penguin
- CBC — key is previous round’s block. Hard to use: need proper padding, also needs MAC
- CTR — counter, allows to create streaming cipher. Requires good IV. Parallelizable. OK, but no MAC
- GCM — modern CTR, parallel, with MAC. Not ideal:
- Conservative key wear-out is
2**32
(4B) msgs - MAC can be forged: see Poly1305 section above
- SIV — synthetic initialization vector, nonce-misuse-resistant
- Can be 1.5-2x slower than GCM by itself
- nonce misuse-resistant schemes guarantee that if a
nonce repeats, then the only security loss is that identical
plaintexts will produce identical ciphertexts
- MAC can be forged: see Poly1305 section above
- XTS — used in hard drives. Similar to ECB (deterministic), but has
[i][j]
tweak arguments corresponding to sector i and 16-byte block (part of sector) j. Not authenticated!
FF1
Format-preserving encryption algorithm (FPE-FF1) specified in NIST Special Publication 800-38G.
More info: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38G.pdf
Security
The library is experimental. Use at your own risk.
Speed
Benchmark results on Apple M2 with node v20:
encrypt (32B)
├─salsa x 1,562,500 ops/sec @ 640ns/op
├─chacha x 1,968,503 ops/sec @ 508ns/op
├─xsalsa x 969,932 ops/sec @ 1μs/op
├─xchacha x 959,692 ops/sec @ 1μs/op
├─xsalsa20_poly1305 x 554,631 ops/sec @ 1μs/op
├─chacha20_poly1305 x 542,888 ops/sec @ 1μs/op
└─xchacha20poly1305 x 346,140 ops/sec @ 2μs/op
encrypt (64B)
├─salsa x 1,392,757 ops/sec @ 718ns/op
├─chacha x 1,709,401 ops/sec @ 585ns/op
├─xsalsa x 892,857 ops/sec @ 1μs/op
├─xchacha x 888,099 ops/sec @ 1μs/op
├─xsalsa20_poly1305 x 476,871 ops/sec @ 2μs/op
├─chacha20_poly1305 x 488,997 ops/sec @ 2μs/op
└─xchacha20poly1305 x 326,157 ops/sec @ 3μs/op
encrypt (1KB)
├─salsa x 219,780 ops/sec @ 4μs/op
├─chacha x 227,634 ops/sec @ 4μs/op
├─xsalsa x 204,290 ops/sec @ 4μs/op
├─xchacha x 203,873 ops/sec @ 4μs/op
├─xsalsa20_poly1305 x 116,049 ops/sec @ 8μs/op
├─chacha20_poly1305 x 116,522 ops/sec @ 8μs/op
└─xchacha20poly1305 x 103,487 ops/sec @ 9μs/op
encrypt (8KB)
├─salsa x 30,695 ops/sec @ 32μs/op
├─chacha x 30,817 ops/sec @ 32μs/op
├─xsalsa x 30,193 ops/sec @ 33μs/op
├─xchacha x 30,255 ops/sec @ 33μs/op
├─xsalsa20_poly1305 x 17,402 ops/sec @ 57μs/op
├─chacha20_poly1305 x 17,513 ops/sec @ 57μs/op
└─xchacha20poly1305 x 17,208 ops/sec @ 58μs/op
encrypt (1MB)
├─salsa x 249 ops/sec @ 4ms/op
├─chacha x 250 ops/sec @ 3ms/op
├─xsalsa x 249 ops/sec @ 4ms/op
├─xchacha x 250 ops/sec @ 3ms/op
├─xsalsa20_poly1305 x 142 ops/sec @ 7ms/op
├─chacha20_poly1305 x 143 ops/sec @ 6ms/op
└─xchacha20poly1305 x 143 ops/sec @ 6ms/op
How does this compare to other implementations?
- node.js native code is 3-10x faster than noble-ciphers
- tweetnacl is 25% slower than noble-ciphers on 1KB+ inputs
- noble-slow "slow, but more readable" version of noble-ciphers is 3-8x slower
(check out
_slow.ts
)
xsalsa20_poly1305 (encrypt, 1MB)
├─tweetnacl x 112 ops/sec @ 8ms/op
├─noble x 142 ops/sec @ 7ms/op
└─noble-slow x 20 ops/sec @ 47ms/op
chacha20_poly1305 (encrypt, 1MB)
├─node x 1,369 ops/sec @ 729μs/op
├─stablelib x 120 ops/sec @ 8ms/op
├─noble x 142 ops/sec @ 7ms/op
└─noble-slow x 19 ops/sec @ 51ms/op
Contributing & testing
- Clone the repository
npm install
to install build dependencies like TypeScriptnpm run build
to compile TypeScript codenpm run test
will execute all main tests
License
The MIT License (MIT)
Copyright (c) 2023 Paul Miller (https://paulmillr.com)
Copyright (c) 2016 Thomas Pornin pornin@bolet.org
See LICENSE file.