// 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. //! Hybrid public key encryption. use bssl_crypto::hpke; use mls_rs_core::crypto::{ CipherSuite, HpkeCiphertext, HpkeContextR, HpkeContextS, HpkePublicKey, HpkeSecretKey, }; use mls_rs_core::error::{AnyError, IntoAnyError}; use mls_rs_crypto_traits::{DhType, KdfType, KemId, KemResult, KemType}; use std::sync::Mutex; use thiserror::Error; /// Errors returned from HPKE. #[derive(Debug, Error)] pub enum HpkeError { /// Error returned from BoringSSL. #[error("BoringSSL error")] BoringsslError, /// Error returned from Diffie-Hellman operations. #[error(transparent)] DhError(AnyError), /// Error returned from KDF operations. #[error(transparent)] KdfError(AnyError), /// Error returned when unsupported cipher suite is requested. #[error("unsupported cipher suite")] UnsupportedCipherSuite, } impl IntoAnyError for HpkeError { fn into_dyn_error(self) -> Result, Self> { Ok(self.into()) } } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct KdfWrapper { suite_id: Vec, kdf: KDF, } impl KdfWrapper { pub fn new(suite_id: Vec, kdf: KDF) -> Self { Self { suite_id, kdf } } // https://www.rfc-editor.org/rfc/rfc9180.html#section-4-9 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn labeled_extract( &self, salt: &[u8], label: &[u8], ikm: &[u8], ) -> Result, ::Error> { self.kdf.extract(salt, &[b"HPKE-v1" as &[u8], &self.suite_id, label, ikm].concat()).await } // https://www.rfc-editor.org/rfc/rfc9180.html#section-4-9 #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn labeled_expand( &self, key: &[u8], label: &[u8], info: &[u8], len: usize, ) -> Result, ::Error> { let labeled_info = [&(len as u16).to_be_bytes() as &[u8], b"HPKE-v1", &self.suite_id, label, info] .concat(); self.kdf.expand(key, &labeled_info, len).await } } /// KemType implementation backed by BoringSSL. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DhKem { dh: DH, kdf: KdfWrapper, kem_id: KemId, n_secret: usize, } impl DhKem { /// Creates a new DhKem. pub fn new(cipher_suite: CipherSuite, dh: DH, kdf: KDF) -> Option { // https://www.rfc-editor.org/rfc/rfc9180.html#section-4.1-5 let kem_id = KemId::new(cipher_suite)?; let suite_id = [b"KEM", &(kem_id as u16).to_be_bytes() as &[u8]].concat(); let kdf = KdfWrapper::new(suite_id, kdf); Some(Self { dh, kdf, kem_id, n_secret: kem_id.n_secret() }) } } #[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 KemType for DhKem { type Error = HpkeError; fn kem_id(&self) -> u16 { self.kem_id as u16 } async fn generate(&self) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> { if self.kem_id != KemId::DhKemX25519Sha256 { return Err(HpkeError::UnsupportedCipherSuite); } let kem = hpke::Kem::X25519HkdfSha256; let (public_key, private_key) = kem.generate_keypair(); Ok((private_key.to_vec().into(), public_key.to_vec().into())) } // https://www.rfc-editor.org/rfc/rfc9180.html#section-7.1.3-8 async fn derive(&self, ikm: &[u8]) -> Result<(HpkeSecretKey, HpkePublicKey), Self::Error> { let dkp_prk = match self.kdf.labeled_extract(&[], b"dkp_prk", ikm).await { Ok(p) => p, Err(e) => return Err(HpkeError::KdfError(e.into_any_error())), }; let sk = match self.kdf.labeled_expand(&dkp_prk, b"sk", &[], self.dh.secret_key_size()).await { Ok(s) => s.into(), Err(e) => return Err(HpkeError::KdfError(e.into_any_error())), }; let pk = match self.dh.to_public(&sk).await { Ok(p) => p, Err(e) => return Err(HpkeError::KdfError(e.into_any_error())), }; Ok((sk, pk)) } fn public_key_validate(&self, key: &HpkePublicKey) -> Result<(), Self::Error> { match self.dh.public_key_validate(key) { Ok(_) => Ok(()), Err(e) => Err(HpkeError::DhError(e.into_any_error())), } } // Using BoringSSL's HPKE implementation so this is not needed. async fn encap(&self, _remote_pk: &HpkePublicKey) -> Result { unimplemented!(); } // Using BoringSSL's HPKE implementation so this is not needed. async fn decap( &self, _enc: &[u8], _secret_key: &HpkeSecretKey, _public_key: &HpkePublicKey, ) -> Result, Self::Error> { unimplemented!(); } } /// HpkeContextS implementation backed by BoringSSL. pub struct ContextS(pub Mutex); #[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 HpkeContextS for ContextS { type Error = HpkeError; #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn seal(&mut self, aad: Option<&[u8]>, data: &[u8]) -> Result, Self::Error> { Ok(self.0.lock().unwrap().seal(data, aad.unwrap_or_default())) } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn export(&self, exporter_context: &[u8], len: usize) -> Result, Self::Error> { Ok(self.0.lock().unwrap().export(exporter_context, len).to_vec()) } } /// HpkeContextR implementation backed by BoringSSL. pub struct ContextR(pub Mutex); #[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 HpkeContextR for ContextR { type Error = HpkeError; #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn open( &mut self, aad: Option<&[u8]>, ciphertext: &[u8], ) -> Result, Self::Error> { self.0 .lock() .unwrap() .open(ciphertext, aad.unwrap_or_default()) .ok_or(HpkeError::BoringsslError) } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn export(&self, exporter_context: &[u8], len: usize) -> Result, Self::Error> { Ok(self.0.lock().unwrap().export(exporter_context, len).to_vec()) } } /// HPKE implementation backed by BoringSSL. #[derive(Clone)] pub struct Hpke(pub CipherSuite); impl Hpke { /// Creates a new Hpke. pub fn new(cipher_suite: CipherSuite) -> Self { Self(cipher_suite) } /// Sets up HPKE sender context. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn setup_sender( &self, remote_key: &HpkePublicKey, info: &[u8], ) -> Result<(Vec, ContextS), HpkeError> { let params = Self::cipher_suite_to_params(self.0)?; match hpke::SenderContext::new(¶ms, remote_key, info) { Some((ctx, encapsulated_key)) => Ok((encapsulated_key, ContextS(ctx.into()))), None => Err(HpkeError::BoringsslError), } } /// Sets up HPKE sender context and encrypts `pt` with optional associated data `aad`. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn seal( &self, remote_key: &HpkePublicKey, info: &[u8], aad: Option<&[u8]>, pt: &[u8], ) -> Result { let (kem_output, mut ctx) = self.setup_sender(remote_key, info).await?; Ok(HpkeCiphertext { kem_output, ciphertext: ctx.seal(aad, pt).await? }) } /// Sets up HPKE receiver context. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn setup_receiver( &self, enc: &[u8], local_secret: &HpkeSecretKey, info: &[u8], ) -> Result { let params = Self::cipher_suite_to_params(self.0)?; match hpke::RecipientContext::new(¶ms, local_secret, enc, info) { Some(ctx) => Ok(ContextR(ctx.into())), None => Err(HpkeError::BoringsslError), } } /// Sets up HPKE receiver context and decrypts `ciphertext` with optional associated data `aad`. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn open( &self, ciphertext: &HpkeCiphertext, local_secret: &HpkeSecretKey, info: &[u8], aad: Option<&[u8]>, ) -> Result, HpkeError> { let mut ctx = self.setup_receiver(&ciphertext.kem_output, local_secret, info).await?; ctx.open(aad, &ciphertext.ciphertext).await } fn cipher_suite_to_params(cipher_suite: CipherSuite) -> Result { match cipher_suite { CipherSuite::CURVE25519_AES128 => Ok(hpke::Params::new( hpke::Kem::X25519HkdfSha256, hpke::Kdf::HkdfSha256, hpke::Aead::Aes128Gcm, )), CipherSuite::CURVE25519_CHACHA => Ok(hpke::Params::new( hpke::Kem::X25519HkdfSha256, hpke::Kdf::HkdfSha256, hpke::Aead::Chacha20Poly1305, )), _ => Err(HpkeError::UnsupportedCipherSuite), } } } #[cfg(all(not(mls_build_async), test))] mod test { use super::{DhKem, Hpke, KdfWrapper}; use crate::ecdh::Ecdh; use crate::kdf::Kdf; use crate::test_helpers::decode_hex; use mls_rs_core::crypto::{ CipherSuite, HpkeContextR, HpkeContextS, HpkePublicKey, HpkeSecretKey, }; use mls_rs_crypto_traits::{AeadId, KdfId, KemId, KemType}; use std::thread; // https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1-8 fn hpke_suite_id(cipher_suite: CipherSuite) -> Vec { [ b"HPKE", &(KemId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8], &(KdfId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8], &(AeadId::new(cipher_suite).unwrap() as u16).to_be_bytes() as &[u8], ] .concat() } #[test] fn kdf_labeled_extract() { let cipher_suite = CipherSuite::CURVE25519_AES128; let suite_id = hpke_suite_id(cipher_suite); let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap()); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let shared_secret: [u8; 32] = decode_hex("fe0e18c9f024ce43799ae393c7e8fe8fce9d218875e8227b0187c04e7d2ea1fc"); let expected_secret: [u8; 32] = decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397"); let label = b"secret"; let secret = kdf.labeled_extract(&shared_secret, label, &[]).unwrap(); assert_eq!(secret, expected_secret); } #[test] fn kdf_labeled_expand() { let cipher_suite = CipherSuite::CURVE25519_AES128; let suite_id = hpke_suite_id(cipher_suite); let kdf = KdfWrapper::new(suite_id, Kdf::new(cipher_suite).unwrap()); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let secret: [u8; 32] = decode_hex("12fff91991e93b48de37e7daddb52981084bd8aa64289c3788471d9a9712f397"); let key_schedule_ctx : [u8; 65] = decode_hex("00725611c9d98c07c03f60095cd32d400d8347d45ed67097bbad50fc56da742d07cb6cffde367bb0565ba28bb02c90744a20f5ef37f30523526106f637abb05449"); let expected_key: [u8; 16] = decode_hex("4531685d41d65f03dc48f6b8302c05b0"); let label = b"key"; let key = kdf.labeled_expand(&secret, label, &key_schedule_ctx, 16).unwrap(); assert_eq!(key, expected_key); } #[test] fn dh_kem_kem_id() { let cipher_suite = CipherSuite::CURVE25519_CHACHA; let dh = Ecdh::new(cipher_suite).unwrap(); let kdf = Kdf::new(cipher_suite).unwrap(); let kem = DhKem::new(cipher_suite, dh, kdf).unwrap(); assert_eq!(kem.kem_id(), 32); } #[test] fn dh_kem_generate() { let cipher_suite = CipherSuite::CURVE25519_AES128; let dh = Ecdh::new(cipher_suite).unwrap(); let kdf = Kdf::new(cipher_suite).unwrap(); let kem = DhKem::new(cipher_suite, dh, kdf).unwrap(); assert!(kem.generate().is_ok()); } #[test] fn dh_kem_derive() { let cipher_suite = CipherSuite::CURVE25519_CHACHA; let dh = Ecdh::new(cipher_suite).unwrap(); let kdf = Kdf::new(cipher_suite).unwrap(); let kem = DhKem::new(cipher_suite, dh, kdf).unwrap(); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.2.1 let ikm: [u8; 32] = decode_hex("909a9b35d3dc4713a5e72a4da274b55d3d3821a37e5d099e74a647db583a904b"); // ikmE let expected_sk = HpkeSecretKey::from( decode_hex::<32>("f4ec9b33b792c372c1d2c2063507b684ef925b8c75a42dbcbf57d63ccd381600") .to_vec(), ); // skEm let expected_pk = HpkePublicKey::from( decode_hex::<32>("1afa08d3dec047a643885163f1180476fa7ddb54c6a8029ea33f95796bf2ac4a") .to_vec(), ); // pkEm let (sk, pk) = kem.derive(&ikm).unwrap(); assert_eq!(sk, expected_sk); assert_eq!(pk, expected_pk); } #[test] fn dh_kem_public_key_validate() { let cipher_suite = CipherSuite::CURVE25519_AES128; let dh = Ecdh::new(cipher_suite).unwrap(); let kdf = Kdf::new(cipher_suite).unwrap(); let kem = DhKem::new(cipher_suite, dh, kdf).unwrap(); // https://www.rfc-editor.org/rfc/rfc7748.html#section-6.1 let public_key = HpkePublicKey::from( decode_hex::<32>("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a") .to_vec(), ); assert!(kem.public_key_validate(&public_key).is_ok()); } #[test] fn hpke_seal_open() { let hpke = Hpke::new(CipherSuite::CURVE25519_AES128); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let receiver_pub_key = HpkePublicKey::from( decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") .to_vec(), ); let receiver_priv_key = HpkeSecretKey::from( decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8") .to_vec(), ); let info = b"some_info"; let plaintext = b"plaintext"; let associated_data = b"some_ad"; let ct = hpke.seal(&receiver_pub_key, info, Some(associated_data), plaintext).unwrap(); assert_eq!( plaintext.as_ref(), hpke.open(&ct, &receiver_priv_key, info, Some(associated_data)).unwrap(), ); } #[test] fn hpke_context_seal_open() { let hpke = Hpke::new(CipherSuite::CURVE25519_AES128); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let receiver_pub_key = HpkePublicKey::from( decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") .to_vec(), ); let receiver_priv_key = HpkeSecretKey::from( decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8") .to_vec(), ); let info = b"some_info"; let plaintext = b"plaintext"; let associated_data = b"some_ad"; let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap(); let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap(); let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap(); assert_eq!(plaintext.as_ref(), receiver_ctx.open(Some(associated_data), &ct).unwrap(),); } #[test] fn hpke_context_seal_open_multithreaded() { let hpke = Hpke::new(CipherSuite::CURVE25519_AES128); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let receiver_pub_key = HpkePublicKey::from( decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") .to_vec(), ); let receiver_priv_key = HpkeSecretKey::from( decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8") .to_vec(), ); let info = b"some_info"; let plaintext = b"plaintext"; let associated_data = b"some_ad"; let (enc, mut sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap(); let mut receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap(); let pool = thread::spawn(move || { for _ in 1..100 { let ct = sender_ctx.seal(Some(associated_data), plaintext).unwrap(); assert_eq!( plaintext.as_ref(), receiver_ctx.open(Some(associated_data), &ct).unwrap(), ); } }); pool.join().unwrap(); } #[test] fn hpke_context_export() { let hpke = Hpke::new(CipherSuite::CURVE25519_AES128); // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let receiver_pub_key = HpkePublicKey::from( decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") .to_vec(), ); let receiver_priv_key = HpkeSecretKey::from( decode_hex::<32>("4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8") .to_vec(), ); let info = b"some_info"; let exporter_ctx = b"export_ctx"; let (enc, sender_ctx) = hpke.setup_sender(&receiver_pub_key, info).unwrap(); let receiver_ctx = hpke.setup_receiver(&enc, &receiver_priv_key, info).unwrap(); assert_eq!( sender_ctx.export(exporter_ctx, 32).unwrap(), receiver_ctx.export(exporter_ctx, 32).unwrap(), ); } #[test] fn hpke_unsupported_cipher_suites() { // https://www.rfc-editor.org/rfc/rfc9180.html#appendix-A.1.1 let receiver_pub_key = HpkePublicKey::from( decode_hex::<32>("3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d") .to_vec(), ); for suite in vec![ CipherSuite::P256_AES128, CipherSuite::P384_AES256, CipherSuite::P521_AES256, CipherSuite::CURVE448_CHACHA, CipherSuite::CURVE448_AES256, ] { assert!(Hpke::new(suite).setup_sender(&receiver_pub_key, b"some_info").is_err()); } } }