| // Copyright 2023 The BoringSSL Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! Authenticated Encryption with Additional Data. | |
| //! | |
| //! AEAD couples confidentiality and integrity in a single primitive. AEAD | |
| //! algorithms take a key and then can seal and open individual messages. Each | |
| //! message has a unique, per-message nonce and, optionally, additional data | |
| //! which is authenticated but not included in the ciphertext. | |
| //! | |
| //! No two distinct plaintexts must ever be sealed using the same (key, nonce) | |
| //! pair. It is up to the user of these algorithms to ensure this. For example, | |
| //! when encrypting a stream of messages (e.g. over a TCP socket) a message | |
| //! counter can provide distinct nonces as long as the key is randomly generated | |
| //! for the specific connection and is distinct in each direction. | |
| //! | |
| //! To implement that example: | |
| //! | |
| //! ``` | |
| //! use bssl_crypto::aead::{Aead, Aes256Gcm}; | |
| //! | |
| //! let key = bssl_crypto::rand_array(); | |
| //! let aead = Aes256Gcm::new(&key); | |
| //! | |
| //! let mut message_counter: u64 = 0; | |
| //! let mut nonce = bssl_crypto::rand_array(); | |
| //! nonce[4..].copy_from_slice(message_counter.to_be_bytes().as_slice()); | |
| //! message_counter += 1; | |
| //! let plaintext = b"message"; | |
| //! let ciphertext = aead.seal(&nonce, plaintext, b""); | |
| //! | |
| //! let decrypted = aead.open(&nonce, ciphertext.as_slice(), b""); | |
| //! assert_eq!(plaintext, decrypted.unwrap().as_slice()); | |
| //! ``` | |
| use crate::{with_output_array, with_output_vec, with_output_vec_fallible, FfiMutSlice, FfiSlice}; | |
| use alloc::vec::Vec; | |
| /// The error type returned when a fallible, in-place operation fails. | |
| #[derive(Debug)] | |
| pub struct InvalidCiphertext; | |
| /// Authenticated Encryption with Associated Data (AEAD) algorithm trait. | |
| pub trait Aead { | |
| /// The type of tags produced by this AEAD. Generally a u8 array of fixed | |
| /// length. | |
| type Tag: AsRef<[u8]>; | |
| /// The type of nonces used by this AEAD. Generally a u8 array of fixed | |
| /// length. | |
| type Nonce: AsRef<[u8]>; | |
| /// Encrypt and authenticate `plaintext`, and authenticate `ad`, returning | |
| /// the result as a freshly allocated [`Vec`]. The `nonce` must never | |
| /// be used in any sealing operation with the same key, ever again. | |
| fn seal(&self, nonce: &Self::Nonce, plaintext: &[u8], ad: &[u8]) -> Vec<u8>; | |
| /// Encrypt and authenticate `plaintext`, and authenticate `ad`, writing | |
| /// the ciphertext over `plaintext` and additionally returning the calculated | |
| /// tag, which is usually appended to the ciphertext. The `nonce` must never | |
| /// be used in any sealing operation with the same key, ever again. | |
| fn seal_in_place(&self, nonce: &Self::Nonce, plaintext: &mut [u8], ad: &[u8]) -> Self::Tag; | |
| /// Authenticate `ciphertext` and `ad` and, if valid, decrypt `ciphertext`, | |
| /// returning the original plaintext in a newly allocated [`Vec`]. The `nonce` | |
| /// must be the same value as given to the sealing operation that produced | |
| /// `ciphertext`. | |
| fn open(&self, nonce: &Self::Nonce, ciphertext: &[u8], ad: &[u8]) -> Option<Vec<u8>>; | |
| /// Authenticate `ciphertext` and `ad` using `tag` and, if valid, decrypt | |
| /// `ciphertext` in place. The `nonce` must be the same value as given to | |
| /// the sealing operation that produced `ciphertext`. | |
| fn open_in_place( | |
| &self, | |
| nonce: &Self::Nonce, | |
| ciphertext: &mut [u8], | |
| tag: &Self::Tag, | |
| ad: &[u8], | |
| ) -> Result<(), InvalidCiphertext>; | |
| } | |
| /// AES-128 in Galois Counter Mode. | |
| pub struct Aes128Gcm(EvpAead<16, 12, 16>); | |
| aead_algo!(Aes128Gcm, EVP_aead_aes_128_gcm, 16, 12, 16); | |
| /// AES-256 in Galois Counter Mode. | |
| pub struct Aes256Gcm(EvpAead<32, 12, 16>); | |
| aead_algo!(Aes256Gcm, EVP_aead_aes_256_gcm, 32, 12, 16); | |
| /// AES-128 in GCM-SIV mode (which is different from SIV mode!). | |
| pub struct Aes128GcmSiv(EvpAead<16, 12, 16>); | |
| aead_algo!(Aes128GcmSiv, EVP_aead_aes_128_gcm_siv, 16, 12, 16); | |
| /// AES-256 in GCM-SIV mode (which is different from SIV mode!). | |
| pub struct Aes256GcmSiv(EvpAead<32, 12, 16>); | |
| aead_algo!(Aes256GcmSiv, EVP_aead_aes_256_gcm_siv, 32, 12, 16); | |
| /// The AEAD built from ChaCha20 and Poly1305 as described in <https://datatracker.ietf.org/doc/html/rfc8439>. | |
| pub struct Chacha20Poly1305(EvpAead<32, 12, 16>); | |
| aead_algo!(Chacha20Poly1305, EVP_aead_chacha20_poly1305, 32, 12, 16); | |
| /// Chacha20Poly1305 with an extended nonce that makes random generation of nonces safe. | |
| pub struct XChacha20Poly1305(EvpAead<32, 24, 16>); | |
| aead_algo!(XChacha20Poly1305, EVP_aead_xchacha20_poly1305, 32, 24, 16); | |
| /// An internal struct that implements AEAD operations given an `EVP_AEAD`. | |
| struct EvpAead<const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize>( | |
| *mut bssl_sys::EVP_AEAD_CTX, | |
| ); | |
| #[allow(clippy::unwrap_used)] | |
| impl<const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> | |
| EvpAead<KEY_LEN, NONCE_LEN, TAG_LEN> | |
| { | |
| // Tagged unsafe because `evp_aead` must be valid. | |
| unsafe fn new(key: &[u8; KEY_LEN], evp_aead: *const bssl_sys::EVP_AEAD) -> Self { | |
| // `evp_aead` is assumed to be valid. The function will validate | |
| // the other lengths and return NULL on error. In that case we | |
| // crash the address space because that should never happen. | |
| let ptr = | |
| unsafe { bssl_sys::EVP_AEAD_CTX_new(evp_aead, key.as_ffi_ptr(), key.len(), TAG_LEN) }; | |
| assert!(!ptr.is_null()); | |
| Self(ptr) | |
| } | |
| fn seal(&self, nonce: &[u8; NONCE_LEN], plaintext: &[u8], ad: &[u8]) -> Vec<u8> { | |
| let max_output = plaintext.len() + TAG_LEN; | |
| unsafe { | |
| with_output_vec(max_output, |out_buf| { | |
| let mut out_len = 0usize; | |
| // Safety: the input buffers are all valid, with corresponding | |
| // ptr and length. The output buffer has at least `max_output` | |
| // bytes of space and that maximum is passed to | |
| // `EVP_AEAD_CTX_seal` as a limit. | |
| let result = bssl_sys::EVP_AEAD_CTX_seal( | |
| self.0, | |
| out_buf, | |
| &mut out_len, | |
| max_output, | |
| nonce.as_ffi_ptr(), | |
| nonce.len(), | |
| plaintext.as_ffi_ptr(), | |
| plaintext.len(), | |
| ad.as_ffi_ptr(), | |
| ad.len(), | |
| ); | |
| // Sealing never fails unless there's a programmer error. | |
| assert_eq!(result, 1); | |
| // For the implemented AEADs, we should always have calculated | |
| // the overhead exactly. | |
| assert_eq!(out_len, max_output); | |
| // Safety: `out_len` bytes have been written to. | |
| out_len | |
| }) | |
| } | |
| } | |
| fn seal_in_place( | |
| &self, | |
| nonce: &[u8; NONCE_LEN], | |
| plaintext: &mut [u8], | |
| ad: &[u8], | |
| ) -> [u8; TAG_LEN] { | |
| // Safety: the buffers are all valid, with corresponding ptr and length. | |
| // `tag_len` is passed at the maximum size of `tag` and `out_tag_len` | |
| // is checked to ensure that the whole output was written to. | |
| unsafe { | |
| with_output_array(|tag, tag_len| { | |
| let mut out_tag_len = 0usize; | |
| let result = bssl_sys::EVP_AEAD_CTX_seal_scatter( | |
| self.0, | |
| plaintext.as_mut_ffi_ptr(), | |
| tag, | |
| &mut out_tag_len, | |
| tag_len, | |
| nonce.as_ffi_ptr(), | |
| nonce.len(), | |
| plaintext.as_ffi_ptr(), | |
| plaintext.len(), | |
| /*extra_in=*/ core::ptr::null(), | |
| /*extra_in_len=*/ 0, | |
| ad.as_ffi_ptr(), | |
| ad.len(), | |
| ); | |
| // Failure indicates that one of the configured lengths was wrong. | |
| // Crashing is a good answer in that case. | |
| assert_eq!(result, 1); | |
| // The whole output must have been written to. | |
| assert_eq!(out_tag_len, TAG_LEN); | |
| }) | |
| } | |
| } | |
| fn open(&self, nonce: &[u8; NONCE_LEN], ciphertext: &[u8], ad: &[u8]) -> Option<Vec<u8>> { | |
| if ciphertext.len() < TAG_LEN { | |
| return None; | |
| } | |
| let max_output = ciphertext.len() - TAG_LEN; | |
| unsafe { | |
| with_output_vec_fallible(max_output, |out_buf| { | |
| let mut out_len = 0usize; | |
| // Safety: the input buffers are all valid, with corresponding | |
| // ptr and length. The output buffer has at least `max_output` | |
| // bytes of space and that maximum is passed to | |
| // `EVP_AEAD_CTX_open` as a limit. | |
| let result = bssl_sys::EVP_AEAD_CTX_open( | |
| self.0, | |
| out_buf, | |
| &mut out_len, | |
| max_output, | |
| nonce.as_ffi_ptr(), | |
| nonce.len(), | |
| ciphertext.as_ffi_ptr(), | |
| ciphertext.len(), | |
| ad.as_ffi_ptr(), | |
| ad.len(), | |
| ); | |
| if result == 1 { | |
| // Safety: `out_len` bytes have been written to. | |
| Some(out_len) | |
| } else { | |
| None | |
| } | |
| }) | |
| } | |
| } | |
| fn open_in_place( | |
| &self, | |
| nonce: &[u8; NONCE_LEN], | |
| ciphertext: &mut [u8], | |
| tag: &[u8; TAG_LEN], | |
| ad: &[u8], | |
| ) -> Result<(), InvalidCiphertext> { | |
| // Safety: | |
| // - The buffers are all valid, with corresponding ptr and length | |
| let result = unsafe { | |
| bssl_sys::EVP_AEAD_CTX_open_gather( | |
| self.0, | |
| ciphertext.as_mut_ffi_ptr(), | |
| nonce.as_ffi_ptr(), | |
| nonce.len(), | |
| ciphertext.as_ffi_ptr(), | |
| ciphertext.len(), | |
| tag.as_ffi_ptr(), | |
| tag.len(), | |
| ad.as_ffi_ptr(), | |
| ad.len(), | |
| ) | |
| }; | |
| if result == 1 { | |
| Ok(()) | |
| } else { | |
| Err(InvalidCiphertext) | |
| } | |
| } | |
| } | |
| impl<const KEY_LEN: usize, const NONCE_LEN: usize, const TAG_LEN: usize> Drop | |
| for EvpAead<KEY_LEN, NONCE_LEN, TAG_LEN> | |
| { | |
| fn drop(&mut self) { | |
| // Safety: `self.0` was initialized by `EVP_AEAD_CTX_init` because all | |
| // paths to create an `EvpAead` do so. | |
| unsafe { bssl_sys::EVP_AEAD_CTX_free(self.0) } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod test { | |
| use super::*; | |
| use crate::test_helpers::{decode_hex, decode_hex_into_vec}; | |
| fn check_aead_invariants< | |
| const NONCE_LEN: usize, | |
| const TAG_LEN: usize, | |
| A: Aead<Nonce = [u8; NONCE_LEN], Tag = [u8; TAG_LEN]>, | |
| >( | |
| aead: A, | |
| ) { | |
| let plaintext = b"plaintext"; | |
| let ad = b"additional data"; | |
| let nonce: A::Nonce = [0u8; NONCE_LEN]; | |
| let mut ciphertext = aead.seal(&nonce, plaintext, ad); | |
| let plaintext2 = aead | |
| .open(&nonce, ciphertext.as_slice(), ad) | |
| .expect("should decrypt"); | |
| assert_eq!(plaintext, plaintext2.as_slice()); | |
| ciphertext[0] ^= 1; | |
| assert!(aead.open(&nonce, ciphertext.as_slice(), ad).is_none()); | |
| ciphertext[0] ^= 1; | |
| let (ciphertext_in_place, tag_slice) = | |
| ciphertext.as_mut_slice().split_at_mut(plaintext.len()); | |
| let tag: [u8; TAG_LEN] = tag_slice.try_into().unwrap(); | |
| aead.open_in_place(&nonce, ciphertext_in_place, &tag, ad) | |
| .expect("should decrypt"); | |
| assert_eq!(plaintext, ciphertext_in_place); | |
| let tag = aead.seal_in_place(&nonce, ciphertext_in_place, ad); | |
| aead.open_in_place(&nonce, ciphertext_in_place, &tag, ad) | |
| .expect("should decrypt"); | |
| assert_eq!(plaintext, ciphertext_in_place); | |
| assert!(aead.open(&nonce, b"tooshort", b"").is_none()); | |
| } | |
| #[test] | |
| fn aes_128_gcm_invariants() { | |
| check_aead_invariants(Aes128Gcm::new(&[0u8; 16])); | |
| } | |
| #[test] | |
| fn aes_256_gcm_invariants() { | |
| check_aead_invariants(Aes256Gcm::new(&[0u8; 32])); | |
| } | |
| #[test] | |
| fn aes_128_gcm_siv_invariants() { | |
| check_aead_invariants(Aes128GcmSiv::new(&[0u8; 16])); | |
| } | |
| #[test] | |
| fn aes_256_gcm_siv_invariants() { | |
| check_aead_invariants(Aes256GcmSiv::new(&[0u8; 32])); | |
| } | |
| #[test] | |
| fn chacha20_poly1305_invariants() { | |
| check_aead_invariants(Chacha20Poly1305::new(&[0u8; 32])); | |
| } | |
| #[test] | |
| fn xchacha20_poly1305_invariants() { | |
| check_aead_invariants(XChacha20Poly1305::new(&[0u8; 32])); | |
| } | |
| struct TestCase<const KEY_LEN: usize, const NONCE_LEN: usize> { | |
| key: [u8; KEY_LEN], | |
| nonce: [u8; NONCE_LEN], | |
| msg: Vec<u8>, | |
| ad: Vec<u8>, | |
| ciphertext: Vec<u8>, | |
| } | |
| fn check_test_cases< | |
| const KEY_LEN: usize, | |
| const NONCE_LEN: usize, | |
| const TAG_LEN: usize, | |
| F: Fn(&[u8; KEY_LEN]) -> Box<dyn Aead<Nonce = [u8; NONCE_LEN], Tag = [u8; TAG_LEN]>>, | |
| >( | |
| new_func: F, | |
| test_cases: &[TestCase<KEY_LEN, NONCE_LEN>], | |
| ) { | |
| for (test_num, test) in test_cases.iter().enumerate() { | |
| let ctx = new_func(&test.key); | |
| let ciphertext = ctx.seal(&test.nonce, test.msg.as_slice(), test.ad.as_slice()); | |
| assert_eq!(ciphertext, test.ciphertext, "Failed on test #{}", test_num); | |
| let plaintext = ctx | |
| .open(&test.nonce, ciphertext.as_slice(), test.ad.as_slice()) | |
| .unwrap(); | |
| assert_eq!(plaintext, test.msg, "Decrypt failed on test #{}", test_num); | |
| } | |
| } | |
| #[test] | |
| fn aes_128_gcm_siv() { | |
| let test_cases: &[TestCase<16, 12>] = &[ | |
| TestCase { | |
| // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json | |
| // TC1 - Empty Message | |
| key: decode_hex("01000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: Vec::new(), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec("dc20e2d83f25705bb49e439eca56de25"), | |
| }, | |
| TestCase { | |
| // TC2 | |
| key: decode_hex("01000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: decode_hex_into_vec("0100000000000000"), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec("b5d839330ac7b786578782fff6013b815b287c22493a364c"), | |
| }, | |
| TestCase { | |
| // TC14 | |
| key: decode_hex("01000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: decode_hex_into_vec("02000000"), | |
| ad: decode_hex_into_vec("010000000000000000000000"), | |
| ciphertext: decode_hex_into_vec("a8fe3e8707eb1f84fb28f8cb73de8e99e2f48a14"), | |
| }, | |
| ]; | |
| check_test_cases(|key| Box::new(Aes128GcmSiv::new(key)), test_cases); | |
| } | |
| #[test] | |
| fn aes_256_gcm_siv() { | |
| let test_cases: &[TestCase<32, 12>] = &[ | |
| TestCase { | |
| // https://github.com/google/wycheproof/blob/master/testvectors/aes_gcm_siv_test.json | |
| // TC77 | |
| key: decode_hex("0100000000000000000000000000000000000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: decode_hex_into_vec("0100000000000000"), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec("c2ef328e5c71c83b843122130f7364b761e0b97427e3df28"), | |
| }, | |
| TestCase { | |
| // TC78 | |
| key: decode_hex("0100000000000000000000000000000000000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: decode_hex_into_vec("010000000000000000000000"), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec( | |
| "9aab2aeb3faa0a34aea8e2b18ca50da9ae6559e48fd10f6e5c9ca17e", | |
| ), | |
| }, | |
| TestCase { | |
| // TC89 contains associated data | |
| key: decode_hex("0100000000000000000000000000000000000000000000000000000000000000"), | |
| nonce: decode_hex("030000000000000000000000"), | |
| msg: decode_hex_into_vec("02000000"), | |
| ad: decode_hex_into_vec("010000000000000000000000"), | |
| ciphertext: decode_hex_into_vec("22b3f4cd1835e517741dfddccfa07fa4661b74cf"), | |
| }, | |
| ]; | |
| check_test_cases(|key| Box::new(Aes256GcmSiv::new(key)), test_cases); | |
| } | |
| #[test] | |
| fn aes_128_gcm() { | |
| let test_cases: &[TestCase<16, 12>] = &[ | |
| TestCase { | |
| // TC 1 from crypto/cipher/test/aes_128_gcm_tests.txt | |
| key: decode_hex("d480429666d48b400633921c5407d1d1"), | |
| nonce: decode_hex("3388c676dc754acfa66e172a"), | |
| msg: Vec::new(), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec("7d7daf44850921a34e636b01adeb104f"), | |
| }, | |
| TestCase { | |
| // TC2 | |
| key: decode_hex("3881e7be1bb3bbcaff20bdb78e5d1b67"), | |
| nonce: decode_hex("dcf5b7ae2d7552e2297fcfa9"), | |
| msg: decode_hex_into_vec("0a2714aa7d"), | |
| ad: decode_hex_into_vec("c60c64bbf7"), | |
| ciphertext: decode_hex_into_vec("5626f96ecbff4c4f1d92b0abb1d0820833d9eb83c7"), | |
| }, | |
| ]; | |
| check_test_cases(|key| Box::new(Aes128Gcm::new(key)), test_cases); | |
| } | |
| #[test] | |
| fn aes_256_gcm() { | |
| let test_cases: &[TestCase<32, 12>] = &[ | |
| TestCase { | |
| // TC 1 from crypto/cipher/test/aes_128_gcm_tests.txt | |
| key: decode_hex("e5ac4a32c67e425ac4b143c83c6f161312a97d88d634afdf9f4da5bd35223f01"), | |
| nonce: decode_hex("5bf11a0951f0bfc7ea5c9e58"), | |
| msg: Vec::new(), | |
| ad: Vec::new(), | |
| ciphertext: decode_hex_into_vec("d7cba289d6d19a5af45dc13857016bac"), | |
| }, | |
| TestCase { | |
| // TC2 | |
| key: decode_hex("73ad7bbbbc640c845a150f67d058b279849370cd2c1f3c67c4dd6c869213e13a"), | |
| nonce: decode_hex("a330a184fc245812f4820caa"), | |
| msg: decode_hex_into_vec("f0535fe211"), | |
| ad: decode_hex_into_vec("e91428be04"), | |
| ciphertext: decode_hex_into_vec("e9b8a896da9115ed79f26a030c14947b3e454db9e7"), | |
| }, | |
| ]; | |
| check_test_cases(|key| Box::new(Aes256Gcm::new(key)), test_cases); | |
| } | |
| } |