Skip to content

Fast and powerful cryptographic functions thanks to javax.crypto and CommonCrypto.

License

Notifications You must be signed in to change notification settings

hugo-pcl/native-crypto-flutter

Repository files navigation

Fast and powerful cryptographic functions for Flutter.

Style: Wyatt Analysis Maintained with Melos Build Status


[Changelog] | [License]


About

The goal of this plugin is to provide a fast and powerful cryptographic functions by calling native libraries. On Android, it uses javax.cypto, and on iOS, it uses CommonCrypto and CryptoKit

I started this projet because I wanted to add cryptographic functions on a Flutter app. But I faced a problem with the well-known Pointy Castle library: the performance was very poor. Here some benchmarks and comparison:

For comparison, on a iPhone 13, you can encrypt/decrypt a message of 2MiB in ~5.6s with PointyCastle and in ~40ms with NativeCrypto. And on an OnePlus 5, you can encrypt/decrypt a message of 50MiB in ~6min30 with PointyCastle and in less than ~1s with NativeCrypto.

In short, NativeCrypto is incomparable with PointyCastle.

Features

  • Hash functions
    • SHA-256
    • SHA-384
    • SHA-512
  • HMAC functions
    • HMAC-SHA-256
    • HMAC-SHA-384
    • HMAC-SHA-512
  • Secure random
  • PBKDF2
  • AES
    • Uint8List encryption/decryption
    • File encryption/decryption

Quick start

import 'package:native_crypto/native_crypto.dart';

Future<void> main() async {
    // Message to encrypt
    final Uint8List message = 'Hello World!'.toBytes();
    
    // Ask user for a password
    final String password = await getPassword();

    // Initialize a PBKDF2 object
    final Pbkdf2 pbkdf2 = Pbkdf2(
        length: 32, // 32 bytes
        iterations: 1000,
        salt: 'salt'.toBytes(),
        hashAlgorithm: HashAlgorithm.sha256,
    );
    
    // Derive a secret key from the password
    final SecretKey secretKey = await pbkdf2(password: password);

    // Initialize an AES cipher
    final AES cipher = AES(
        key: secretKey,
        mode: AESMode.gcm,
        padding: AESPadding.none,
    );

    // Encrypt the message
    final CipherText<AESCipherChunk> cipherText = await cipher.encrypt(message);

    // Decrypt the message
    final Uint8List decryptedMessage = await cipher.decrypt(cipherText);

    // Verify and print the decrypted message
    assert(listEquals(message, decryptedMessage));
    
    print(decryptedMessage.toStr());
}

Check the example for a complete example.

Please take a look a the compatibility table below to check if your target is supported.

Note: This Flutter example must run on a real device or a simulator.

Usage

Compatibility

First, check compatibility with your targets.

iOS Android MacOS Linux Windows Web

Warning: NativeCrypto 0.2.0+ is not compatible with lower NativeCrypto versions. Especially, with NativeCrypto 0.0. X because the cipher mode is not the same. Now, NativeCrypto uses AES-GCM mode instead of AES-CBC mode. (See Changelog)

NativeCrypto ciphertexts are formatted as follow:

+------------------+--------------------+------------------+
| Nonce (12 bytes) | Cipher text (n-28) |  Tag (16 bytes)  |
+------------------+--------------------+------------------+

Warning: If your data comes from another source, make sur to use the same format.

Hash

To digest a message, you'll need to initialize a Hasher object implementing Hash . Then, you can digest your message.

Hash hasher = Sha256();
Uint8List digest = await hasher.digest(message);

In NativeCrypto, you can use the following hash functions: SHA-256, SHA-384, SHA-512

HMAC

To generate a HMAC, you'll need to initialize a Hmac object. Then, you can generate a HMAC from a message and a secret key.

Hmac hmac = HmacSha256();
Uint8List hmac = await hmac.digest(message, secretKey);

In NativeCrypto, you can use the following HMAC functions: HMAC-SHA-256, HMAC-SHA-384, HMAC-SHA-512

Keys

You can build a SecretKey from utf8, utf16, base64, base16 (hex) strings, int list or raw bytes. You can also generate a SecretKey from secure random.

SecretKey secretKey = SecretKey(bytes); // bytes is a Uint8List
SecretKey secretKey = SecretKey.fromUtf8('secret');
SecretKet secretKey = SecretKey.fromUtf16('secret');
SecretKey secretKey = SecretKey.fromBase64('c2VjcmV0');
SecretKey secretKey = SecretKey.fromBase16('63657274');
SecretKey secretKey = SecretKey.fromList([0x73, 0x65, 0x63, 0x72, 0x65, 0x74]);
SecretKey secretKey = await SecretKey.fromSecureRandom(32); // 32 bytes

Key derivation

You can derive a SecretKey using PBKDF2.

First, you need to initialize a Pbkdf2 object.

final Pbkdf2 pbkdf2 = Pbkdf2(
    length: 32, // 32 bytes
    iterations: 1000,
    salt: salt.toBytes(),
    hashAlgorithm: HashAlgorithm.sha256,
);

Then, you can derive a SecretKey from a password.

SecretKey secretKey = await pbkdf2(password: password); 

Note: Pbkdf2 is a callable class. You can use it like a function.

Cipher

And now, you can use the SecretKey to encrypt/decrypt a message.

First, you need to initialize a Cipher object.

final AES cipher = AES(
    key: key,
    mode: AESMode.gcm,
    padding: AESPadding.none,
);

Then, you can encrypt your message.

final CipherText<AESCipherChunk> cipherText = await cipher.encrypt(message); 

After an encryption you obtain a CipherText which contains chunks. You can get the underlying bytes with cipherText.bytes .

Uppon receiving encrypted message receivedData , you can decrypt it. You have to reconstruct the ciphertext and the setup the chunk factory.

final CipherText<AESCipherChunk> receivedCipherText CipherText(
    receivedData,
    chunkFactory: (bytes) => AESCipherChunk(
        bytes,
        ivLength: cipher.mode.ivLength,
        tagLength: cipher.mode.tagLength,
    ),
),

Then, you can decrypt your message.

Uint8List message = await cipher.decrypt(receivedCipherText);

Files

You can encrypt/decrypt files.

First, you need to initialize a Cipher object.

final AES cipher = AES(
    key: key,
    mode: AESMode.gcm,
    padding: AESPadding.none,
);

Then, you can encrypt your file.

await cipher.encryptFile(plainText, cipherText);

Note: plainText and cipherText are File objects.

You can decrypt your file.

await cipher.decryptFile(cipherText, plainText);

Advanced

You can force the use of a specific IV. Please note that the IV must be unique for each encryption.

final CipherText<AESCipherChunk> cipherText = await cipher.encryptWithIV(message, iv);

⚠️ Use encrypt(...) instead of encryptWithIV(...) if you don't know what you are doing.

Development

Android

https://docs.flutter.dev/development/packages-and-plugins/developing-packages#step-2b-add-android-platform-code-ktjava

  • Launch Android Studio.
  • Select Open an existing Android Studio Project in the Welcome to Android Studio dialog, or select File > Open from the menu, and select the packages/native_crypto/example/android/build.gradle file.
  • In the Gradle Sync dialog, select OK.
  • In the Android Gradle Plugin Update dialog, select Don’t remind me again for this project.

iOS

https://docs.flutter.dev/development/packages-and-plugins/developing-packages#step-2c-add-ios-platform-code-swifthm

  • Launch Xcode.
  • Select File > Open, and select the packages/native_crypto/example/ios/Runner.xcworkspace file.