Thu. Mar 28th, 2024

Overview

This post covers simple, symmetric key encryption using Dart. If you don’t know much about encryption and you’re looking for a basic “password” encryption solution, this is the post for you.

Problem

You need to encrypt and later decrypt some data with a password. So

  • What library do you use?
  • What encryption algorithm do you use?
  • Are there code samples?
  • What does the terminology mean?

I had these problems recently. Dart has some great encryption libraries. The biggest problem for me was finding the right library to use. Once I did, the documentation was horrible. Being a newbie at encryption, I also didn’t understand the terminology, and the code samples weren’t great.

Explanations

  • Symmetric key cryptography – This means you have one password to encrypt and decrypt. If different people or computers are doing the encryption and decryption, you need to figure out how both people/computers will get the same password securely. This is a big problem in many cases, which is why Asymmetric keys (i.e. public and private keys) are more popular now. This post covers symmetric keys.
  • Algorithm choice. You can’t just use any encryption algorithm. RSA encryption, for instance, uses asymmetric keys, and therefore is not suitable for this use case. This post covers AES-GCM 128-bit. This is one of the most trusted solutions. For greater security, you can use 256-bit instead. This will mean adjusting your key (password) size, but the rest should be the same.
  • Key – key means password in this case. For 128-bit AES-GCM, you need a 128-bit key. That’s 16 bytes. The rust code will panic if you do not provide exactly 16 bytes for the key. If you use 256-bit AES-GCM, you will need a 256-bit, or 32 byte, key. There is a padding option with some algorithms. I am not using it. I am performing my own padding. The padding function is included in the code sample.
  • Nonce/IV – Nonce and IV (for this algorithm) are the same thing. Nonce means “number only used once”. IV means “initialization vector”. Some posts and libraries use the word nonce, others IV. Don’t be confused by the change in terminology. The nonce/iv is the seed used in random number generators. Traditional random number generators use one number only (eg. 8-bit or 32-bit) as the seed. AES-GCM needs a 96-bit, or 12 byte, seed. This is supplied as a list of 12 bytes. Again, if your seed length is incorrect, the library panics. The nonce/iv used for encryption is needed for decryption.
  • Cipher Text – This is the encrypted data. It will be exactly the same length as the unencrypted data.
  • MAC – this is a list of numbers calculated after encryption. It is needed for decryption. It is 16-bytes long for 128-bit AES-GCM.
  • AAD – additional verification data. I don’t need it so I didn’t bother using it.

Encryption Process

  • Create a new IV/nonce
  • Ensure your key is the correct length. I provide a padding function so if the key is less than 16 bytes, it is padded with 0x00. If it is more than 16 bytes, it is truncated.
  • Convert your data to encrypt, and your key, to a list of bytes
  • Initialize the library and call encrypt(). The result will be the encrypted data (cipher text) as a list of bytes (exactly the same length as the unencrypted input), and a 16-byte mac.
  • You then need to return the nonce, the cipher text, and the mac. All three values are required for decryption. I hex encode this data before returning so it can easily be used in places like JSON messages. I’m sure there is an industry standard for passing around this information. However, the encoding system I’m using is my own, and not official.

Decryption Process

  • You need the nonce/iv, the cipher text and the mac values to perform decryption, along with the password. In my example, all this data is hex encoded. I have provided a function to decode (not decrypt) this hex encoded information, and extract the relevant values
  • Initialize the library and call decrypt() with the values above
  • The result will be a List of bytes. You can process this list however you want eg. in my example, I just convert it to a string and display it.

Pubspec.yaml

Add the following dependencies:

name: test_aes_gcm_dart
description: A sample command-line application.
version: 1.0.0
# homepage: https://www.example.com

environment:
  sdk: '>=2.18.5 <3.0.0'

dependencies:
  cryptography: ^2.0.5

dev_dependencies:
  lints: ^2.0.0
  test: ^1.16.0
  hex: ^0.2.0

