Encryption and Decryption Using Rust's Ring Crate
January 15th, 2023
Rust does not provide encryption methods in its standard library, so cryptographic functionality must be included
with external dependencies. The ring
crate is probably the most popular and well-tested of the
available
crates on crates.io. Unfortunately, this library is much more low-level than many
developers may be familiar with. This article includes a code sample and explanation of how to use this crate
effectively.
/// Aes256GcmEngine is a high-level encryption engine. Once created, it
/// can encrypt and decrypt slices of bytes, using a single parameter to
/// `encrypt_bytes` or `decrypt_bytes`, providing a conceptually simple
/// model of symmetric encryption.
///
/// An Aes256GcmEngine must be initialized with a plaintext password and
/// salt byte slice. The encryption key passed to the SealingKey and
/// OpeningKey is derived from the password using PBKDF2_HMAC_SHA256, in
/// `derive_key_from_pass`. `new` may panic under catastrophic
/// circumstances, namely if 100000 is not a valid u32 or if the system
/// is unable to fill bytes with random values.
///
/// ```
/// use rust_ring_aead::Aes256GcmEngine;
/// let engine = Aes256GcmEngine::new(String::from("my_password"), &[1u8, 2, 3, 4, 5, 6]);
///
/// let payload = [1u8, 2, 3];
/// let encrypted = engine.encrypt_bytes(payload.as_slice()).unwrap();
///
/// assert_eq!(payload.as_slice(), engine.decrypt_bytes(encrypted.as_slice()).unwrap());
/// ```
pub struct Aes256GcmEngine {
key: [u8; 32],
counter: InitializedNonceSequence,
}
impl Aes256GcmEngine {
pub fn new(pass: String, salt: &[u8]) -> Self {
Self {
key: derive_key_from_pass(pass, salt),
counter: InitializedNonceSequence::new(new_iv().unwrap()),
}
}
pub fn encrypt_bytes(&self, payload: &[u8]) -> Result<Vec<u8>, ring::error::Unspecified> {
let nonce_bytes = self.counter.current();
let mut sealing_key = SealingKey::new(UnboundKey::new(&AES_256_GCM, &self.key)?, self.counter);
let mut raw = payload.to_owned();
sealing_key.seal_in_place_append_tag(Aad::empty(), &mut raw)?;
// Append the nonce to the beginning of the encrypted bytes
let mut data = nonce_bytes.to_vec();
data.append(&mut raw);
Ok(data)
}
pub fn decrypt_bytes(&self, bytes: &[u8]) -> Result<Vec<u8>, ring::error::Unspecified> {
// Split the incoming bytes at the nonce length
let (nonce_bytes, bytes) = bytes.split_at(NONCE_LEN);
let mut opening_key = OpeningKey::new(
UnboundKey::new(&AES_256_GCM, &self.key)?,
InitializedNonceSequence::new(nonce_bytes.try_into()?),
);
let mut raw = bytes.to_owned();
let plaintext = opening_key.open_in_place(Aad::empty(), &mut raw)?;
Ok(plaintext.to_owned())
}
}
/// InitializedNonceSequence represents a NonceSequence initialized with
/// a random sequence of 12 bytes. These bytes are interpreted as a u128
/// for quick advancement of the counter.
///
/// Allow for copy and clone of this counter to pass to each sealing key.
/// Normally, we would not want to copy a Nonce sequence, since this
/// would lead to duplication of nonces and therefore compromise the
/// security of the encryption. However, here we only copy the nonce
/// sequence so we are able to append the nonce to the resulting
/// ciphertext. Nonces are safe to pass in clear text since they are
/// unique for each invocation, and we do so here so we can decrypt the
/// ciphertext without needing to internally track the nonces used.
#[derive(Copy, Clone)]
struct InitializedNonceSequence(u128);
impl InitializedNonceSequence {
fn new(iv: [u8; NONCE_LEN]) -> Self {
let mut bytes = [0u8; 16];
iv.into_iter().enumerate().for_each(|(i, b)| bytes[i + 4] = b);
Self(u128::from_be_bytes(bytes))
}
// Gets the current nonce so it can be added to ciphertext. This will unwrap the
// result of `try_into`, which will only fail if the nonce is an invalid u128.
// This *should* never happen, since [u8; 12] will always be less than a u128
fn current(&self) -> [u8; 12] {
self.0.to_be_bytes()[4..].try_into().unwrap()
}
}
/// Implement a NonceSequence for the InitializedNonce. Each time the sequence
/// is advanced, the current value of the counter is returned, and the counter
/// is incremented by one, mod 2^96. This ensures that any InitializedNonceSequence
/// sequences can use the nonce obtained from ciphertext.
impl NonceSequence for InitializedNonceSequence {
fn advance(&mut self) -> Result<Nonce, ring::error::Unspecified> {
// Use the current value of the counter as the nonce
let nonce = Nonce::try_assume_unique_for_key(&self.current())?;
// Increase the counter for the next invocation.
// 79228162514264337593543950336 = 2^96, the total number of possible nonces
self.0 = (self.0 + 1) % 79228162514264337593543950336u128;
Ok(nonce)
}
}
// Create a new random initialization vector, or counter, to use in a NonceSequence
fn new_iv() -> Result<[u8; NONCE_LEN], ring::error::Unspecified> {
let mut nonce_buf = [0u8; NONCE_LEN];
SystemRandom::new().fill(&mut nonce_buf)?;
Ok(nonce_buf)
}
fn derive_key_from_pass(pass: String, salt: &[u8]) -> [u8; 32] {
// Byte buffer to store derived bytes
let mut key = [0u8; 32];
// Derive the key and store in `key`
derive(PBKDF2_HMAC_SHA256, NonZeroU32::new(100000u32).unwrap(), salt, &pass.as_bytes(), &mut key);
key
}
AEAD, or 'Authenticated Encryption with Associated Data', is a family of cryptographic algorithms including AES. These algorithms can encrypt data and additionally include data that is unencrypted, but can be verified against tampering. The specific algorithm used by this code sample is AES-256-GCM.
The Aes256GcmEngine
provides a high-level interface to encrypt and decrypt data. This struct must be
initialized with a plaintext password along with salt bytes. We add additional entropy to the plaintext password by
using PBKDF2 key derivation. During construction of this struct,
a random initialization vector is created to start the nonce sequence. Since a unique nonce must be used for
each encryption, the advance
function will increase the random value by one each time it is used. This
could also be implemented by generating a new random value each time.
In the Aes256GcmEngine
, encrypt_bytes
provides the implementation of encryption. First,
we must copy the current nonce value from the nonce sequence to use later. We can then create a new SealingKey
with
the engine's derived key and nonce sequence. Next, we copy the payload into a slice owned by the function so it
can be modified and seal the new slice in-place by overwriting the bytes. Our new ciphertext is encrypted, and we
append the copied nonce to the beginning of the ciphertext to enable decryption. A nonce is unique for each
encryption, so it is safe to send in plaintext. This nonce is useless without the sealing key and would not help
an attacker as long as every nonce is unique.
Decryption is implemented in a similar way in decrypt_bytes
. First, the nonce is split from the
ciphertext at a fixed index since the nonce will always be 12 bytes for AES-GCM encryption. Then, an OpeningKey
is created using the engine's derived key along with the nonce from the ciphertext. Finally, we copy the bytes to
an owned slice so we can modify them in-place and overwrite the bytes with the plaintext bytes.
The full code for this examples can be found here: github.com/david-wiles/rust-ring-aead. This also includes a REPL to interactively encrypt and decrypt text.