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.

example/example.dart

import 'dart:convert';
import 'dart:typed_data';

import 'package:fortis/fortis.dart';

Future<void> main() async {
  await aesKeyExample();
  await aesEncryptionExample();
  await aesAuthenticatedExample();
  await ccmTagSizeExample();
  await aesPayloadExample();
  await aesDecryptInputFormatsExample();
  await rsaBasicExample();
  await rsaPaddingHashMatrixExample();
  await rsaLabelExample();
  await rsaKeySerializationExample();
  await ecdhBasicExample();
  await ecdhKeySerializationExample();
  await ecdhAdvancedDerivationExample();
  await errorHandlingExample();
}

Future<void> aesKeyExample() async {
  final key128 = await Fortis.aes().keySize(128).generateKey();
  final key192 = await Fortis.aes().keySize(192).generateKey();
  final key256 = await Fortis.aes().keySize(256).generateKey();

  print('AES-128 key size: ${key128.keySize}');
  print('AES-192 key size: ${key192.keySize}');
  print('AES-256 key size: ${key256.keySize}');

  // From existing bytes
  final key = FortisAesKey.fromBytes(Uint8List(32));
  print('Key from bytes: ${key.keySize}-bit');

  // Base64 round-trip
  final b64 = key256.toBase64();
  final restored = FortisAesKey.fromBase64(b64);
  print('Key restored: ${restored.keySize}-bit');
}

Future<void> aesEncryptionExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();

  // CBC (block mode) — requires padding
  final cbc = Fortis.aes().cbc().cipher(key);
  final cbcCiphertext = cbc.encrypt('hello fortis');
  print('AES-CBC decrypted: ${cbc.decryptToString(cbcCiphertext)}');

  // CTR (stream mode) — no padding needed
  final ctr = Fortis.aes().ctr().cipher(key);
  final ctrCiphertext = ctr.encrypt('hello fortis');
  print('AES-CTR decrypted: ${ctr.decryptToString(ctrCiphertext)}');

  // CFB (stream mode)
  final cfb = Fortis.aes().cfb().cipher(key);
  final cfbCiphertext = cfb.encrypt('hello fortis');
  print('AES-CFB decrypted: ${cfb.decryptToString(cfbCiphertext)}');

  // OFB (stream mode)
  final ofb = Fortis.aes().ofb().cipher(key);
  final ofbCiphertext = ofb.encrypt('hello fortis');
  print('AES-OFB decrypted: ${ofb.decryptToString(ofbCiphertext)}');

  // ECB (block mode) — no IV
  final ecb = Fortis.aes().ecb().cipher(key);
  final ecbCiphertext = ecb.encrypt('hello fortis');
  print('AES-ECB decrypted: ${ecb.decryptToString(ecbCiphertext)}');

  // Encrypt with explicit IV (deterministic output)
  final cipher = Fortis.aes().gcm().cipher(key);
  final iv = Uint8List(12);
  final r1 = cipher.encrypt('hello', iv: iv);
  final r2 = cipher.encrypt('hello', iv: iv);
  print('Deterministic IV produces same output: ${r1.length == r2.length}');

  // encryptToString returns Base64
  final b64 = cipher.encryptToString('hello fortis');
  print('Base64 ciphertext valid: ${base64Decode(b64).isNotEmpty}');

  // Two ciphers with same key are compatible
  final cipher1 = Fortis.aes().gcm().cipher(key);
  final cipher2 = Fortis.aes().gcm().cipher(key);
  final ciphertext = cipher1.encrypt('hello fortis');
  print('Cross-cipher decrypt: ${cipher2.decryptToString(ciphertext)}');
}

Future<void> aesAuthenticatedExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();

  // GCM — default IV size (12 bytes)
  final gcm = Fortis.aes().mode(AesMode.gcm).cipher(key);
  final gcmCiphertext = gcm.encrypt('hello fortis');
  print('AES-GCM decrypted: ${gcm.decryptToString(gcmCiphertext)}');

  // CCM — default IV size (11 bytes)
  final ccm = Fortis.aes().mode(AesMode.ccm).cipher(key);
  final ccmCiphertext = ccm.encrypt('hello fortis');
  print('AES-CCM decrypted: ${ccm.decryptToString(ccmCiphertext)}');

  // GCM with AAD (Additional Authenticated Data)
  final aad = Uint8List.fromList(utf8.encode('additional-data'));
  final gcmWithAad = Fortis.aes().gcm().aad(aad).cipher(key);
  final aadCiphertext = gcmWithAad.encrypt('hello fortis');
  print('AES-GCM+AAD decrypted: ${gcmWithAad.decryptToString(aadCiphertext)}');

  // GCM with custom IV size
  final gcmCustom = Fortis.aes().gcm().ivSize(8).cipher(key);
  final customCiphertext = gcmCustom.encrypt('hello fortis');
  print(
    'AES-GCM iv=8 decrypted: ${gcmCustom.decryptToString(customCiphertext)}',
  );

  // CCM with custom IV sizes (7–13 bytes)
  for (final size in [7, 11, 13]) {
    final ccmCustom = Fortis.aes().ccm().ivSize(size).cipher(key);
    final ct = ccmCustom.encrypt('hello fortis');

    print('AES-CCM iv=$size decrypted: ${ccmCustom.decryptToString(ct)}');
  }
}

