cipherlib 0.7.2
cipherlib: ^0.7.2 copied to clipboard
Pure-Dart symmetric ciphers (AES modes, ChaCha20, XChaCha20, Salsa20, XSalsa20), their Poly1305 AEAD variants, and ML-KEM post-quantum key encapsulation (FIPS 203).
cipherlib #
A pure-Dart cryptography library: modern stream ciphers, authenticated encryption, a full AES mode set, and post-quantum cryptography with no native bindings and a single dependency.
cipherlib is the top layer of a three-package family:
It reuses hashlib for the SHA-3/SHAKE, Poly1305, and secure-random
primitives it is built on, and that is its only runtime dependency.
Highlights #
- Runs on every platform: pure Dart with no native code or FFI, so the same library works everywhere Dart does: the VM, Flutter (Android, iOS, Windows, macOS, Linux), and the web (dart2js and dart2wasm).
- Authenticated by default: AES-GCM, AES-CCM, and a Poly1305 variant of every stream cipher, each with a constant-time tag check that rejects tampered messages.
- Broad AES coverage: ten modes, from ECB and CBC to GCM, CCM, and XTS.
- One-shot or streaming: encrypt a buffer in a single call, or pipe a
Stream<List<int>>through any cipher as aStreamTransformer. - Post-quantum ready: ML-KEM-512/768/1024 (FIPS 203) for quantum-resistant key exchange.
- Fast: consistently outruns
PointyCastleandcryptography, see in the benchmarks below.
Install #
dependencies:
cipherlib: ^0.7.1
or run dart pub add cipherlib. A single import exposes every algorithm:
import 'package:cipherlib/cipherlib.dart';
Two optional companions pair well with it — codecs for hex/UTF-8 conversion
(re-exported from hashlib) and random for secure key and nonce generation:
import 'package:cipherlib/codecs.dart'; // toHex, fromHex, toUtf8, fromUtf8
import 'package:cipherlib/random.dart'; // randomBytes
Full API reference: cipherlib library.
Quickstart #
AES-256-GCM is the recommended default for most applications: it encrypts and authenticates in one step, and decryption fails loudly if the ciphertext or the associated data bound to it was modified.
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
void main() {
final key = randomBytes(32); // AES-256
final nonce = randomBytes(12); // Recommended IV size for GCM
final aad = toUtf8('order-id=INV-1001');
final plain = toUtf8('Ship 3 units to dock-7');
final aes = AES(key).gcm(nonce, aad: aad);
final sealed = aes.encrypt(plain);
final opened = aes.decrypt(sealed);
print('AES-256-GCM');
print('key : ${toHex(key)}');
print('nonce : ${toHex(nonce)}');
print('aad : ${fromUtf8(aad)}');
print('cipher: ${toHex(sealed)}');
print('plain : ${fromUtf8(opened)}');
}
Every snippet in this README is also a runnable program in the example folder.
Supported algorithms #
Block ciphers #
| Algorithm | Public class and methods | Source |
|---|---|---|
| AES | AES |
NIST.FIPS.197 |
| Blowfish | Blowfish |
Blowfish-1993 |
| Twofish | Twofish |
Twofish-1998 |
AES supports 10 modes:
ECB- Electronic CodebookCBC- Cipher Block ChainingCTR- CounterGCM- Galois/Counter Mode (authenticated)CCM- Counter with CBC-MAC (authenticated)CFB- Cipher FeedbackOFB- Output FeedbackIGE- Infinite Garble ExtensionPCBC- Propagating Cipher Block ChainingXTS- XEX Tweakable Block Cipher with Ciphertext Stealing
Blowfish and Twofish support 2 modes:
ECB- Electronic CodebookCBC- Cipher Block ChainingCTR- Counter
Stream ciphers #
| Algorithm | Public class and methods | Source |
|---|---|---|
| XOR | XOR, xor |
Wikipedia |
| ChaCha20 | ChaCha20, chacha20 |
RFC-8439 |
| XChaCha20 | XChaCha20, xchacha20 |
libsodium |
| Salsa20 | Salsa20, salsa20 |
Snuffle-2005 |
| XSalsa20 | XSalsa20, xsalsa20 |
libsodium |
Authenticated encryption (AEAD) #
| Algorithm | Public class and methods | Source |
|---|---|---|
| ChaCha20/Poly1305 | ChaCha20Poly1305, chacha20poly1305 |
RFC-8439 |
| XChaCha20/Poly1305 | XChaCha20Poly1305, xchacha20poly1305 |
libsodium |
| Salsa20/Poly1305 | Salsa20Poly1305, salsa20poly1305 |
Snuffle-2005 |
| XSalsa20/Poly1305 | XSalsa20Poly1305, xsalsa20poly1305 |
libsodium |
AES adds authenticated encryption through its GCM and CCM modes.
Post-Quantum Cryptography #
| Algorithm | Public class and methods | Source |
|---|---|---|
| ML-KEM | MLKEM |
NIST.FIPS.203 |
MLKEM.kem512(), MLKEM.kem768(), and MLKEM.kem1024() provide key
generation, encapsulation, and decapsulation, with optional deterministic key
generation from a 64-byte seed.
Security notes #
- Never reuse a
(key, nonce)pair. For the stream ciphers and for AES-CTR/GCM/CCM, encrypting two messages under the same key and nonce destroys confidentiality — and for GCM/CCM it also lets an attacker forge tags. Use a fresh random nonce (or a strictly increasing counter) per message; XChaCha20/XSalsa20's 24-byte nonce is large enough to randomize safely. - AES is not constant-time. The pure-Dart AES uses table lookups, which are not guaranteed to run in constant time. Weigh the deployment environment before using it where cache-timing side channels matter.
- ML-KEM timing. The implementation follows the constant-time structure of the reference implementation, but Dart runtimes (JIT, AOT, dart2js) cannot guarantee constant-time execution. Consider the deployment environment before using ML-KEM in side-channel-sensitive settings.
Recipes #
ChaCha20-Poly1305 #
A fast software AEAD, well suited to mobile and network protocols. The one-shot
function returns both the ciphertext (.data) and the tag (.mac); pass the
tag back on decryption and a mismatch throws.
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
void main() {
final key = randomBytes(32);
final nonce = randomBytes(12);
final aad = toUtf8('content-type=application/json');
final message = toUtf8('{"event":"payment.settled","id":42}');
final sealed = chacha20poly1305(
message,
key,
nonce: nonce,
aad: aad,
);
final opened = chacha20poly1305(
sealed.data,
key,
nonce: nonce,
aad: aad,
mac: sealed.mac.bytes,
);
print('ChaCha20-Poly1305');
print('key : ${toHex(key)}');
print('nonce : ${toHex(nonce)}');
print('tag : ${sealed.mac.hex()}');
print('cipher: ${toHex(sealed.data)}');
print('plain : ${fromUtf8(opened.data)}');
}
XChaCha20 (extended nonce) #
XChaCha20 widens the nonce to 24 bytes, so a random nonce per message is safe
without tracking a counter. Append poly1305 (xchacha20poly1305) for the
authenticated variant.
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
void main() {
final key = randomBytes(32);
final nonce = randomBytes(24); // Extended nonce
final plain = toUtf8('XChaCha20 extended nonce payload');
final cipher = xchacha20(plain, key, nonce: nonce);
final opened = xchacha20(cipher, key, nonce: nonce);
print('XChaCha20');
print('key : ${toHex(key)}');
print('nonce : ${toHex(nonce)}');
print('cipher: ${toHex(cipher)}');
print('plain : ${fromUtf8(opened)}');
}
AES-CBC with PKCS#7 padding #
For interoperability with systems that expect classic block modes. AES.pkcs7
adds and strips padding automatically, and encryptString/decrypt handle the
UTF-8 conversion.
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
void main() {
final key = randomBytes(32);
final iv = randomBytes(16);
final plain = 'Confidential invoice payload';
// PKCS7 is the common padding choice for block modes like CBC.
final cbc = AES.pkcs7(key).cbc(iv);
final cipher = cbc.encryptString(plain);
final opened = cbc.decrypt(cipher);
print('AES-256-CBC + PKCS7');
print('key : ${toHex(key)}');
print('iv : ${toHex(iv)}');
print('cipher: ${toHex(cipher)}');
print('plain : ${fromUtf8(opened)}');
}
Streaming large data #
Every stream cipher is a StreamTransformer<List<int>, Uint8List>, so a
Stream<List<int>> can be piped through cipher.bind(...) (or
stream.transform(cipher)) and processed chunk by chunk without buffering the
whole message in memory:
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
Future<void> main() async {
final key = randomBytes(32);
final nonce = randomBytes(12);
final chacha = ChaCha20(key, nonce);
final plainChunks = Stream<List<int>>.fromIterable([
toUtf8('a streamed '),
toUtf8('message'),
]);
await for (final encrypted in chacha.bind(plainChunks)) {
print(toHex(encrypted));
}
}
For byte-by-byte Stream<int> sources, use the stream() extension instead of
bind.
Detecting tampering #
Authenticated ciphers reject modified input instead of returning garbage —
flipping a single bit of the tag makes decryption throw a StateError:
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
import 'package:cipherlib/random.dart';
void main() {
final key = randomBytes(32);
final nonce = randomBytes(24);
final sealed = xchacha20poly1305(
toUtf8('integrity protected'),
key,
nonce: nonce,
);
// Flip one bit of the tag to simulate tampering in transit.
final badTag = List<int>.from(sealed.mac.bytes)..[0] ^= 0xff;
try {
xchacha20poly1305(sealed.data, key, nonce: nonce, mac: badTag);
print('accepted (unexpected)');
} on StateError catch (e) {
print('rejected: ${e.message}');
}
}
Post-quantum key exchange (ML-KEM) #
Two parties agree on a shared secret that stays safe even against an attacker with a quantum computer. Alice publishes her encapsulation key; Bob derives a shared secret and a ciphertext from it; Alice recovers the same secret from the ciphertext.
import 'package:cipherlib/cipherlib.dart';
import 'package:cipherlib/codecs.dart';
void main() {
final kem = MLKEM.kem768();
// Alice generates a key pair and publishes the encapsulation key
final alice = kem.keygen();
// Bob encapsulates a shared secret with Alice's public key and
// transmits the ciphertext back to Alice
final bob = kem.encaps(alice.encapsulationKey);
// Alice recovers the same shared secret from the ciphertext
final shared = kem.decaps(alice.decapsulationKey, bob.cipherText);
print(kem.name);
print('bob : ${bob.sharedSecret.hex()}');
print('alice : ${shared.hex()}');
print('match : ${shared.isEqual(bob.sharedSecret)}');
// A key pair can be stored as its 64-byte seed and re-derived later
final seed = List.generate(64, (i) => i);
final first = kem.keygen(seed: seed);
final again = kem.keygen(seed: seed);
final isSeeded =
toHex(first.encapsulationKey) == toHex(again.encapsulationKey);
print('seeded: $isSeeded');
}
The shared secret is a HashDigest; compare it with isEqual (constant-time)
rather than ==, and derive symmetric keys from it with a KDF. A tampered
ciphertext does not throw — ML-KEM performs implicit rejection and returns a
deterministic but unrelated secret, so the two sides simply fail to agree.
Benchmarks #
Libraries #
- Cipherlib : https://pub.dev/packages/cipherlib
- PointyCastle : https://pub.dev/packages/pointycastle
- Cryptography : https://pub.dev/packages/cryptography
- PQCrypto : https://pub.dev/packages/pqcrypto
Stream Ciphers #
| Algorithm | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| XOR | cipherlib | ████████████████ 10.85 Gbps 🌟 |
████████████████ 11.1 Gbps 🌟 |
████████████████ 9.49 Gbps 🌟 |
| Salsa20 | cipherlib | ████████████████ 2.17 Gbps 🌟 |
████████████████ 2.13 Gbps 🌟 |
████████████████ 909 Mbps 🌟 |
| PointyCastle | ███░░░░░░░░░░░░░ 351 Mbps 🔻6.17x |
███░░░░░░░░░░░░░ 344 Mbps 🔻6.2x |
██░░░░░░░░░░░░░░ 133 Mbps 🔻6.84x |
|
| Salsa20 / Poly1305 | cipherlib | ████████████████ 2.16 Gbps 🌟 |
████████████████ 2.02 Gbps 🌟 |
████████████████ 509 Mbps 🌟 |
| XSalsa20 | cipherlib | ████████████████ 2.16 Gbps 🌟 |
████████████████ 2.01 Gbps 🌟 |
████████████████ 517 Mbps 🌟 |
| XSalsa20 / Poly1305 | cipherlib | ████████████████ 2.16 Gbps 🌟 |
████████████████ 1.92 Gbps 🌟 |
████████████████ 360 Mbps 🌟 |
| ChaCha20 | cipherlib | ████████████████ 2.02 Gbps 🌟 |
████████████████ 2 Gbps 🌟 |
████████████████ 856 Mbps 🌟 |
| PointyCastle | ███░░░░░░░░░░░░░ 354 Mbps 🔻5.69x |
███░░░░░░░░░░░░░ 348 Mbps 🔻5.74x |
███░░░░░░░░░░░░░ 135 Mbps 🔻6.33x |
|
| ChaCha20 / Poly1305 | cipherlib | ████████████████ 1.38 Gbps 🌟 |
████████████████ 1.26 Gbps 🌟 |
████████████████ 293 Mbps 🌟 |
| cryptography | ███░░░░░░░░░░░░░ 283 Mbps 🔻4.87x |
███░░░░░░░░░░░░░ 258 Mbps 🔻4.89x |
████░░░░░░░░░░░░ 66.66 Mbps 🔻4.4x |
|
| PointyCastle | ███░░░░░░░░░░░░░ 275 Mbps 🔻5.01x |
███░░░░░░░░░░░░░ 256 Mbps 🔻4.93x |
██████░░░░░░░░░░ 116 Mbps 🔻2.53x |
|
| XChaCha20 | cipherlib | ████████████████ 2.01 Gbps 🌟 |
████████████████ 1.88 Gbps 🌟 |
████████████████ 502 Mbps 🌟 |
| XChaCha20 / Poly1305 | cipherlib | ████████████████ 1.38 Gbps 🌟 |
████████████████ 1.25 Gbps 🌟 |
████████████████ 235 Mbps 🌟 |
AES #
| Algorithm | Library | 1MB message | 1KB message | 32B message |
|---|---|---|---|---|
| AES-128 / CBC | cipherlib | ████████████████ 1.46 Gbps 🌟 |
████████████████ 1.33 Gbps 🌟 |
████████████████ 338 Mbps 🌟 |
| cryptography | ███████████████░ 1.41 Gbps 🔻1.04x |
████████████░░░░ 1.02 Gbps 🔻1.31x |
█████░░░░░░░░░░░ 107 Mbps 🔻3.16x |
|
| PointyCastle | ███░░░░░░░░░░░░░ 231 Mbps 🔻6.3x |
██░░░░░░░░░░░░░░ 203 Mbps 🔻6.53x |
██░░░░░░░░░░░░░░ 44.35 Mbps 🔻7.61x |
|
| AES-192 / CBC | cipherlib | ████████████████ 1.26 Gbps 🌟 |
████████████████ 1.16 Gbps 🌟 |
████████████████ 310 Mbps 🌟 |
| cryptography | ███████████████░ 1.21 Gbps 🔻1.04x |
████████████░░░░ 889 Mbps 🔻1.31x |
█████░░░░░░░░░░░ 95.84 Mbps 🔻3.23x |
|
| PointyCastle | ███░░░░░░░░░░░░░ 201 Mbps 🔻6.27x |
██░░░░░░░░░░░░░░ 177 Mbps 🔻6.57x |
██░░░░░░░░░░░░░░ 39.48 Mbps 🔻7.84x |
|
| AES-256 / CBC | cipherlib | ████████████████ 1.11 Gbps 🌟 |
████████████████ 1.02 Gbps 🌟 |
████████████████ 269 Mbps 🌟 |
| cryptography | ███████████████░ 1.06 Gbps 🔻1.05x |
████████████░░░░ 783 Mbps 🔻1.3x |
█████░░░░░░░░░░░ 86.94 Mbps 🔻3.09x |
|
| PointyCastle | ███░░░░░░░░░░░░░ 178 Mbps 🔻6.22x |
██░░░░░░░░░░░░░░ 157 Mbps 🔻6.47x |
██░░░░░░░░░░░░░░ 36.37 Mbps 🔻7.39x |
|
| AES-128 / CCM | cipherlib | ████████████████ 554 Mbps 🌟 |
████████████████ 524 Mbps 🌟 |
████████████████ 201 Mbps 🌟 |
| PointyCastle | ███░░░░░░░░░░░░░ 112 Mbps 🔻4.96x |
███░░░░░░░░░░░░░ 97.2 Mbps 🔻5.39x |
██░░░░░░░░░░░░░░ 19.61 Mbps 🔻10.24x |
|
| AES-192 / CCM | cipherlib | ████████████████ 492 Mbps 🌟 |
████████████████ 468 Mbps 🌟 |
████████████████ 185 Mbps 🌟 |
| PointyCastle | ███░░░░░░░░░░░░░ 98.09 Mbps 🔻5.01x |
███░░░░░░░░░░░░░ 84.71 Mbps 🔻5.52x |
█░░░░░░░░░░░░░░░ 16.54 Mbps 🔻11.21x |
|
| AES-256 / CCM | cipherlib | ████████████████ 446 Mbps 🌟 |
████████████████ 423 Mbps 🌟 |
████████████████ 165 Mbps 🌟 |
| PointyCastle | ███░░░░░░░░░░░░░ 86.9 Mbps 🔻5.13x |
███░░░░░░░░░░░░░ 75.36 Mbps 🔻5.61x |
█░░░░░░░░░░░░░░░ 15.38 Mbps 🔻10.76x |
|
| AES-128 / CFB | cipherlib | ████████████████ 639 Mbps 🌟 |
████████████████ 629 Mbps 🌟 |
████████████████ 415 Mbps 🌟 |
| PointyCastle | █░░░░░░░░░░░░░░░ 3.66 Mbps 🔻174.44x |
███░░░░░░░░░░░░░ 107 Mbps 🔻5.9x |
██░░░░░░░░░░░░░░ 41.37 Mbps 🔻10.03x |
|
| AES-192 / CFB | cipherlib | ████████████████ 558 Mbps 🌟 |
████████████████ 546 Mbps 🌟 |
████████████████ 385 Mbps 🌟 |
| PointyCastle | █░░░░░░░░░░░░░░░ 3.64 Mbps 🔻153.4x |