fortis 0.3.0 copy "fortis: ^0.3.0" to clipboard
fortis: ^0.3.0 copied to clipboard

High-level cryptography for Dart. Fluent builder API with compile-time safety, sane defaults, and seamless cross-platform interoperability.

Fortis #

pub package package publisher

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)
  • ECDH key agreement with NIST curves (P-256, P-384, P-521) and HKDF-SHA256 derivation
  • Fluent builder API with compile-time safety — phantom types for RSA and sealed cipher variants for AES
  • Automatic IV/nonce generation with cryptographically secure random
  • Structured payloads for easy serialization
  • Key serialization in PEM, DER, and Base64 formats
  • Async key generation (non-blocking via Isolate on VM/AOT; runs on the main thread on Flutter web, where dart:isolate is unavailable)
  • Consistent exception hierarchy for error handling

Platform support #

Platform Supported
Dart VM (CLI / servers)
Flutter mobile (iOS, Android)
Flutter desktop (macOS, Windows, Linux)
Flutter web (dart2js, dart2wasm) ✅ (see note below)

On Flutter web, dart:isolate is not available. Key-generation calls (Fortis.aes().generateKey(), Fortis.ecdh().generateKeyPair(), Fortis.rsa().generateKeyPair()) still return a Future but execute synchronously on the main thread. AES and ECDH are effectively instantaneous; RSA ≥ 2048 bits can freeze the UI for seconds — a FortisLog warning is emitted to flag it. Consider pre-generating RSA keys server-side or offloading to a Web Worker if that matters.

Installation #

dart pub add fortis

For a runnable end-to-end tour of the API, see example/example.dart.

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().gcm().cipher(key); // AesAuthCipher

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);

// ECDH → shared AES key
final alice = await Fortis.ecdh().generateKeyPair();
final bob = await Fortis.ecdh().generateKeyPair();

final sharedKey = Fortis.ecdh()
    .keyDerivation(alice.privateKey)
    .deriveAesKey(bob.publicKey);

final sharedCipher = Fortis.aes().gcm().cipher(sharedKey);

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()
    .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().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().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()
    .gcm()
    .aad(aad)
    .cipher(key);

// AAD must match during decryption
final ciphertext = cipher.encrypt('secret message');
final plaintext = cipher.decryptToString(ciphertext);

Custom IV Size

// GCM with custom IV size
final cipher = Fortis.aes().gcm().ivSize(16).cipher(key);

// CCM with custom IV size (7–13 bytes)
final cipher = Fortis.aes().ccm().ivSize(13).cipher(key);

Custom Tag Size (CCM only)

GCM tags are fixed at 128 bits. CCM accepts {32, 48, 64, 80, 96, 112, 128} per NIST SP 800-38C — validated up front, invalid values throw FortisConfigException.

final cipher = Fortis.aes().ccm().tagSize(96).cipher(key);

Payloads #

encryptToPayload returns a structured object for easy serialization. Use the typed shortcuts (.gcm(), .ccm(), .cbc(), .ctr(), .cfb(), .ofb()) to get the concrete cipher type — the payload type is inferred statically, no cast required.

// Authenticated modes (GCM/CCM) → AesAuthCipher → AesAuthPayload
final gcm = Fortis.aes().gcm().cipher(key);
final authPayload = gcm.encryptToPayload('hello');
print(authPayload.iv);   // Base64-encoded nonce
print(authPayload.data); // Base64-encoded ciphertext
print(authPayload.tag);  // Base64-encoded authentication tag
print(authPayload.toMap()); // {'iv': '...', 'data': '...', 'tag': '...'}

// Non-authenticated modes (CBC/CTR/CFB/OFB) → AesStandardCipher → AesPayload
final cbc = Fortis.aes().cbc().cipher(key);
final payload = cbc.encryptToPayload('hello');
print(payload.iv);   // Base64-encoded IV
print(payload.data); // Base64-encoded ciphertext
print(payload.toMap()); // {'iv': '...', 'data': '...'}

When the mode is only known at runtime, use Fortis.aes().mode(runtimeMode).cipher(key) — it returns the sealed base AesCipher; pattern-match or cast to the concrete variant before calling encryptToPayload.

Cross-platform Interoperability #

The authenticated payload maps directly to the {iv|nonce, data, tag} wire format used by .NET, Java, Node.js, and OpenSSL:

final cipher = Fortis.aes().gcm().cipher(key);
final ct = cipher.encrypt('hello fortis');

