// Copyright 2024, The Android Open Source Project // // 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 // // http://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. use bssl_crypto::aead::{Aead, Aes128Gcm, Aes256Gcm, Chacha20Poly1305}; use mls_rs_core::crypto::CipherSuite; use mls_rs_core::error::IntoAnyError; use mls_rs_crypto_traits::{AeadId, AeadType, AES_TAG_LEN}; use core::array::TryFromSliceError; use thiserror::Error; /// Errors returned from AEAD. #[derive(Debug, Error)] pub enum AeadError { /// Error returned when conversion from slice to array fails. #[error(transparent)] TryFromSliceError(#[from] TryFromSliceError), /// Error returned when the ciphertext is invalid. #[error("AEAD ciphertext was invalid")] InvalidCiphertext, /// Error returned when the ciphertext length is too short. #[error("AEAD ciphertext of length {len}, expected length at least {min_len}")] TooShortCiphertext { /// Invalid ciphertext length. len: usize, /// Minimum ciphertext length. min_len: usize, }, /// Error returned when the plaintext is empty. #[error("message cannot be empty")] EmptyPlaintext, /// Error returned when the key length is invalid. #[error("AEAD key of invalid length {len}, expected length {expected_len}")] InvalidKeyLen { /// Invalid key length. len: usize, /// Expected key length. expected_len: usize, }, /// Error returned when the nonce size is invalid. #[error("AEAD nonce of invalid length {len}, expected length {expected_len}")] InvalidNonceLen { /// Invalid nonce length. len: usize, /// Expected nonce length. expected_len: usize, }, /// Error returned when unsupported cipher suite is requested. #[error("unsupported cipher suite")] UnsupportedCipherSuite, } impl IntoAnyError for AeadError { fn into_dyn_error(self) -> Result, Self> { Ok(self.into()) } } /// AeadType implementation backed by BoringSSL. #[derive(Clone)] pub struct AeadWrapper(AeadId); impl AeadWrapper { /// Creates a new AeadWrapper. pub fn new(cipher_suite: CipherSuite) -> Option { AeadId::new(cipher_suite).map(Self) } } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] #[cfg_attr(all(target_arch = "wasm32", mls_build_async), maybe_async::must_be_async(?Send))] #[cfg_attr(all(not(target_arch = "wasm32"), mls_build_async), maybe_async::must_be_async)] impl AeadType for AeadWrapper { type Error = AeadError; async fn seal<'a>( &self, key: &[u8], data: &[u8], aad: Option<&'a [u8]>, nonce: &[u8], ) -> Result, AeadError> { if data.is_empty() { return Err(AeadError::EmptyPlaintext); } if key.len() != self.key_size() { return Err(AeadError::InvalidKeyLen { len: key.len(), expected_len: self.key_size() }); } if nonce.len() != self.nonce_size() { return Err(AeadError::InvalidNonceLen { len: nonce.len(), expected_len: self.nonce_size(), }); } let nonce_array = nonce[..self.nonce_size()].try_into()?; match self.0 { AeadId::Aes128Gcm => { let cipher = Aes128Gcm::new(key[..self.key_size()].try_into()?); Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default())) } AeadId::Aes256Gcm => { let cipher = Aes256Gcm::new(key[..self.key_size()].try_into()?); Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default())) } AeadId::Chacha20Poly1305 => { let cipher = Chacha20Poly1305::new(key[..self.key_size()].try_into()?); Ok(cipher.seal(nonce_array, data, aad.unwrap_or_default())) } _ => Err(AeadError::UnsupportedCipherSuite), } } async fn open<'a>( &self, key: &[u8], ciphertext: &[u8], aad: Option<&'a [u8]>, nonce: &[u8], ) -> Result, AeadError> { if ciphertext.len() < AES_TAG_LEN { return Err(AeadError::TooShortCiphertext { len: ciphertext.len(), min_len: AES_TAG_LEN, }); } if key.len() != self.key_size() { return Err(AeadError::InvalidKeyLen { len: key.len(), expected_len: self.key_size() }); } if nonce.len() != self.nonce_size() { return Err(AeadError::InvalidNonceLen { len: nonce.len(), expected_len: self.nonce_size(), }); } let nonce_array = nonce[..self.nonce_size()].try_into()?; match self.0 { AeadId::Aes128Gcm => { let cipher = Aes128Gcm::new(key[..self.key_size()].try_into()?); cipher .open(nonce_array, ciphertext, aad.unwrap_or_default()) .ok_or(AeadError::InvalidCiphertext) } AeadId::Aes256Gcm => { let cipher = Aes256Gcm::new(key[..self.key_size()].try_into()?); cipher .open(nonce_array, ciphertext, aad.unwrap_or_default()) .ok_or(AeadError::InvalidCiphertext) } AeadId::Chacha20Poly1305 => { let cipher = Chacha20Poly1305::new(key[..self.key_size()].try_into()?); cipher .open(nonce_array, ciphertext, aad.unwrap_or_default()) .ok_or(AeadError::InvalidCiphertext) } _ => Err(AeadError::UnsupportedCipherSuite), } } #[inline(always)] fn key_size(&self) -> usize { self.0.key_size() } fn nonce_size(&self) -> usize { self.0.nonce_size() } fn aead_id(&self) -> u16 { self.0 as u16 } } #[cfg(all(not(mls_build_async), test))] mod test { use super::{AeadError, AeadWrapper}; use assert_matches::assert_matches; use mls_rs_core::crypto::CipherSuite; use mls_rs_crypto_traits::{AeadType, AES_TAG_LEN}; fn get_aeads() -> Vec { [ CipherSuite::CURVE25519_AES128, CipherSuite::CURVE25519_CHACHA, CipherSuite::CURVE448_AES256, ] .into_iter() .map(|suite| AeadWrapper::new(suite).unwrap()) .collect() } #[test] fn seal_and_open() { for aead in get_aeads() { let key = vec![42u8; aead.key_size()]; let nonce = vec![42u8; aead.nonce_size()]; let plaintext = b"message"; let ciphertext = aead.seal(&key, plaintext, None, &nonce).unwrap(); assert_eq!( plaintext, aead.open(&key, ciphertext.as_slice(), None, &nonce).unwrap().as_slice(), "open failed for AEAD with ID {}", aead.aead_id(), ); } } #[test] fn seal_and_open_with_invalid_key() { for aead in get_aeads() { let data = b"top secret data that's long enough"; let nonce = vec![42u8; aead.nonce_size()]; let key_short = vec![42u8; aead.key_size() - 1]; assert_matches!( aead.seal(&key_short, data, None, &nonce), Err(AeadError::InvalidKeyLen { .. }), "seal with short key should fail for AEAD with ID {}", aead.aead_id(), ); assert_matches!( aead.open(&key_short, data, None, &nonce), Err(AeadError::InvalidKeyLen { .. }), "open with short key should fail for AEAD with ID {}", aead.aead_id(), ); let key_long = vec![42u8; aead.key_size() + 1]; assert_matches!( aead.seal(&key_long, data, None, &nonce), Err(AeadError::InvalidKeyLen { .. }), "seal with long key should fail for AEAD with ID {}", aead.aead_id(), ); assert_matches!( aead.open(&key_long, data, None, &nonce), Err(AeadError::InvalidKeyLen { .. }), "open with long key should fail for AEAD with ID {}", aead.aead_id(), ); } } #[test] fn invalid_ciphertext() { for aead in get_aeads() { let key = vec![42u8; aead.key_size()]; let nonce = vec![42u8; aead.nonce_size()]; let ciphertext_short = [0u8; AES_TAG_LEN - 1]; assert_matches!( aead.open(&key, &ciphertext_short, None, &nonce), Err(AeadError::TooShortCiphertext { .. }), "open with short ciphertext should fail for AEAD with ID {}", aead.aead_id(), ); } } #[test] fn associated_data_mismatch() { for aead in get_aeads() { let key = vec![42u8; aead.key_size()]; let nonce = vec![42u8; aead.nonce_size()]; let ciphertext = aead.seal(&key, b"message", Some(b"foo"), &nonce).unwrap(); assert_matches!( aead.open(&key, &ciphertext, Some(b"bar"), &nonce), Err(AeadError::InvalidCiphertext), "open with incorrect associated data should fail for AEAD with ID {}", aead.aead_id(), ); assert_matches!( aead.open(&key, &ciphertext, None, &nonce), Err(AeadError::InvalidCiphertext), "open with incorrect associated data should fail for AEAD with ID {}", aead.aead_id(), ); } } #[test] fn invalid_nonce() { for aead in get_aeads() { let key = vec![42u8; aead.key_size()]; let data = b"top secret data that's long enough"; let nonce_short = vec![42u8; aead.nonce_size() - 1]; assert_matches!( aead.seal(&key, data, None, &nonce_short), Err(AeadError::InvalidNonceLen { .. }), "seal with short nonce should fail for AEAD with ID {}", aead.aead_id(), ); assert_matches!( aead.open(&key, data, None, &nonce_short), Err(AeadError::InvalidNonceLen { .. }), "open with short nonce should fail for AEAD with ID {}", aead.aead_id(), ); let nonce_long = vec![42u8; aead.nonce_size() + 1]; assert_matches!( aead.seal(&key, data, None, &nonce_long), Err(AeadError::InvalidNonceLen { .. }), "seal with long nonce should fail for AEAD with ID {}", aead.aead_id(), ); assert_matches!( aead.open(&key, data, None, &nonce_long), Err(AeadError::InvalidNonceLen { .. }), "open with long nonce should fail for AEAD with ID {}", aead.aead_id(), ); } } }