===============
== snarkydev ==
===============
Open source software, Rust, C

Encrypting Data With AES GCM in Rust Using Openssl

In the previous post we discussed encrypting and decrypting data with AES using the chaining technique called Cipher Block Chaining. In this post we will use AES with GCM Galois/counter mode, another block cipher mode of operation.

Just like the previous post, we will not get into the details and the implementation of GCM itself, we will use the rust APIs for it from openssl library.

Before we begin

  • What is GCM?

    • It’s a mode of operation for symmetric key block ciphers.

    • Introduced in 2004.

    • Combines counter mode (CTR) for encryption with Galois mode for authentication, ensuring both data confidentiality and integrity.

    • NIST Recommendation

  • How is it different from CBC?

    • GCM is an Authenticated Encryption (AE) mode, meaning it provides both encryption for confidentiality and an authentication tag to ensure data integrity and authenticity. You need both the encryption key and the tag to successfully decrypt and verify the data.

    • Especially for encrypting large datasets, because of its counter mode operation, encryption and authentication can be parallelised in the case of GCM, which is especially beneficial for hardware implementations. On the other hand CBC is sequential and can not be parallelised.

Implementation

To use AES GCM (from openssl) in a Rust project, the steps are similar to CBC mode except for a couple of things.

  • Encryption

    • Generate the AES key (128, 192 or 256 bits).
    • Generate IV/Nonce.
    • Generate the AAD Additional authentication data (in simple terms, a password).
    • Use the GCM mode while initializing the Cipher.
    • Encrypt the plain text and Obtain the tag.
      let mut key = vec![0; 24];
      let _ = rand_bytes(&mut key)?;
      println!("key: {:?}", key);
    
      // Generate an IV with 12 bytes of randomness
      let mut iv = vec![0; 12];
      let _ = rand_bytes(&mut iv)?;
      println!("iv: {:?}", iv);
    
      // additional authentication data
      let aad = b"$3CuRE-D4T4";
      // Buffer for the authentication tag
      let mut tag = [0u8; 16];
    
      // initialize the cipher AES 192 bits gcm mode
      let cipher = Cipher::aes_192_gcm();
    
      // encrypt the plaintext
      let ciphertext = encrypt_aead(
          cipher,
          &key,
          Some(&iv),
          aad,
          plaintext_input.as_bytes(),
          &mut tag,
      )?;
      println!("ciphertext: {:?}", ciphertext);
    
  • Decryption

    • Use the same AES key and IV/Nonce.
    • Use the GCM mode while initializing the Cipher.
    • Using the Cipher, key, IV and the tag that was obtained, decrypt the Ciphertext.
      // decrypt the ciphertext
      let plaintext_decrypted = decrypt_aead(
          cipher,
          &key,
          Some(&iv),
          aad,
          &ciphertext,
          &mut tag)?;
    

The complete code is as follows.

Cargo.toml

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

[dependencies]
openssl = "0.10.68"

main.rs

use openssl::rand::rand_bytes;
use openssl::symm::{decrypt_aead, encrypt_aead, Cipher};
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let plaintext_input = "this is plain text";
    println!("plaintext input: {}", plaintext_input);
    println!("plaintext_input bytes: {:?}", plaintext_input.as_bytes());

    // Generate a 192-bit (24-byte) AES key using CSPRNG
    // which is openssl::rand::rand_bytes
    let mut key = vec![0; 24];
    let _ = rand_bytes(&mut key)?;
    println!("key: {:?}", key);

    // Generate an IV with 12 bytes of randomness
    let mut iv = vec![0; 12];
    let _ = rand_bytes(&mut iv)?;
    println!("iv: {:?}", iv);

    let aad = b"$3CuRE-D4T4";
    // Buffer for the authentication tag
    let mut tag = [0u8; 16];

    // initialize the cipher AES 192 bits gcm mode
    let cipher = Cipher::aes_192_gcm();

    // encrypt the plaintext
    let ciphertext = encrypt_aead(
        cipher,
        &key,
        Some(&iv),
        aad,
        plaintext_input.as_bytes(),
        &mut tag,
    )?;
    println!("ciphertext: {:?}", ciphertext);
 
    // decrypt the ciphertext
    let plaintext_decrypted = decrypt_aead(
        cipher,
        &key,
        Some(&iv),
        aad,
        &ciphertext,
        &mut tag)?;

    // print the decrypted data
    println!("plaintext decrypted bytes: {:?}", plaintext_decrypted);
    let plaintext_decrypted_str = String::from_utf8(plaintext_decrypted)?;
    println!("plaintext_decrypted string: {}", plaintext_decrypted_str);

    Ok(())
}

Output

$ cargo run --release
    Finished release [optimized] target(s) in 0.01s
     Running `target/release/openssl_demo_aes_gcm`
plaintext input: this is plain text
plaintext_input bytes: [116, 104, 105, 115, 32, 105, 115, 32, 112, 108, 97, 105, 110, 32, 116, 101, 120, 116]
key: [42, 88, 63, 171, 3, 67, 247, 133, 72, 132, 163, 209, 95, 38, 32, 2, 209, 104, 250, 42, 159, 168, 0, 175]
iv: [160, 137, 110, 196, 97, 138, 164, 245, 6, 114, 155, 242]
ciphertext: [13, 51, 148, 136, 40, 84, 102, 61, 148, 109, 34, 179, 45, 29, 238, 184, 134, 218]
plaintext decrypted bytes: [116, 104, 105, 115, 32, 105, 115, 32, 112, 108, 97, 105, 110, 32, 116, 101, 120, 116]
plaintext_decrypted string: this is plain text

Points to Remember

  • There is a slight difference between IV that is used in CBC mode and the IV/Nonce used in the GCM mode

    • For GCM, a nonce must never be repeated for the same AES key.
    • For CBC, the requirement is that the Initialization Vector must be unpredictable in advance to an adversary.
  • IV/Nonce size: NIST's Recommendation states:

    For IVs, it is recommended that implementations restrict support to the 
    length of 96 bits, to promote interoperability, efficiency, and 
    simplicity of design.
    
    • OpenSSL supports IVs of any length (defaulting to 96 bits), but non-standard lengths require additional processing.
    • If the IV is not 96 bits, OpenSSL uses GHASH to derive a 96-bit IV internally, which adds computational overhead.
  • In real-world scenarios, usually the AAD is passed as metadata such as header fields in an HTTP request. It is not the same thing as a password as mentioned above.

References