at_chops 3.6.0
at_chops: ^3.6.0 copied to clipboard
Package for at_protocol cryptographic and hashing operations
Package for Cryptographic and Hashing Operations (CHOPS) such as encryption, decryption, data signing, key agreement, and hashing that can be leveraged by client applications using the Atsign Protocol.
Features #
- Asymmetric encryption/decryption using RSA-2048 and RSA-4096
- Symmetric encryption/decryption using AES-128, AES-192, and AES-256 (CTR and GCM modes)
- Digest signing and verification for PKAM authentication (RSA, ECC secp256r1, Ed25519)
- Data signing and verification for public data in the Atsign Protocol
- Post-quantum digital signatures: ML-DSA-65 (FIPS 204) — pure-Dart and OpenSSL FFI backends
- Post-quantum key encapsulation: ML-KEM-768 (FIPS 203) — pure-Dart and OpenSSL FFI backends; ML-KEM-1024 (FIPS 203, IANA HPKE KEM id 0x0042) — pure-Dart
- Hybrid PQ/classical KEM: X-Wing (X25519 + ML-KEM-768, IANA HPKE KEM id 0x647A)
- Elliptic-curve key agreement: X25519 — pure-Dart and OpenSSL FFI backends
- Seed-based KEM key persistence (
newSeed/keyPairFromSeed) that means the same thing on every backend - Serializable key-pair generation helpers for RSA, X25519, ML-KEM-768, ML-DSA-65, and X-Wing
- Hashing: SHA-256, SHA-512, MD5, Argon2id
- HKDF key derivation
Getting started #
Developers should have a basic understanding of asymmetric and symmetric encryption, as well as key encapsulation mechanisms (KEMs) for the PQC APIs.
Use the algorithm classes directly. Generate or load key material first, then pass it to the relevant encryption, signing, key agreement, KEM, or hashing class.
Usage #
Examples assume package:at_chops/at_chops.dart is imported. Snippets using
utf8 or Uint8List also require dart:convert or dart:typed_data.
Serializable key generation #
Use these helpers when the key material needs to fit the SDK's string-backed key
types (AtPublicKey, AtPrivateKey, and SymmetricKey). Byte-oriented key
pairs are base64-encoded by the wrapper classes.
final aes128 = AESKey.generate(16);
final aes192 = AESKey.generate(24);
final aes256 = AESKey.generate(32);
final rsa2048 = RsaKeyPair.generate();
final rsa4096 = RsaKeyPair.generate(keySize: 4096);
final x25519 = await X25519KeyPair.generate();
final mlKem768 = await MlKem768KeyPair.generate();
final mlDsa65 = await MlDsa65KeyPair.generate();
final xWing = await XWingKeyPair.generate();
RSA encryption #
final keyPair = RsaKeyPair.generate();
final rsa = RsaEncryptionAlgo.fromKeyPair(keyPair);
final message = Uint8List.fromList(utf8.encode('Hello World'));
final encrypted = rsa.encrypt(message);
final decrypted = rsa.decrypt(encrypted);
AES encryption #
final aesKey = AESKey.generate(32);
final iv = InitialisationVector.random(16);
final aes = AESEncryptionAlgo(aesKey);
final message = Uint8List.fromList(utf8.encode('Hello World'));
final encrypted = await aes.encrypt(message, iv: iv);
final decrypted = await aes.decrypt(encrypted, iv: iv);
Signing and verification #
final keyPair = RsaKeyPair.generate();
final signing = RsaSigningAlgo(keyPair, HashingAlgoType.sha256);
final message = Uint8List.fromList(utf8.encode('data to sign'));
final signature = signing.sign(message);
final valid = signing.verify(message, signature);
ML-DSA-65 (post-quantum signing, pure-Dart) #
final mlDsa65 = MlDsa65PureDartAlgo();
final kp = await mlDsa65.generateKeyPair();
// kp.publicKey — 1952 bytes; kp.secretKey — 4032 bytes
final signature = await mlDsa65.signBytes(message, secretKey: kp.secretKey);
final valid =
await mlDsa65.verifyBytes(message, signature: signature, publicKey: kp.publicKey);
ML-KEM-768 (post-quantum KEM, pure-Dart) #
final kem = MlKem768PureDartAlgo.instance;
final kp = await kem.generateKeyPair();
// kp.publicKey — 1184 bytes
// kp.secretKey — 2400 bytes, the expanded decapsulation key. NOT a seed:
// see "Persisting a KEM key" below before storing it.
// Sender
final (ciphertext: ct, sharedSecret: ss1) = await kem.encapsulate(kp.publicKey);
// Receiver
final ss2 = await kem.decapsulate(kp.secretKey, ct);
// ss1 == ss2
ML-KEM-1024 (post-quantum KEM, pure-Dart) #
The no-hybrid option, for callers whose specification chain has to contain no draft — FIPS 203 for the KEM itself — and the parameter set CNSA 2.0 mandates. Same interface, larger sizes; its ciphertext is 448 bytes bigger than X-Wing's, which is the cost that shows up per sealed record.
final kem = MlKem1024PureDartAlgo.instance;
final kp = await kem.generateKeyPair();
// kp.publicKey — 1568 bytes; kp.secretKey — 3168 bytes (expanded, not a seed)
final (ciphertext: ct, sharedSecret: ss1) = await kem.encapsulate(kp.publicKey);
final ss2 = await kem.decapsulate(kp.secretKey, ct);
// ss1 == ss2
X-Wing (hybrid PQ/classical KEM) #
final xwing = XWingPureDartAlgo.instance;
final kp = await xwing.generateKeyPair();
// kp.publicKey — 1216 bytes; kp.secretKey — 32 bytes, which for THIS backend
// happens to be the seed. Don't generalise that — see below.
// Sender
final (ciphertext: ct, sharedSecret: ss1) = await xwing.encapsulate(kp.publicKey);
// Receiver
final ss2 = await xwing.decapsulate(kp.secretKey, ct);
// ss1 == ss2
Persisting a KEM key #
generateKeyPair's secretKey is what decapsulate takes, and it does not
mean the same thing on every backend: X-Wing's is its 32-byte seed, ML-KEM's is
the expanded decapsulation key that no seeded call reproduces, and the FFI
backends' is an opaque handle that does not outlive the process. So code written
against X-Wing persists recoverable bytes by accident, and the identical code
persists unrecoverable ones for ML-KEM — with no compile error, and no failure
until a restart.
Store the seed and re-derive. That is correct on every backend, and it is the only form in which a caller can hold a key for a KEM it does not name in source.
// Any backend — the two calls mean the same thing on all of them.
final AtKemAlgorithm kem = XWingPureDartAlgo.instance;
// Minting: keep the seed, not the secret key.
final seed = kem.newSeed();
final kp = await kem.keyPairFromSeed(seed);
// Recovering, after a restart — from the stored seed:
final recovered = await kem.keyPairFromSeed(seed);
// recovered.publicKey == kp.publicKey
Seed lengths are backend-specific (32 bytes for X-Wing, 64 for ML-KEM's
d || z) and deliberately absent from the interface: newSeed makes a valid
one and keyPairFromSeed rejects an invalid one. Concrete classes expose
seedLength for callers that do name a backend. Store the algorithm alongside
the seed — 32 and 64 bytes are both valid for some backend, so the bytes alone
cannot say which.
AtPqc (auto-resolved PQ backends) #
AtPqc is the recommended entry point for PQ crypto — it auto-resolves the
OpenSSL FFI backend when libcrypto supports it, falling back to pure-Dart
otherwise, so callers don't need to pick a backend by hand. Import
package:at_chops/at_chops_ffi.dart to access it.
import 'package:at_chops/at_chops_ffi.dart';
// Signing — AtPqc.mlDsa65 is typed as AtSignatureAlgorithm
final kp = await MlDsa65KeyPair.generate();
final signature =
await AtPqc.mlDsa65.signBytes(message, secretKey: kp.privateKeyBytes);
final valid = await AtPqc.mlDsa65.verifyBytes(message,
signature: signature, publicKey: kp.publicKeyBytes);
// KEM — AtPqc.xWing is typed as AtKemAlgorithm
final xwKp = await XWingKeyPair.generate();
final (ciphertext: ct, sharedSecret: ss1) = await AtPqc.xWing.encapsulate(xwKp.publicKeyBytes);
final ss2 = await AtPqc.xWing.decapsulate(xwKp.privateKeyBytes, ct);
// ss1 == ss2
X25519 (Diffie–Hellman, pure-Dart) #
final x25519 = X25519PureDartAlgo.instance;
final kpA = await x25519.generateKeyPair();
final kpB = await x25519.generateKeyPair();
final ssA = await x25519.dh(kpA.privateKey, kpB.publicKey);
final ssB = await x25519.dh(kpB.privateKey, kpA.publicKey);
// ssA == ssB
Hashing #
final hash = SHA512HashingAlgo().hash('some-data'.codeUnits);
FFI backends #
ML-DSA-65, ML-KEM-768, and X25519 each have an OpenSSL FFI backend (MlDsa65FfiAlgo, MlKem768FfiAlgo, X25519FfiAlgo) that requires libcrypto to be installed. The pure-Dart backends (*PureDartAlgo) work on all platforms without native dependencies.
X-Wing (XWingFfiAlgo) composes the FFI backends for maximum performance when libcrypto is available.
AES-256-GCM also has an OpenSSL FFI backend (AesGcm256FfiAlgo) alongside its pure-Dart counterpart (AesGcm256EncryptionAlgo); the two are fully interoperable. AtPqc.aesGcm256(key) auto-selects FFI or pure-Dart when AAD is not needed. If you need AAD (e.g. for PQ-HPKE), construct AesGcm256FfiAlgo.fromLib(lib, key) or AesGcm256EncryptionAlgo(key) directly — both expose encrypt/decrypt with {List<int> aad}.
FFI backends are exported from package:at_chops/at_chops_ffi.dart, not the
main at_chops.dart barrel, so pure-Dart-only consumers aren't forced to
carry FFI bindings. Use AtPqc instead of
picking an FFI/pure-Dart backend directly when possible.
Running Tests #
Some tests require libcrypto.so to be installed. Running dart test without it will fail those tests.
To run all tests EXCEPT the FFI tests:
dart test --exclude-tags ffi
To run ONLY the FFI tests:
dart test --tags ffi