Cryptography for Dart and Flutter

repository·master·Indexed 41 days ago

https://github.com/dint-dev/cryptography

A collection of Dart and Flutter packages providing cryptographic primitives and utilities. It supports pure Dart, Web Crypto, and native mobile implementations (Android, iOS, macOS). The library includes implementations for ciphers (AES-GCM, ChaCha20), digital signatures (Ed25519, ECDSA, RSA), key exchange (X25519, ECDH), KDFs (Argon2id, PBKDF2), and hash algorithms (SHA-2, BLAKE2). The `cryptography_flutter` package enables native API delegation for improved performance and security.

Tokens
13.1K
Snippets
46
Records
68
Agent score
47%

What's inside Cryptography

  1. Overview of the Cryptography ecosystem

    master

    The cryptography ecosystem provides cryptographic algorithm implementations for Dart and Flutter developers. It is split into several packages depending on the required implementation backend (pure Dart, Web Crypto, or Native OS APIs) and utility needs (like JWK handling).

    To use cryptography in your project, you should choose the package that matches your target platform and performance requirements:

    • cryptography: The core API. Use this for pure Dart implementations or when targeting web browsers (via Web Cryptography API).
    • cryptography_flutter: Use this for high-performance implementations that leverage native operating system APIs on Android and Apple platforms (iOS, macOS, etc.).
    • jwk: A utility package for encoding and decoding JSON Web Keys (JWK).
  2. Use different Random Number Generators

    master

    The package provides different levels of randomness depending on your needs:

    • Default: Uses Dart SDK's Random.secure() for all APIs.
    • SecureRandom.fast: A faster, cryptographically reasonably strong RNG using a 12-round ChaCha cipher. It can generate up to 0.25 GB of random data per second by reducing expensive OS calls after initial seeding.
    • SecureRandom.forTesting(seed: int): A deterministic RNG using the ChaCha-based implementation with a fixed seed and no re-seeding. Use this to ensure deterministic behavior in tests.
  3. Choose a Cryptography implementation

    master

    The Cryptography class uses factory methods to return specific implementations. You can choose an implementation based on your platform and performance requirements:

    • BrowserCryptography (Default): Uses the Web Cryptography API whenever possible. It falls back to pure Dart implementations if the Web Crypto API is unavailable. Works on all platforms.
    • DartCryptography: Provides pure Dart implementations that work on all platforms. Algorithms are Apache 2.0 licensed.
    • FlutterCryptography: (Available via the cryptography_flutter package) Provides up to 100x faster performance on Android, iOS, and macOS.
  4. How cryptography_flutter classes work

    master

    The package provides two main categories of classes to handle cryptographic operations:

    1. Native-backed classes (e.g., FlutterChacha20): These attempt to use operating system APIs on Android, iOS, or macOS. If the OS does not support the specific algorithm, they fall back to a background implementation or a pure Dart implementation (from package:cryptography) if available.
    2. Background classes (e.g., BackgroundChacha20): These move lengthy computations to a background isolate using Flutter's compute function to prevent UI jank.

    Isolate Behavior:

    • Small inputs: If the input is very small (e.g., a 16-byte message), computations are performed in the same isolate to avoid the overhead of message passing.
    • Large inputs: Extremely large inputs are also computed in the same isolate to avoid the high cost of allocating massive messages across isolates.
    • Concurrency: The package uses a queue to prevent memory exhaustion when many requests are sent concurrently.
  5. Use Wands for high-level API abstraction

    master

    For a higher level of API abstraction and potential performance boosts, the package recommends using Wand instances. Wands perform operations using a fixed, "invisible" key (often referred to as "magic wands").

    Available wand types:

    • CipherWand
    • KeyExchangeWand
    • SignatureWand

    Note: The package plans to transition to wands as the default API in version 3.x.

  6. Understand key types and data handling

    master

    The package uses several abstraction layers for cryptographic keys. Note that SecretKey and KeyPair instances are opaque and asynchronous by default; they may not be directly readable in memory for security reasons.

    Key Abstractions

    • SecretKey: Used by ciphers, MACs, and KDFs.
    • KeyPair: Used for asymmetric operations. Types include:
      • SimpleKeyPair (e.g., 32-byte Ed25519/X25519 keys)
      • EcKeyPairData (e.g., P-256, P-384, P-521)
      • RsaKeyPairData (RSA private keys)
    • PublicKey: Types include:
      • SimplePublicKey (e.g., Ed25519/X25519 public keys)
      • EcPublicKey (e.g., P-256, P-384, P-512)
      • RsaPublicKey (RSA public keys)

    Data Access

    If a key is accessible in memory, it will be an instance of one of the following data classes:

    • SecretKeyData
    • SimpleKeyPairData
    • EcKeyPairData
    • RsaKeyPairData

    For encoding/decoding keys in JWK (JSON Web Key) format, use the package:jwk package.

  7. Understand the cryptography package architecture

    master

    The cryptography package is designed to be cross-platform and performant by defaulting to platform-provided implementations:

    • Android: Uses javax.crypto.
    • iOS / macOS: Uses Apple CryptoKit.
    • Web: Uses the Web Crypto API.

    If a platform does not provide a required algorithm, the package automatically falls back to a pure Dart implementation. This ensures compatibility across all platforms, including browsers (supporting both JavaScript and WASM compilers).

  8. Use Authenticated Encryption (AesGcm)

    master

    Authenticated encryption requires a SecretKey and an optional nonce (Initialization Vector). If no nonce is provided, one is automatically generated.

    Encryption produces a SecretBox containing:

    • nonce: The unique byte sequence used.
    • cipherText: The encrypted message.
    • mac: The Message Authentication Code.

    You can use secretBox.concatenation() to combine these parts for transmission.

    import 'dart:convert';
    import 'package:cryptography/cryptography.dart';
    
    Future<void> main() async {
      // Choose the cipher
      final algorithm = AesGcm.with256bits();
    
      // Generate a random secret key.
      final secretKey = await algorithm.newSecretKey();
      final secretKeyBytes = await secretKey.extractBytes();
      print('Secret key: ${secretKeyBytes}');
    
      // Encrypt
      final secretBox = await algorithm.encryptString(
        'Hello!',
        secretKey: secretKey,
      );
      print('Nonce: ${secretBox.nonce}'); // Randomly generated nonce
      print('Ciphertext: ${secretBox.cipherText}'); // Encrypted message
      print('MAC: ${secretBox.mac}'); // Message authentication code
      
      // If you are sending the secretBox somewhere, you can concatenate all parts of it:
      final concatenatedBytes = secretBox.concatenation();
      print('All three parts concatenated: $concatenatedBytes');
    
      // Decrypt
      final clearText = await algorithm.decryptString(
        secretBox,
        secretKey: secretKey,
      );
      print('Cleartext: $clearText'); // Hello!
    }
  9. Install cryptography and cryptography_flutter

    master

    To use this package in a Dart or Flutter project, add cryptography to your dependencies. If you are developing a Flutter app or package, it is highly recommended to also include cryptography_flutter. This sibling package ensures that the library delegates cryptographic operations to native Android, iOS, or macOS APIs whenever possible, providing better performance and security.

    dependencies:
      cryptography: ^2.9.0
      
      # If you are writing a Flutter app/package, also add this:
      cryptography_flutter: ^2.3.4
  10. Verify a Cipher implementation with testCipher()

    master

    Use testCipher to automatically validate that your Cipher implementation complies with API specifications and passes sanity checks (such as decrypt(encrypt(input))) using various inputs.

    You can extend these automatic checks by providing custom tests via the otherTests parameter using expectCipherExample to verify specific test vectors.

    import 'package:cryptography_test/cryptography_test.dart';
    import 'package:cryptography_test/cipher.dart';
    
    void main() {
      testCipher(
        builder: () => MyCipher(someParameter: 123),
        // `testCipher` will do various automatic sanity checks such as:
        //   decrypt(encrypt(input))
        // ...with various interesting inputs.
        //
        // You can give it real test vectors too:
        otherTests: () {
          test('test vector', () async {
            await expectCipherExample(
              clearText: hexToBytes('01 23 45 67 89 ab cd ef'),
              secretKey: hexToBytes('01 23 45 67 89 ab cd ef'),
              nonce: hexToBytes('01 23 45 67 89 ab cd ef'),
              aad: hexToBytes('01 23 45 67 89 ab cd ef'),
              cipherText: hexToBytes('01 23 45 67 89 ab cd ef'),
              mac: hexToBytes('01 23 45 67 89 ab cd ef'),
            );
          });
        },
      );
    }
  11. Achieve deterministic behavior in tests

    master

    To make cryptographic operations (like key generation) deterministic for testing, supply a deterministic random number generator to the Cryptography instance using .withRandom().

    import 'package:cryptography/cryptography.dart';
    
    void main() {
      setUp(() {
        // Set a deterministic RNG for the global instance
        Cryptography.instance = Cryptography.instance.withRandom(SecureRandom.forTesting(seed: 42));
      });
      
      test('example', () async {
        final algorithm = AesGcm.with256bits();
        
        // This will always return the same secret key because of the deterministic RNG.
        final secretKey = await algorithm.newSecretKey();
        
        // ...
      });
    }