Thu. Mar 28th, 2024

Overview

This post covers simple, symmetric key encryption using Rust. 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. 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

  • 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.

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);
}

Hope this helps!

7 thoughts on “Simple Encryption In Rust”
  1. 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

    1. Thanks for the feedback! I’ll need to read up on pbkdf. I had a fairly simple encryption problem to solve the other day, and I was surprised by how difficult it was to get information as a beginner. I hope the article helps address some of the issues

  2. Thanks so much for this example which is working well for my requirements.

    > …I was surprised by how difficult it was to get information…

    Yep agreed. I also couldn’t find a good example and then stumbled upon your post. I even tried some ChatGPT variations but nothing working came out.

  3. Thank you very much for examples 🙂 I don’t know why but I couldn’t find any complete examples how to use crypto primitives in Rust.

Leave a Reply

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