Simple Encryption In Rust and Dart

Overview

This post covers simple, symmetric key encryption. And example is provided in both Rust and 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

I had these problems recently. Rust 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

Encryption Process

Decryption Process

Code Examples

Rust and Dart examples can be found below.

**Note: users more experienced with encryption have commented that my solution is sorely lacking in certain areas. Therefore, use at your own risk. Here is the most useful comment, supplied by huntrss@posteo.me

Thank you for the article. Some remarks from my side:

1. MAC = message authentication code. To be more precise, it is needed to authenticate the data. This means that the data has not been tampered with or that some random error has changed it. It can be used on plaintext data as well as on encrypted data.

2. In your code your password is used directly as key. Using a password based key derivation function (PBKDF) is a better option.

Best regards

Example Using Rust

Cargo.toml

Add the following dependencies:

[package]
name = "aes_gcm_128_rs"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
hex = "0.4.3"
rust-crypto = "0.2.0"
rand = "0.8.5"

Code

Here’s a full application that shows encryption and decryption. The code is well commented and despite being a little long, it should be very easy to understand.

use std::error::Error;
use std::{io, str};
use std::io::ErrorKind;
use std::iter::repeat;
use std::str::from_utf8;
use crypto::aead::{AeadDecryptor, AeadEncryptor};
use crypto::aes_gcm::AesGcm;
/// orig must be a string of the form [hexNonce]/[hexCipherText]/[hexMac]. This
/// is the data returned from encrypt(). This function splits the data, removes
/// the hex encoding, and returns each as a list of bytes.
fn split_iv_data_mac(orig: &str) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), Box<dyn Error>> {
    let split: Vec<&str> = orig.split('/').into_iter().collect();
    if split.len() != 3 {
        return Err(Box::new(io::Error::from(ErrorKind::Other)));
    }
    let iv_res = hex::decode(split[0]);
    if iv_res.is_err() {
        return Err(Box::new(io::Error::from(ErrorKind::Other)));
    }
    let iv = iv_res.unwrap();
    let data_res = hex::decode(split[1]);
    if data_res.is_err() {
        return Err(Box::new(io::Error::from(ErrorKind::Other)));
    }
    let data = data_res.unwrap();
    let mac_res = hex::decode(split[2]);
    if mac_res.is_err() {
        return Err(Box::new(io::Error::from(ErrorKind::Other)));
    }
    let mac = mac_res.unwrap();
    Ok((iv, data, mac))
}
/// gets a valid key. This must be exactly 16 bytes. if less than 16 bytes, it will be padded with 0.
/// If more than 16 bytes, it will be truncated
fn get_valid_key(key: &str) -> Vec<u8> {
    let mut bytes = key.as_bytes().to_vec();
    if bytes.len() < 16 {
        for j in 0..(16 - bytes.len()) {
            bytes.push(0x00);
        }
    } else if bytes.len() > 16 {
        bytes = bytes[0..16].to_vec();
    }
    bytes
}
///Decryption using AES-GCM 128
///iv_data_mac is a string that contains the iv/nonce, data, and mac values. All these values
/// must be hex encoded, and separated by "/" i.e. [hex(iv)/hex(data)/hex(mac)]. This function decodes
/// the values. key (or password) is the raw (not hex encoded) password
pub fn decrypt(iv_data_mac: &str, key: &str) -> Result<Vec<u8>, Box<dyn Error>> {
    let (iv, data, mac) = split_iv_data_mac(iv_data_mac)?;
    let key = get_valid_key(key);
    let key_size = crypto::aes::KeySize::KeySize128;
    // I don't use the aad for verification. aad isn't encrypted anyway, so it's just specified
    // as &[].
    let mut decipher = AesGcm::new(key_size, &key, &iv, &[]);
    // create a list where the decoded data will be saved. dst is transformed in place. It must be exactly the same
    // size as the encrypted data
    let mut dst: Vec<u8> = repeat(0).take(data.len()).collect();
    let result = decipher.decrypt(&data, &mut dst, &mac);
    if result { println!("Successful decryption"); }
    println!("\nDecrypted {}", str::from_utf8(&dst).unwrap());
    Ok(dst)
}
/// Creates an initial vector (iv). This is also called a nonce
fn get_iv(size: usize) -> Vec<u8> {
    let mut iv = vec![];
    for j in 0..size {
        let r = rand::random();
        iv.push(r);
    }
    iv
}
///encrypt "data" using "password" as the password
/// Output is [hexNonce]/[hexCipher]/[hexMac] (nonce and iv are the same thing)
pub fn encrypt(data: &[u8], password: &str) -> String {
    let key_size = crypto::aes::KeySize::KeySize128;
    //pad or truncate the key if necessary
    let valid_key = get_valid_key(password);
    let iv = get_iv(12); //initial vector (iv), also called a nonce
    let mut cipher = AesGcm::new(key_size, &valid_key, &iv, &[]);
    //create a vec of data.len 0's. This is where the encrypted data will be saved.
    //the encryption is performed in-place, so this vector of 0's will be converted
    //to the encrypted data
    let mut encrypted: Vec<u8> = repeat(0).take(data.len()).collect();
    //create a vec of 16 0's. This is for the mac. This library calls it a "tag", but it's really
    // the mac address. This vector will be modified in place, just like the "encrypted" vector
    // above
    let mut mac: Vec<u8> = repeat(0).take(16).collect();
    //encrypt data, put it into "encrypted"
    cipher.encrypt(data, &mut encrypted, &mut mac[..]);
    //create the output string that contains the nonce, cipher text, and mac
    let hex_iv = hex::encode(iv);
    let hex_cipher = hex::encode(encrypted);
    let hex_mac = hex::encode(mac);
    let output = format!("{}/{}/{}", hex_iv, hex_cipher, hex_mac);
    output
}
fn main() {
    let data = "hello world";
    let password = "12345";
    println!("Data to encrypt: \"{}\" and password: \"{}\"", &data, &password);
    println!("Encrypting now");
    let res = encrypt(data.as_bytes(), password);
    println!("Encrypted response: {}", res);
    println!("Decrypting the response");
    let decrypted_bytes = decrypt(res.as_str(), password).unwrap();
    let decrypted_string = from_utf8(&decrypted_bytes).unwrap();
    println!("Decrypted response: {}", decrypted_string);
}

Example Using Dart

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!