Skip to main content

cipherlib 0.7.2 copy "cipherlib: ^0.7.2" to clipboard
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 #

package version dart support likes pub points codecov Test Ask DeepWiki

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:

convertlib → hashlib → cipherlib

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 a StreamTransformer.
  • Post-quantum ready: ML-KEM-512/768/1024 (FIPS 203) for quantum-resistant key exchange.
  • Fast: consistently outruns PointyCastle and cryptography, 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 Codebook
  • CBC - Cipher Block Chaining
  • CTR - Counter
  • GCM - Galois/Counter Mode (authenticated)
  • CCM - Counter with CBC-MAC (authenticated)
  • CFB - Cipher Feedback
  • OFB - Output Feedback
  • IGE - Infinite Garble Extension
  • PCBC - Propagating Cipher Block Chaining
  • XTS - XEX Tweakable Block Cipher with Ciphertext Stealing

Blowfish and Twofish support 2 modes:

  • ECB - Electronic Codebook
  • CBC - Cipher Block Chaining
  • CTR - 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 #

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