What we will cover:

  • Brief explanation of AES encryption
  • AES-256 encryption and decryption
  • Full Rust code for AES-256 encryption

Introduction

AES stands for Advanced Encryption Standard. It is a type of Symmetric key encryption algorithm, which means the same key is used for encryption and decryption.

In this tutorial we will use aes-gcm crate from Rust Crypto.

There are two type of AES encryption: AES-128, AES-192 and AES-256, which represents encryption Key of length 128 bit (16 byte), 192 bit (24 byte) and 256 bit (32 byte) respectively.

Both are pretty secure and hard to crack but for obvious reasons AES-256 is the most secure one.

AES encryption requires key and nonce (for gcm variant) to encrypt/decrypt the data.

Key

Key means encryption key, which is used to encrypt and decrypt the data. Length of the key matters, AES-128 requires key of 16 byte and AES-256 requires key of 32 byte.

For the simplicity from now on we will focus on AES-256, but the core concept is similar even for AES-128.

Key is byte array of length 32, where each element is of 1 byte. We can either generate the key using aes-gcm crate itself or we can use our own key. It’s prefferable to use our own key, so let’s see it in action:

Create new crypto project and add aes-gcm crate.

1
2
cargo new crypto
cargo add aes-gcm

main.rs

1
2
3
4
5
6
7
8
9
use aes_gcm::{
    aead::{Aead, AeadCore, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce
};

fn main() {
    let key_str = "thiskeystrmustbe32charlongtowork";
    let key = Key::<Aes256Gcm>::from_slice(key_str.as_bytes());
}

It’s important that the string/str used for key must be 32 char long, since AES-256 requires key length of 32 byte.

Note: If you want to use key string of any length (less or more than 32), you can use Key Derivation Function to generate secure 32 byte long key. Argon2, PBKDF2 are some of the fitting choices for this.

Nonce

Nonce means “number used only once”. It is an array of random numbers (type: u8) that is unique to each operation. For AES encryption the required size of Nonce is 12 byte.

For each encryption operation the same key can be used but the Nonce must be unique, so we generate Nonce each time we encrypt data.

Since nonce is required to decrypt the data, it is stored along with the encrypted data itself.

1
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);

Encryption

We will create encrypt function which will take key_str and plaintext string. This will make our code more readable and easy to use.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use aes_gcm::{
    aead::{Aead, AeadCore, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce
};

fn main() {
    let plaintext = "backendengineer.io".to_string();

    let key_str = "thiskeystrmustbe32charlongtowork".to_string();

    let encrypted_data = encrypt(key_str, plaintext);
}

fn encrypt(key_str: String, plaintext: String) -> Vec<u8> {
    let key = Key::<Aes256Gcm>::from_slice(key_str.as_bytes());
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
    
    let cipher = Aes256Gcm::new(key);

    let ciphered_data = cipher.encrypt(&nonce, plaintext.as_bytes())
        .expect("failed to encrypt");

    // combining nonce and encrypted data together
    // for storage purpose
    let mut encrypted_data: Vec<u8> = nonce.to_vec();
    encrypted_data.extend_from_slice(&ciphered_data);

    encrypted_data
}

Here we are:

  • creating instance of Aes256Gcm with our encryption key, which will be used for encryption.
  • we need to convert our plaintext from string to byte array for the encrypt method.
  • combine nonce and ciphered_data together

Decryption

Here first we will separate out nonce and ciphered data by splitting encrypted data vector at 12th position (because we used nonce of length 12).

Then we create Nonce instance from our nonce array that we got.

After decrypting we will get byte array which we can pass to String::from_utf8 function to create our original string value back.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn decrypt(key_str: String, encrypted_data: Vec<u8>) -> String {
    let key = Key::<Aes256Gcm>::from_slice(key_str.as_bytes());

    let (nonce_arr, ciphered_data) = encrypted_data.split_at(12);
    let nonce = Nonce::from_slice(nonce_arr);

    let cipher = Aes256Gcm::new(key);

    let plaintext = cipher.decrypt(nonce, ciphered_data)
        .expect("failed to decrypt data");

    String::from_utf8(plaintext)
        .expect("failed to convert vector of bytes to string")
}

Storage

There is one issue you might encounter that the encrypt function will give out Vector of numbers (u8), which is not easy to store. Of course you might convert it to string by joining them with comma but it’s not good solution.

To solve this we can convert our vector of numbers to hex string, this will make storage and operation much easier for us.

Let’s install hex crate for this.

1
cargo add hex

hex crate provides encode (vec to string) and decode (string to vec) function for easy conversion.

1
2
3
4
let encoded_string = hex::encode(encrypted_data);

let decoded_vec = hex::decode(encoded_string)
    .expect("failed to decode hex string into vec");

Full Code

In this final version we have made few changes (highlighted lines) to incorporate hex conversion within the encrypt and decrypt function itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
use aes_gcm::{
    aead::{Aead, AeadCore, KeyInit, OsRng},
    Aes256Gcm, Key, Nonce
};

fn main() {
    let plaintext = "backendengineer.io".to_string();

    let key_str = "thiskeystrmustbe32charlongtowork".to_string();

    let encrypted_data = encrypt(key_str.clone(), plaintext);

    println!("encrypted_data: {:?}", encrypted_data.clone());

    let original = decrypt(key_str, encrypted_data);

    println!("original: {:?}", original);
}

fn encrypt(key_str: String, plaintext: String) -> String {
    let key = Key::<Aes256Gcm>::from_slice(key_str.as_bytes());
    let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
    
    let cipher = Aes256Gcm::new(key);

    let ciphered_data = cipher.encrypt(&nonce, plaintext.as_bytes())
        .expect("failed to encrypt");

    // combining nonce and encrypted data together
    // for storage purpose
    let mut encrypted_data: Vec<u8> = nonce.to_vec();
    encrypted_data.extend_from_slice(&ciphered_data);

    hex::encode(encrypted_data)
}

fn decrypt(key_str: String, encrypted_data: String) -> String {
    let encrypted_data = hex::decode(encrypted_data)
        .expect("failed to decode hex string into vec");

    let key = Key::<Aes256Gcm>::from_slice(key_str.as_bytes());

    let (nonce_arr, ciphered_data) = encrypted_data.split_at(12);
    let nonce = Nonce::from_slice(nonce_arr);

    let cipher = Aes256Gcm::new(key);

    let plaintext = cipher.decrypt(nonce, ciphered_data)
        .expect("failed to decrypt data");

    String::from_utf8(plaintext)
        .expect("failed to convert vector of bytes to string")
}