Future<void> aesPayloadExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();

  // Authenticated payload (GCM/CCM) — has iv, data, tag.
  // .gcm() returns AesAuthCipher, so encryptToPayload() is statically
  // typed as AesAuthPayload — no cast required.
  final gcm = Fortis.aes().gcm().cipher(key);
  final authPayload = gcm.encryptToPayload('hello');
  print('Auth payload iv: ${authPayload.iv}');
  print('Auth payload data: ${authPayload.data}');
  print('Auth payload tag: ${authPayload.tag}');
  print('Auth payload map: ${authPayload.toMap()}');
  print('Auth payload map (nonce): ${authPayload.toMap(ivKey: 'nonce')}');

  // Non-authenticated payload (CBC/CTR/CFB/OFB) — has iv, data.
  // .cbc() returns AesStandardCipher → encryptToPayload() is AesPayload.
  final cbc = Fortis.aes().cbc().cipher(key);
  final payload = cbc.encryptToPayload('hello');
  print('Payload iv: ${payload.iv}');
  print('Payload data: ${payload.data}');
  print('Payload map: ${payload.toMap()}');

  // Decrypt from payload object
  print('Decrypt from AesAuthPayload: ${gcm.decryptToString(authPayload)}');
  print('Decrypt from AesPayload: ${cbc.decryptToString(payload)}');
}

Future<void> aesDecryptInputFormatsExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();
  final cipher = Fortis.aes().gcm().cipher(key);
  const plaintext = 'hello fortis';

  // From Uint8List
  final bytes = cipher.encrypt(plaintext);
  print('From Uint8List: ${cipher.decryptToString(bytes)}');

  // From Base64 String
  final b64 = cipher.encryptToString(plaintext);
  print('From Base64: ${cipher.decryptToString(b64)}');

  // From Map with 'iv' key
  final payload = cipher.encryptToPayload(plaintext);
  print('From Map (iv): ${cipher.decryptToString(payload.toMap())}');

  // From Map with 'nonce' key
  print(
    'From Map (nonce): ${cipher.decryptToString(payload.toMap(ivKey: 'nonce'))}',
  );

  // From AesAuthPayload object
  print('From AesAuthPayload: ${cipher.decryptToString(payload)}');

  // Interop: decrypt .NET-style separated fields
  final ciphertext = cipher.encrypt(plaintext);
  final iv = base64Encode(ciphertext.sublist(0, 12));
  final tag = base64Encode(ciphertext.sublist(ciphertext.length - 16));
  final data = base64Encode(ciphertext.sublist(12, ciphertext.length - 16));
  print(
    'Interop (.NET-style): ${cipher.decryptToString({'nonce': iv, 'data': data, 'tag': tag})}',
  );
}

Future<void> rsaBasicExample() async {
  final pair = await Fortis.rsa().keySize(2048).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);

  // Encrypt String
  final ciphertext = encrypter.encrypt('hello fortis');
  print('RSA decrypt (bytes): ${decrypter.decryptToString(ciphertext)}');

  // Encrypt Uint8List
  final bytesInput = Uint8List.fromList([1, 2, 3, 4, 5]);
  final bytesCiphertext = encrypter.encrypt(bytesInput);
  print('RSA decrypt (Uint8List): ${decrypter.decrypt(bytesCiphertext)}');

  // encryptToString → decryptToString
  final b64 = encrypter.encryptToString('hello fortis');
  print('RSA Base64 round-trip: ${decrypter.decryptToString(b64)}');
}

