Implement mls-rs-crypto-traits backed by BoringSSL.

Fix: 302021139
Test: Presubmit
Change-Id: Iaefa21d3fb69f92d735875778f3f96e1878d0876
diff --git a/mls/mls-rs-crypto-boringssl/src/aead.rs b/mls/mls-rs-crypto-boringssl/src/aead.rs
new file mode 100644
index 0000000..eaa33a9
--- /dev/null
+++ b/mls/mls-rs-crypto-boringssl/src/aead.rs
@@ -0,0 +1,334 @@
+// 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<Box<dyn std::error::Error + Send + Sync>, 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<Self> {
+        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<Vec<u8>, 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<Vec<u8>, 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<AeadWrapper> {
+        [
+            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(),
+            );
+        }
+    }
+}