fortis 0.1.0
fortis: ^0.1.0 copied to clipboard
High-level cryptography for Dart. Fluent builder API with compile-time safety, sane defaults, and seamless cross-platform interoperability.
Fortis #
High-level cryptography for Dart. Fluent builder API with compile-time safety, sane defaults, and seamless cross-platform interoperability.
Features #
- AES encryption with 7 cipher modes (ECB, CBC, CTR, CFB, OFB, GCM, CCM)
- RSA encryption with 4 padding schemes (PKCS#1 v1.5, OAEP v1/v2/v2.1)
- Fluent builder API with compile-time safety via phantom types
- Automatic IV/nonce generation with cryptographically secure random
- Structured payloads for easy serialization
- Key serialization in PEM, DER, and Base64 formats
- Async key generation using isolates (non-blocking)
- Consistent exception hierarchy for error handling
Learn Cryptography #
New to cryptography or want to understand the concepts behind AES, RSA, padding schemes, and cipher modes? Check out our Cryptography Guide
Quick Start #
import 'package:fortis/fortis.dart';
// AES-256-GCM encryption
final key = await Fortis.aes().generateKey();
final cipher = Fortis.aes().mode(AesMode.gcm).cipher(key);
final ciphertext = cipher.encrypt('Hello, Fortis!');
final plaintext = cipher.decryptToString(ciphertext);
// RSA-OAEP encryption
final pair = await Fortis.rsa().generateKeyPair();
final encrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2)
.hash(RsaHash.sha256)
.encrypter(pair.publicKey);
final decrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2)
.hash(RsaHash.sha256)
.decrypter(pair.privateKey);
final encrypted = encrypter.encrypt('Hello, Fortis!');
final decrypted = decrypter.decryptToString(encrypted);
AES (Advanced Encryption Standard) #
Supported Modes #
| Mode | Type | IV/Nonce | Description |
|---|---|---|---|
ECB |
Block | None | Electronic Code Book — no IV, not recommended for production |
CBC |
Block | 16 bytes | Cipher Block Chaining |
CTR |
Stream | 16 bytes | Counter Mode |
CFB |
Stream | 16 bytes | Cipher Feedback |
OFB |
Stream | 16 bytes | Output Feedback |
GCM |
Authenticated | Configurable (default 12) | Galois/Counter Mode with authentication tag |
CCM |
Authenticated | Configurable 7–13 bytes (default 11) | Counter with CBC-MAC |
Supported Padding (Block Modes Only) #
Padding applies only to ECB and CBC modes. Stream and authenticated modes do not use padding.
| Padding | Description |
|---|---|
pkcs7 |
PKCS#7 — standard, recommended |
iso7816 |
ISO 7816-4 — 0x80 followed by zero bytes |
zeroPadding |
Zero-byte padding — ambiguous, legacy use only |
noPadding |
No padding — data must be 16-byte aligned |
Key Sizes #
128, 192, and 256 bits. Default is 256 bits.
Key Generation #
// Generate a random key (default 256-bit)
final key = await Fortis.aes().generateKey();
// Generate with specific size
final key128 = await Fortis.aes().keySize(128).generateKey();
final key192 = await Fortis.aes().keySize(192).generateKey();
// Create from existing bytes
final key = FortisAesKey.fromBytes(myBytes);
// Serialize / deserialize
final base64 = key.toBase64();
final restored = FortisAesKey.fromBase64(base64);
Block Modes (ECB, CBC) #
// CBC with PKCS7 padding
final cipher = Fortis.aes()
.mode(AesMode.cbc)
.padding(AesPadding.pkcs7)
.cipher(key);
final ciphertext = cipher.encrypt('secret message');
final plaintext = cipher.decryptToString(ciphertext);
// With explicit IV
final ciphertext = cipher.encrypt('secret message', iv: myIv);
Stream Modes (CTR, CFB, OFB) #
// CTR mode — no padding needed
final cipher = Fortis.aes()
.mode(AesMode.ctr)
.cipher(key);
final ciphertext = cipher.encrypt('secret message');
final plaintext = cipher.decryptToString(ciphertext);
Authenticated Modes (GCM, CCM) #
GCM and CCM provide both encryption and integrity verification via an authentication tag.
// GCM with default settings
final cipher = Fortis.aes()
.mode(AesMode.gcm)
.cipher(key);
final ciphertext = cipher.encrypt('secret message');
final plaintext = cipher.decryptToString(ciphertext);
Additional Authenticated Data (AAD)
final aad = Uint8List.fromList(utf8.encode('metadata'));
final cipher = Fortis.aes()
.mode(AesMode.gcm)
.aad(aad)
.cipher(key);
// AAD must match during decryption
final ciphertext = cipher.encrypt('secret message');
final plaintext = cipher.decryptToString(ciphertext);
Custom Nonce Size
// GCM with custom nonce size
final cipher = Fortis.aes()
.mode(AesMode.gcm)
.nonceSize(16)
.cipher(key);
// CCM with custom nonce size (7–13 bytes)
final cipher = Fortis.aes()
.mode(AesMode.ccm)
.nonceSize(13)
.cipher(key);
Payloads #
encryptToPayload returns a structured object for easy serialization:
// Authenticated modes return AesAuthPayload
final payload = cipher.encryptToPayload('hello') as AesAuthPayload;
print(payload.iv); // Base64-encoded nonce
print(payload.data); // Base64-encoded ciphertext
print(payload.tag); // Base64-encoded authentication tag
print(payload.toMap()); // {'iv': '...', 'data': '...', 'tag': '...'}
// Non-authenticated modes return AesPayload
final payload = cipher.encryptToPayload('hello') as AesPayload;
print(payload.iv); // Base64-encoded IV
print(payload.data); // Base64-encoded ciphertext
print(payload.toMap()); // {'iv': '...', 'data': '...'}
Decryption Input Formats #
The decrypt method accepts multiple input types:
// From Uint8List (raw bytes)
cipher.decrypt(ciphertextBytes);
// From String (Base64-encoded)
cipher.decrypt(base64String);
// From Map
cipher.decrypt({'iv': '...', 'data': '...'});
cipher.decrypt({'nonce': '...', 'data': '...', 'tag': '...'});
// From payload object
cipher.decrypt(payload);
RSA (Rivest-Shamir-Adleman) #
Supported Padding Schemes #
| Padding | Description |
|---|---|
pkcs1_v1_5 |
PKCS#1 v1.5 — legacy, widely supported |
oaep_v1 |
OAEP with SHA-1 |
oaep_v2 |
OAEP with configurable hash and MGF1 |
oaep_v2_1 |
OAEP with configurable hash, MGF1, and label support |
Supported Hash Algorithms #
| Hash | Bits |
|---|---|
sha1 |
160 |
sha224 |
224 |
sha256 |
256 |
sha384 |
384 |
sha512 |
512 |
sha3_256 |
256 |
sha3_512 |
512 |
Key Sizes #
Minimum 2048 bits, must be a power of 2 (2048, 4096, 8192, ...). Default is 2048 bits.
Key Pair Generation #
// Generate with default size (2048-bit)
final pair = await Fortis.rsa().generateKeyPair();
// Generate with specific size
final pair = await Fortis.rsa().keySize(4096).generateKeyPair();
final publicKey = pair.publicKey;
final privateKey = pair.privateKey;
Key Serialization #
Public Key
// PEM format
final pem = publicKey.toPem(); // X.509 (default)
final pem = publicKey.toPem(format: RsaPublicKeyFormat.pkcs1);
// DER format
final der = publicKey.toDer();
final derBase64 = publicKey.toDerBase64();
// Import
final key = FortisRsaPublicKey.fromPem(pemString);
final key = FortisRsaPublicKey.fromDer(derBytes);
final key = FortisRsaPublicKey.fromDerBase64(base64String);
// With specific format
final key = FortisRsaPublicKey.fromPem(pem, format: RsaPublicKeyFormat.pkcs1);
Private Key
// PEM format
final pem = privateKey.toPem(); // PKCS#8 (default)
final pem = privateKey.toPem(format: RsaPrivateKeyFormat.pkcs1);
// DER format
final der = privateKey.toDer();
final derBase64 = privateKey.toDerBase64();
// Import
final key = FortisRsaPrivateKey.fromPem(pemString);
final key = FortisRsaPrivateKey.fromDer(derBytes);
final key = FortisRsaPrivateKey.fromDerBase64(base64String);
// With specific format
final key = FortisRsaPrivateKey.fromPem(pem, format: RsaPrivateKeyFormat.pkcs1);
Encryption & Decryption #
// OAEP v2 with SHA-256
final encrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2)
.hash(RsaHash.sha256)
.encrypter(pair.publicKey);
final decrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2)
.hash(RsaHash.sha256)
.decrypter(pair.privateKey);
// Encrypt
final ciphertext = encrypter.encrypt('Hello, Fortis!');
final ciphertextBase64 = encrypter.encryptToString('Hello, Fortis!');
// Decrypt
final plaintext = decrypter.decrypt(ciphertext);
final text = decrypter.decryptToString(ciphertext);
// Also accepts Uint8List input
final ciphertext = encrypter.encrypt(Uint8List.fromList([1, 2, 3]));
PKCS#1 v1.5 #
final encrypter = Fortis.rsa()
.padding(RsaPadding.pkcs1_v1_5)
.hash(RsaHash.sha256)
.encrypter(pair.publicKey);
final decrypter = Fortis.rsa()
.padding(RsaPadding.pkcs1_v1_5)
.hash(RsaHash.sha256)
.decrypter(pair.privateKey);
OAEP v2.1 with Label #
OAEP v2.1 supports an optional label for domain separation:
// With string label
final encrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2_1)
.hash(RsaHash.sha256)
.encrypter(pair.publicKey, label: 'my-context');
final decrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2_1)
.hash(RsaHash.sha256)
.decrypter(pair.privateKey, label: 'my-context');
// With Uint8List label
final encrypter = Fortis.rsa()
.padding(RsaPadding.oaep_v2_1)
.hash(RsaHash.sha256)
.encrypter(pair.publicKey, label: Uint8List.fromList([1, 2, 3]));
Error Handling #
Fortis uses a consistent exception hierarchy:
| Exception | Description |
|---|---|
FortisException |
Abstract base class for all Fortis errors |
FortisConfigException |
Invalid configuration (e.g., unsupported key size, invalid mode/padding combination) |
FortisKeyException |
Key-related errors (e.g., invalid key bytes, failed PEM/DER parsing) |
FortisEncryptionException |
Encryption/decryption failures (e.g., tampered ciphertext, wrong key, AAD mismatch) |
try {
final plaintext = cipher.decrypt(ciphertext);
} on FortisEncryptionException catch (e) {
print('Decryption failed: ${e.message}');
} on FortisException catch (e) {
print('Fortis error: ${e.message}');
}
License #
See LICENSE for details.