Future<void> rsaPaddingHashMatrixExample() async {
  final pair = await Fortis.rsa().keySize(2048).generateKeyPair();
  final plaintext = Uint8List.fromList('round-trip test'.codeUnits);

  // PKCS#1 v1.5
  final pkcs1Ct = Fortis.rsa()
      .padding(RsaPadding.pkcs1_v1_5)
      .hash(RsaHash.sha256)
      .encrypter(pair.publicKey)
      .encrypt(plaintext);
  final pkcs1Pt = Fortis.rsa()
      .padding(RsaPadding.pkcs1_v1_5)
      .hash(RsaHash.sha256)
      .decrypter(pair.privateKey)
      .decrypt(pkcs1Ct);
  print('PKCS#1 v1.5: ${String.fromCharCodes(pkcs1Pt)}');

  // OAEP v1 + SHA-1
  final oaep1Ct = Fortis.rsa()
      .padding(RsaPadding.oaep_v1)
      .hash(RsaHash.sha1)
      .encrypter(pair.publicKey)
      .encrypt(plaintext);
  final oaep1Pt = Fortis.rsa()
      .padding(RsaPadding.oaep_v1)
      .hash(RsaHash.sha1)
      .decrypter(pair.privateKey)
      .decrypt(oaep1Ct);
  print('OAEP v1: ${String.fromCharCodes(oaep1Pt)}');

  // OAEP v2 with all hash algorithms
  for (final hash in RsaHash.values) {
    final ct = Fortis.rsa()
        .padding(RsaPadding.oaep_v2)
        .hash(hash)
        .encrypter(pair.publicKey)
        .encrypt(plaintext);
    final pt = Fortis.rsa()
        .padding(RsaPadding.oaep_v2)
        .hash(hash)
        .decrypter(pair.privateKey)
        .decrypt(ct);

    print('OAEP v2 + $hash: ${String.fromCharCodes(pt)}');
  }

  // OAEP v2.1 with all hash algorithms
  for (final hash in RsaHash.values) {
    final ct = Fortis.rsa()
        .padding(RsaPadding.oaep_v2_1)
        .hash(hash)
        .encrypter(pair.publicKey)
        .encrypt(plaintext);
    final pt = Fortis.rsa()
        .padding(RsaPadding.oaep_v2_1)
        .hash(hash)
        .decrypter(pair.privateKey)
        .decrypt(ct);

    print('OAEP v2.1 + $hash: ${String.fromCharCodes(pt)}');
  }
}

Future<void> rsaLabelExample() async {
  final pair = await Fortis.rsa().keySize(2048).generateKeyPair();
  final plaintext = Uint8List.fromList('round-trip test'.codeUnits);

  // String label
  final encrypter = Fortis.rsa()
      .padding(RsaPadding.oaep_v2_1)
      .hash(RsaHash.sha256)
      .encrypter(pair.publicKey, label: 'my-label');
  final decrypter = Fortis.rsa()
      .padding(RsaPadding.oaep_v2_1)
      .hash(RsaHash.sha256)
      .decrypter(pair.privateKey, label: 'my-label');

  final ciphertext = encrypter.encrypt(plaintext);
  final recovered = decrypter.decrypt(ciphertext);
  print('OAEP v2.1 String label: ${String.fromCharCodes(recovered)}');

  // Uint8List label
  final labelBytes = Uint8List.fromList('binary-label'.codeUnits);
  final encrypterBytes = Fortis.rsa()
      .padding(RsaPadding.oaep_v2_1)
      .hash(RsaHash.sha256)
      .encrypter(pair.publicKey, label: labelBytes);
  final decrypterBytes = Fortis.rsa()
      .padding(RsaPadding.oaep_v2_1)
      .hash(RsaHash.sha256)
      .decrypter(pair.privateKey, label: labelBytes);

  final ct = encrypterBytes.encrypt(plaintext);
  final pt = decrypterBytes.decrypt(ct);
  print('OAEP v2.1 Uint8List label: ${String.fromCharCodes(pt)}');
}