Code

import 'dart:convert';

import 'package:hex/hex.dart';
import 'package:cryptography/cryptography.dart';

/// Pads the password if necessary.
/// The final password must be exactly 16 bytes. If the input password is less than 16 bytes, it will be padded with 0x00.
/// if it is more than 16 bytes, only the first 16 bytes will be used.
List<int> getValidPassword(String origPassword, int maxLength) {
  var origBytes = utf8.encode(origPassword).toList();

  var len = origPassword.length;
  if (len < maxLength) {
    for (int j = 0; j < (maxLength - len); j++) {
      origBytes.add(0x00);
    }
  } else if (len > maxLength) {
    origBytes.sublist(0, maxLength);
  }

  return origBytes;
}

/// Encrypt data with password. Password is the raw password (i.e. not hex encoded)
Future<String> encrypt(List<int> data, String password) async {
  final pwBytes = getValidPassword(password, 16);

  /* use 128 bit encryption. If you plan on using, eg. 256 bit, adjust the password length appropriately */
  final algorithm = AesGcm.with128bits();

  final secretKey = await algorithm.newSecretKeyFromBytes(pwBytes);
  final nonce = algorithm.newNonce();

  var secretBox = await algorithm.encrypt(
    data,
    secretKey: secretKey,
    nonce: nonce,
  );
  //print('Nonce: ${secretBox.nonce}');
  //print('Ciphertext: ${secretBox.cipherText}');
  //print('MAC: ${secretBox.mac.bytes}');
  //print("mac length is: ${secretBox.mac.bytes.length}");

  var hexNonce = HEX.encode(nonce);
  var hexCipher = HEX.encode(secretBox.cipherText);
  var hexMac = HEX.encode(secretBox.mac.bytes);

  String encryptedString = "$hexNonce/$hexCipher/$hexMac";
  return encryptedString;
}

class DecodedData {
  List<int> nonce = [];
  List<int> cipherText = [];
  List<int> mac = [];
}

DecodedData decodeCipherString(String encryptedString) {
  List<String> split = encryptedString.split("/");
  if (split.length != 3) {
    throw Exception("Invalid cipher text size");
  }

  String hexNonce = split[0];
  String hexCipher = split[1];
  String hexMac = split[2];

  DecodedData decoded = DecodedData();
  decoded.nonce = HEX.decode(hexNonce);
  decoded.cipherText = HEX.decode(hexCipher);
  decoded.mac = HEX.decode(hexMac);

  return decoded;
}

/// encryptedString must be in the format [hexNonce]/[hexCipher]/[hexMac].
/// password is just a string.
Future<List<int>> decrypt(String encryptedString, String password) async {
  List<int> pwBytes = getValidPassword(password, 16);

  final algorithm = AesGcm.with128bits();

  /* decode (not decrypt) */
  var decoded = decodeCipherString(encryptedString);

  var secretBox = SecretBox(decoded.cipherText, nonce: decoded.nonce, mac: Mac(decoded.mac));
  var secretKey = await algorithm.newSecretKeyFromBytes(pwBytes);
  final decrypted = await algorithm.decrypt(
    secretBox,
    secretKey: secretKey,
  );

  return decrypted;
}

void main(List<String> arguments) async {
  String data = "hello world";
  String password = "12345";

  print("Original message is: $data. Password for encryption/decryption is: $password");

  print("First, let's encrypt");
  String encryptedString = await encrypt(utf8.encode(data), password);
  print("Encrypted string is: $encryptedString");

  print("Now, let's decrypt");
  List<int> decrypted = await decrypt(encryptedString, password);

  /* we're expecting a string, so convert to a string and output */
  String decryptedMsg = utf8.decode(decrypted);
  print("Decrypted message is: $decryptedMsg");
}

Hope this helps!

Leave a Reply

Your email address will not be published. Required fields are marked *