// Split the combined buffer (iv | ciphertext | tag) for custom transports:
final iv = base64Encode(ct.sublist(0, 12));
final tag = base64Encode(ct.sublist(ct.length - 16));
final data = base64Encode(ct.sublist(12, ct.length - 16));

// Decrypt accepts the same fields back via a Map (or AesAuthPayload):
final plain = cipher.decryptToString({'nonce': iv, 'data': data, 'tag': tag});

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]));

ECDH (Elliptic Curve Diffie–Hellman) #

Supported Curves #

Curve Security Level Description
p256 128-bit NIST P-256 (default)
p384 192-bit NIST P-384
p521 256-bit NIST P-521

Key Pair Generation #

// Default (P-256)
final pair = await Fortis.ecdh().generateKeyPair();

// Specific curve
final pair = await Fortis.ecdh().curve(EcdhCurve.p384).generateKeyPair();

final publicKey = pair.publicKey;
final privateKey = pair.privateKey;

Key Serialization #

// Public key — PEM (X.509), DER, Base64
final pem = publicKey.toPem();
final der = publicKey.toDer();
final derBase64 = publicKey.toDerBase64();

// Private key — PEM defaults to PKCS#8; SEC1 available for interop
final pem = privateKey.toPem();
final sec1 = privateKey.toPem(format: EcdhPrivateKeyFormat.sec1);
final der = privateKey.toDer();

// Import
final publicKey = FortisEcdhPublicKey.fromPem(pemString);
final privateKey = FortisEcdhPrivateKey.fromPem(pemString);
final publicKey = FortisEcdhPublicKey.fromDerBase64(base64String);

// Raw uncompressed point (interop with WebCrypto / JWK / most JS crypto libs)
final raw = publicKey.toDer(format: EcdhPublicKeyFormat.uncompressedPoint);
final restored = FortisEcdhPublicKey.fromDer(
  raw,
  format: EcdhPublicKeyFormat.uncompressedPoint,
  curve: EcdhCurve.p256,
);

Deriving a Shared AES Key #

Classic ECDH handshake — each peer generates a key pair, exchanges public keys, and derives the same AES key via HKDF-SHA256.

// Alice and Bob each generate a key pair
final alice = await Fortis.ecdh().generateKeyPair();
final bob = await Fortis.ecdh().generateKeyPair();

// Alice derives the shared AES key (default 256-bit)
final aliceKey = Fortis.ecdh()
    .keyDerivation(alice.privateKey)
    .deriveAesKey(bob.publicKey);

// Bob derives the same key independently
final bobKey = Fortis.ecdh()
    .keyDerivation(bob.privateKey)
    .deriveAesKey(alice.publicKey);

// Use directly with Fortis.aes()
final cipher = Fortis.aes().gcm().cipher(aliceKey);

Advanced Derivation #

// Raw derived bytes with HKDF (configurable size, salt, and info)
final bytes = Fortis.ecdh()
    .keySize(512)
    .keyDerivation(myPrivateKey)
    .deriveKey(
      theirPublicKey,
      salt: sessionSalt,
      info: utf8.encode('fortis/session-v1'),
    );

// Raw shared secret (without HKDF) — for protocols with their own KDF
final secret = Fortis.ecdh()
    .keyDerivation(myPrivateKey)
    .deriveSharedSecret(theirPublicKey);

// Pre-shared secret → AES key (static utility)
final aesKey = EcdhKeyDerivation.hkdfDeriveAesKey(
  preSharedSecret,
  salt: sessionSalt,
  info: utf8.encode('fortis/aes-key'),
);

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}');
}

Common failure modes and the matching exception type:

// AAD mismatch or tampered ciphertext → FortisEncryptionException
final gcm = Fortis.aes().gcm().aad(aadA).cipher(key);
final ct = gcm.encrypt('hello');

try {
  Fortis.aes().gcm().aad(aadB).cipher(key).decryptToString(ct); // wrong AAD
} on FortisEncryptionException catch (e) {
  print('Integrity check failed: ${e.message}');
}

// Invalid Base64 input → FortisConfigException (wrapped at the API boundary)
try {
  gcm.decrypt('!!!not base64!!!');
} on FortisConfigException catch (e) {
  print('Invalid input: ${e.message}');
}

License #

See LICENSE for details.

1
likes
160
points
312
downloads

Documentation

API reference

Publisher

verified publisheredunatalec.com

Weekly Downloads

High-level cryptography for Dart. Fluent builder API with compile-time safety, sane defaults, and seamless cross-platform interoperability.

Repository (GitHub)
View/report issues

Topics

#cryptography #encryption #security

License

MIT (license)

Dependencies

pointycastle

More

Packages that depend on fortis