Future<void> rsaKeySerializationExample() async {
  final pair = await Fortis.rsa().keySize(2048).generateKeyPair();

  // --- Public key ---
  // PEM X.509 (default)
  final pubPemX509 = pair.publicKey.toPem();
  final pubFromPem = FortisRsaPublicKey.fromPem(pubPemX509);
  print('Public PEM X.509 round-trip: ${pubFromPem.toPem() == pubPemX509}');

  // PEM PKCS#1
  final pubPemPkcs1 = pair.publicKey.toPem(format: RsaPublicKeyFormat.pkcs1);
  final pubFromPkcs1 = FortisRsaPublicKey.fromPem(
    pubPemPkcs1,
    format: RsaPublicKeyFormat.pkcs1,
  );
  print(
    'Public PEM PKCS#1 round-trip: '
    '${pubFromPkcs1.toPem(format: RsaPublicKeyFormat.pkcs1) == pubPemPkcs1}',
  );

  // DER Base64
  final publicB64 = pair.publicKey.toDerBase64();
  final restoredPublic = FortisRsaPublicKey.fromDerBase64(publicB64);
  print(
    'Public DER Base64 round-trip: ${restoredPublic.toDerBase64() == publicB64}',
  );

  // --- Private key ---
  // PEM PKCS#8 (default)
  final privPemPkcs8 = pair.privateKey.toPem();
  final privFromPkcs8 = FortisRsaPrivateKey.fromPem(privPemPkcs8);
  print(
    'Private PEM PKCS#8 round-trip: ${privFromPkcs8.toPem() == privPemPkcs8}',
  );

  // PEM PKCS#1
  final privPemPkcs1 = pair.privateKey.toPem(format: RsaPrivateKeyFormat.pkcs1);
  final privFromPkcs1 = FortisRsaPrivateKey.fromPem(
    privPemPkcs1,
    format: RsaPrivateKeyFormat.pkcs1,
  );
  print(
    'Private PEM PKCS#1 round-trip: '
    '${privFromPkcs1.toPem(format: RsaPrivateKeyFormat.pkcs1) == privPemPkcs1}',
  );

  // DER Base64
  final privateB64 = pair.privateKey.toDerBase64();
  final restoredPrivate = FortisRsaPrivateKey.fromDerBase64(privateB64);
  print(
    'Private DER Base64 round-trip: '
    '${restoredPrivate.toDerBase64() == privateB64}',
  );
}

Future<void> ccmTagSizeExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();

  // All tag sizes valid per NIST SP 800-38C
  for (final bits in [32, 64, 96, 128]) {
    final cipher = Fortis.aes().ccm().tagSize(bits).cipher(key);
    final ct = cipher.encrypt('hello fortis');
    print('AES-CCM tag=$bits decrypted: ${cipher.decryptToString(ct)}');
  }

  // Invalid tag size → FortisConfigException up front
  try {
    Fortis.aes().ccm().tagSize(65).cipher(key);
  } on FortisConfigException catch (e) {
    print('AES-CCM tag=65 rejected: ${e.message}');
  }
}

Future<void> ecdhBasicExample() async {
  // Defaults (P-256)
  final alice = await Fortis.ecdh().generateKeyPair();
  final bob = await Fortis.ecdh().generateKeyPair();

  // Alice and Bob derive the same AES key independently
  final aliceKey = Fortis.ecdh()
      .keyDerivation(alice.privateKey)
      .deriveAesKey(bob.publicKey);
  final bobKey = Fortis.ecdh()
      .keyDerivation(bob.privateKey)
      .deriveAesKey(alice.publicKey);

  print(
    'ECDH P-256 shared keys match: ${aliceKey.toBase64() == bobKey.toBase64()}',
  );

  // Use the derived key with AES-GCM
  final cipher = Fortis.aes().gcm().cipher(aliceKey);
  final ct = cipher.encrypt('hello fortis');
  final recovered = Fortis.aes().gcm().cipher(bobKey).decryptToString(ct);
  print('ECDH → AES-GCM round-trip: $recovered');

  // P-384 and P-521
  for (final curve in [EcdhCurve.p384, EcdhCurve.p521]) {
    final a = await Fortis.ecdh().curve(curve).generateKeyPair();
    final b = await Fortis.ecdh().curve(curve).generateKeyPair();
    final k1 = Fortis.ecdh()
        .curve(curve)
        .keyDerivation(a.privateKey)
        .deriveAesKey(b.publicKey);
    final k2 = Fortis.ecdh()
        .curve(curve)
        .keyDerivation(b.privateKey)
        .deriveAesKey(a.publicKey);
    print('ECDH $curve shared keys match: ${k1.toBase64() == k2.toBase64()}');
  }
}

