Skip to content
Blog » Authenticated Encryption in Rust using Ring

Authenticated Encryption in Rust using Ring

    Authenticated Encryption with Associated Data (AEAD) is a modern cryptography primitive that enables secure encryption and decryption of data using a symmetric key in a way that prevents it from being altered or tampered with. The Ring cryptography library implements the AES-GCM and ChaCha20-Poly1305 authenticated encryption algorithms which are two of the most commonly used schemes on the internet. In this post I show you how to use the ring::aead module to generate an encryption key, encrypt some data and then decrypt using the provided UnboundKey, SealingKey and OpeningKey types.

    Introduction to AEAD

    Basic encryption (without authentication) only protects against a passive man in the middle attacker who can observe the data as it travels over an insecure channel but it doesn’t prevent active attacks where the encrypted ciphertext can still be altered. Allowing the ciphertext to be altered without detection can lead to a number of vulnerabilities and so modern systems generally use authenticated encryption to prevent this. For example the AES-CBC-HMAC scheme uses HMAC authentication tags to verify the integrity of the ciphertext after encryption.

    AEAD is a type of authenticated encryption which supports data integrity verification and additionally allows you to bind some plaintext associated data to the ciphertext which must be supplied as a part of the decryption. The decryption will fail if either the ciphertext or the associated data was altered as both are part of the integrity checks. The associated data is not encrypted but is authenticated and is useful for sending contextual information related to the encryption.

    AES with the Galois/Counter mode (AES-GCM) is the most widely used AEAD. It has hardware support on many devices and internally uses GMAC which is a high performance MAC algorithm which is used for the integrity checks. It is based on AES-CTR which uses a nonce combined with an incrementing counter to produce a key stream which is then XORed with the plaintext to produce the ciphertext. This is why the algorithm requires supplying a nonce as a part of the encryption and decryption as you will see later in this post.

    ChaCha20-Poly1305 is an AEAD designed by Daniel J. Bernstein which was standardised by Google in 2013. It is faster than AES-GCM when AES hardware support is not available and so it is generally preferred for low end devices. Similar to AES-CTR, ChaCha20 is used to encrypt a nonce and counter to produce a key stream which is XORed with the plaintext to produce the ciphertext. Poly1305 is a high performance MAC algorithm much like GMAC which is used to verify the integrity of messages.

    AEAD Usage Guidelines

    • For both AES-GCM and ChaCha20-Poly1305 a unique nonce must be supplied for each encryption operation. Repeated nonce usage can lead to vulnerabilities and so care must be taken to ensure that that there are no duplicate nonce values used with the same encryption key.
    • Each nonce needs to be unique but doesn’t need to be unpredictable.
    • The nonce has a size of 12 bytes which can hold up to 2^96 – 1 distinct values.
    • An incrementing counter could be used as the nonce but requires keeping track of the state of the last counter used which can be error prone. If using a counter for the nonce the maximum number of encryption operations per key is 2^96 – 1.
    • Alternatively a randomised nonce can be used which randomly generates a new nonce for each encryption operation. If using a random nonce no more than 2^30 encryption operations should be used to prevent increasing the probability of randomly generating a duplicate nonce.
    • Encryption keys should be rotated periodically if encrypting a larger amount a data.
    • Use ChaCha20-Poly1305 instead of AES-GCM when hardware support for AES is not available.

    Summary of Types

    The ring::aead module contains the following types and functions:

    trait BoundKey – An AEAD key bound to a nonce sequence. Both SealingKey and OpeningKey implement this trait.

    trait NonceSequence – A sequence of unique nonces. We will create a type which implements this trait for generating nonces as the library doesn’t implement this for us.

    struct Algorithm – An AEAD algorithm. AES_128_GCM, AES_256_GCM and CHACHA20_POLY1305 algorithms are supported.

    struct UnboundKey – An AEAD key without a designated role or nonce sequence.

    struct SealingKey – An AEAD key for encrypting and signing (“sealing”), bound to a nonce sequence.

    struct Nonce – A nonce for a single AEAD opening or sealing operation. Returned by the NonceSequence type.

    struct Aad – The additionally authenticated data (AAD) for an opening or sealing operation. This data is authenticated but is not encrypted.

    struct Tag – An authentication tag. Used for integrity checking the ciphertext during decryption.

    struct OpeningKey – An AEAD key for authenticating and decrypting (“opening”), bound to a nonce sequence.

    Rust Imports

    Let’s start by importing the required types into our project.

    use ring::error::Unspecified;
    use ring::rand::SecureRandom;
    use ring::rand::SystemRandom;
    use ring::aead::Algorithm;
    use ring::aead::AES_128_GCM;
    use ring::aead::AES_256_GCM;
    use ring::aead::CHACHA20_POLY1305;
    use ring::aead::UnboundKey;
    use ring::aead::BoundKey;
    use ring::aead::SealingKey;
    use ring::aead::OpeningKey;
    use ring::aead::Aad;
    use ring::aead::Tag;
    use ring::aead::NonceSequence;
    use ring::aead::NONCE_LEN;
    use ring::aead::Nonce;

    Generate an Encryption Key

    We start by generating a symmetric encryption key by using a new instance of SystemRandom from the rand module. We collect 32 bytes of random data and use it as our secret key which is passed in to create a new instance of UnboundKey which wraps the key value.

    // Create a new instance of SystemRandom to be used as the single source of entropy
    let rand = SystemRandom::new();
    
    // Generate a new symmetric encryption key
    let mut key_bytes = vec![0; AES_256_GCM.key_len()];
    rand.fill(&mut key_bytes)?;
    println!("key_bytes = {}", hex::encode(&key_bytes)); // don't print this in production code
    
    // Create a new AEAD key without a designated role or nonce sequence
    let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;

    Create a NonceSequence & SealingKey

    Next we create a NonceSequence by implementing the trait. In this example I am using a simple incrementing counter as the method to generate nonces. We then create a new instance of the NonceSequence starting from one and then pass it along with the UnboundKey to create a new SealingKey which is a type that binds the key to the nonce sequence. The first nonce value used will be one and so on. Note that the UnboundKey and NonceSequence instances are moved when creating the new SealingKey. This design help to prevent reuse of the these sensitive values.

    struct CounterNonceSequence(u32);
    
    impl NonceSequence for CounterNonceSequence {
        // called once for each seal operation
        fn advance(&mut self) -> Result<Nonce, Unspecified> {
            let mut nonce_bytes = vec![0; NONCE_LEN];
    
            let bytes = self.0.to_be_bytes();
            nonce_bytes[8..].copy_from_slice(&bytes);
            println!("nonce_bytes = {}", hex::encode(&nonce_bytes));
    
            self.0 += 1; // advance the counter
            Nonce::try_assume_unique_for_key(&nonce_bytes)
        }
    }
    
    // Create a new NonceSequence type which generates nonces
    let nonce_sequence = CounterNonceSequence(1);
    
    // Create a new AEAD key for encrypting and signing ("sealing"), bound to a nonce sequence
    // The SealingKey can be used multiple times, each time a new nonce will be used
    let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence);

    Prepare Data & Encrypt Using SealingKey

    We prepare the data to be encrypted, the associated data, and then create a copy of the data so it can be encrypted in place. The original copy of the data will simply be used for comparison later. We then use the SealingKey::seal_in_place_separate_tag method to encrypt the data, bind the associated data to it, and return the authentication tag. After returning, the in_out variable will contain the ciphertext. The SealingKey instance can be used to encrypt data multiple times using the same key.

    // This data will be authenticated but not encrypted
    //let associated_data = Aad::empty(); // is optional so can be empty
    let associated_data = Aad::from(b"additional public data");
    
    // Data to be encrypted
    let data = b"hello world";
    println!("data = {}", String::from_utf8(data.to_vec()).unwrap());
    
    // Create a mutable copy of the data that will be encrypted in place
    let mut in_out = data.clone();
    
    // Encrypt the data with AEAD using the AES_256_GCM algorithm
    let tag = sealing_key.seal_in_place_separate_tag(associated_data, &mut in_out)?;

    Create an OpeningKey for Decryption

    We need to create new instances of some of the types because they were previously moved during use. We then create a new instance of OpeningKey which binds the UnboundKey to the NonceSequence ready for decryption. Note that the NonceSequence needs to generate the same nonce values that were used during the encryption. In this example I’m starting the nonce counter from one again.

    // Recreate the previously moved variables
    let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;
    let nonce_sequence = CounterNonceSequence(1);
    //let associated_data = Aad::empty(); // supplying the wrong data causes the decryption to fail
    let associated_data = Aad::from(b"additional public data");
    
    // Create a new AEAD key for decrypting and verifying the authentication tag
    let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence);

    Decrypt the Ciphertext

    Finally we decrypt the ciphertext using the OpeningKey::open_in_place method, passing in the associated data and the ciphertext concatenated with the authentication tag. If the integrity checks using the authentication tag succeed then the decrypted data is returned from the method otherwise an error::Unspecified is returned.

    // Decrypt the data by passing in the associated data and the cyphertext with the authentication tag appended
    let mut cypher_text_with_tag = [&in_out, tag.as_ref()].concat();
    let decrypted_data = opening_key.open_in_place(associated_data, &mut cypher_text_with_tag)?;
    println!("decrypted_data = {}", String::from_utf8(decrypted_data.to_vec()).unwrap());
    
    assert_eq!(data, decrypted_data);

    Full Sample Code

    Here is the full code sample for reference. 

    use ring::error::Unspecified;
    use ring::rand::SecureRandom;
    use ring::rand::SystemRandom;
    use ring::aead::Algorithm;
    use ring::aead::AES_128_GCM;
    use ring::aead::AES_256_GCM;
    use ring::aead::CHACHA20_POLY1305;
    use ring::aead::UnboundKey;
    use ring::aead::BoundKey;
    use ring::aead::SealingKey;
    use ring::aead::OpeningKey;
    use ring::aead::Aad;
    use ring::aead::Tag;
    use ring::aead::NonceSequence;
    use ring::aead::NONCE_LEN;
    use ring::aead::Nonce;
    
    
    struct CounterNonceSequence(u32);
    
    impl NonceSequence for CounterNonceSequence {
        // called once for each seal operation
        fn advance(&mut self) -> Result<Nonce, Unspecified> {
            let mut nonce_bytes = vec![0; NONCE_LEN];
    
            let bytes = self.0.to_be_bytes();
            nonce_bytes[8..].copy_from_slice(&bytes);
            println!("nonce_bytes = {}", hex::encode(&nonce_bytes));
    
            self.0 += 1; // advance the counter
            Nonce::try_assume_unique_for_key(&nonce_bytes)
        }
    }
    
    fn main() -> Result<(), Unspecified> {
        // Create a new instance of SystemRandom to be used as the single source of entropy
        let rand = SystemRandom::new();
    
        // Generate a new symmetric encryption key
        let mut key_bytes = vec![0; AES_256_GCM.key_len()];
        rand.fill(&mut key_bytes)?;
        println!("key_bytes = {}", hex::encode(&key_bytes)); // don't print this in production code
    
        // Create a new AEAD key without a designated role or nonce sequence
        let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;
    
        // Create a new NonceSequence type which generates nonces
        let nonce_sequence = CounterNonceSequence(1);
    
        // Create a new AEAD key for encrypting and signing ("sealing"), bound to a nonce sequence
        // The SealingKey can be used multiple times, each time a new nonce will be used
        let mut sealing_key = SealingKey::new(unbound_key, nonce_sequence);
    
    
        // This data will be authenticated but not encrypted
        //let associated_data = Aad::empty(); // is optional so can be empty
        let associated_data = Aad::from(b"additional public data");
    
        // Data to be encrypted
        let data = b"hello world";
        println!("data = {}", String::from_utf8(data.to_vec()).unwrap());
    
        // Create a mutable copy of the data that will be encrypted in place
        let mut in_out = data.clone();
    
        // Encrypt the data with AEAD using the AES_256_GCM algorithm
        let tag = sealing_key.seal_in_place_separate_tag(associated_data, &mut in_out)?;
    
        // Recreate the previously moved variables
        let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes)?;
        let nonce_sequence = CounterNonceSequence(1);
        //let associated_data = Aad::empty(); // supplying the wrong data causes the decryption to fail
        let associated_data = Aad::from(b"additional public data");
    
        // Create a new AEAD key for decrypting and verifying the authentication tag
        let mut opening_key = OpeningKey::new(unbound_key, nonce_sequence);
    
        // Decrypt the data by passing in the associated data and the cypher text with the authentication tag appended
        let mut cypher_text_with_tag = [&in_out, tag.as_ref()].concat();
        let decrypted_data = opening_key.open_in_place( associated_data, &mut cypher_text_with_tag)?;
        println!("decrypted_data = {}", String::from_utf8(decrypted_data.to_vec()).unwrap());
    
        assert_eq!(data, decrypted_data);
        Ok(())
    }

    Conclusion

    In this post I introduced the concept of authenticated encryption and explained what it is used for and how it works. I also provided an overview of the two most commonly used AEAD algorithms AES-GCM and ChaCha20-Poly1305 and explained how they both require a nonce as input which must to be unique in order to ensure the security of the encryption scheme. We then walked through an example where we randomly generated a symmetric key then used the UnboundKey, SealingKey and OpeningKey to securely encrypt the data and then later decrypt by supplying the authentication tag and associated data along with the key.

    Leave a Reply

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