Future<void> ecdhKeySerializationExample() async {
  final pair = await Fortis.ecdh().generateKeyPair();

  // Public — PEM X.509 (only PEM format supported for ECDH public)
  final pubPem = pair.publicKey.toPem();
  final pubFromPem = FortisEcdhPublicKey.fromPem(pubPem);
  print('ECDH public PEM round-trip: ${pubFromPem.toPem() == pubPem}');

  // Public — DER Base64
  final pubB64 = pair.publicKey.toDerBase64();
  final pubFromB64 = FortisEcdhPublicKey.fromDerBase64(pubB64);
  print(
    'ECDH public DER Base64 round-trip: ${pubFromB64.toDerBase64() == pubB64}',
  );

  // Public — raw uncompressed point (interop with WebCrypto / JWK / many JS libs)
  final raw = pair.publicKey.toDer(
    format: EcdhPublicKeyFormat.uncompressedPoint,
  );
  final pubFromRaw = FortisEcdhPublicKey.fromDer(
    raw,
    format: EcdhPublicKeyFormat.uncompressedPoint,
    curve: EcdhCurve.p256,
  );
  print('ECDH public uncompressed point length: ${raw.length} bytes');
  print(
    'ECDH public uncompressed round-trip: '
    '${pubFromRaw.toDerBase64() == pair.publicKey.toDerBase64()}',
  );

  // Private — PEM PKCS#8 (default)
  final privPemPkcs8 = pair.privateKey.toPem();
  final privFromPkcs8 = FortisEcdhPrivateKey.fromPem(privPemPkcs8);
  print(
    'ECDH private PEM PKCS#8 round-trip: '
    '${privFromPkcs8.toPem() == privPemPkcs8}',
  );

  // Private — PEM SEC1 (openssl default for EC keys)
  final privPemSec1 = pair.privateKey.toPem(format: EcdhPrivateKeyFormat.sec1);
  final privFromSec1 = FortisEcdhPrivateKey.fromPem(
    privPemSec1,
    format: EcdhPrivateKeyFormat.sec1,
  );
  print(
    'ECDH private PEM SEC1 round-trip: '
    '${privFromSec1.toPem(format: EcdhPrivateKeyFormat.sec1) == privPemSec1}',
  );
}

Future<void> ecdhAdvancedDerivationExample() async {
  final alice = await Fortis.ecdh().generateKeyPair();
  final bob = await Fortis.ecdh().generateKeyPair();

  // Raw shared secret (the x-coord of the ECDH point, left-padded to field size)
  final secret = Fortis.ecdh()
      .keyDerivation(alice.privateKey)
      .deriveSharedSecret(bob.publicKey);
  print('ECDH raw shared secret: ${secret.length} bytes');

  // Derive arbitrary key material via HKDF-SHA256 with salt + info context
  final salt = Uint8List.fromList(utf8.encode('session-123'));
  final info = Uint8List.fromList(utf8.encode('fortis/chat-v1'));
  final keyBytes = Fortis.ecdh()
      .keySize(512) // 64 bytes
      .keyDerivation(alice.privateKey)
      .deriveKey(bob.publicKey, salt: salt, info: info);
  print('ECDH derived key bytes (HKDF): ${keyBytes.length} bytes');

  // Static utility: stretch any pre-shared secret into an AES key
  final preShared = Uint8List.fromList(List.filled(32, 0x42));
  final aesFromPreShared = EcdhKeyDerivation.hkdfDeriveAesKey(
    preShared,
    keySize: 256,
    salt: salt,
    info: info,
  );
  final cipher = Fortis.aes().gcm().cipher(aesFromPreShared);
  final ct = cipher.encrypt('hello from pre-shared');
  print('HKDF static utility decrypt: ${cipher.decryptToString(ct)}');
}

Future<void> errorHandlingExample() async {
  final key = await Fortis.aes().keySize(256).generateKey();

  // (a) Tampered ciphertext → FortisEncryptionException (GCM auth failure)
  final gcm = Fortis.aes().gcm().cipher(key);
  final ct = gcm.encrypt('hello fortis');
  final tampered = Uint8List.fromList(ct)..[ct.length ~/ 2] ^= 0xFF;
  try {
    gcm.decrypt(tampered);
  } on FortisEncryptionException catch (e) {
    print('Tampering detected: ${e.message.split('.').first}');
  }

  // (b) Invalid Base64 input → FortisConfigException (wrapped FormatException)
  try {
    gcm.decrypt('!!!not base64!!!');
  } on FortisConfigException catch (e) {
    print('Invalid Base64 rejected: ${e.message}');
  }

  // (c) Invalid tagSize on AesAuthCipher constructor → FortisConfigException
  try {
    AesAuthCipher(mode: AesMode.gcm, key: key, tagSizeBits: 96);
  } on FortisConfigException catch (e) {
    print('Invalid tagSize rejected: ${e.message.split('.').first}');
  }
}
1
likes
160
points
270
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