From 863249af4a380c0959fec827ac8b01d0ad3b5a7f Mon Sep 17 00:00:00 2001 From: vaf Date: Thu, 9 Jan 2025 17:22:45 +0100 Subject: [PATCH] expose hmac and compatibility test it in the tuta-sdk TODO: make decisions about the somewhat redundant concats... --- tuta-sdk/rust/sdk/src/crypto.rs | 1 + tuta-sdk/rust/sdk/src/crypto/aes.rs | 52 +++++++++++-------- .../src/crypto/compatibility_test_utils.rs | 12 +++++ tuta-sdk/rust/sdk/src/crypto/hmac.rs | 41 +++++++++++++++ 4 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 tuta-sdk/rust/sdk/src/crypto/hmac.rs diff --git a/tuta-sdk/rust/sdk/src/crypto.rs b/tuta-sdk/rust/sdk/src/crypto.rs index 8499aa787655..48ea859eb6a5 100644 --- a/tuta-sdk/rust/sdk/src/crypto.rs +++ b/tuta-sdk/rust/sdk/src/crypto.rs @@ -14,6 +14,7 @@ pub use sha::sha256; pub use tuta_crypt::PQKeyPairs; pub mod aes; +pub mod hmac; mod sha; diff --git a/tuta-sdk/rust/sdk/src/crypto/aes.rs b/tuta-sdk/rust/sdk/src/crypto/aes.rs index 7559e2cf4d13..278dca00a1e8 100644 --- a/tuta-sdk/rust/sdk/src/crypto/aes.rs +++ b/tuta-sdk/rust/sdk/src/crypto/aes.rs @@ -1,5 +1,6 @@ //! Contains code to handle AES128/AES256 encryption and decryption +use crate::crypto::hmac::{HmacError, MAC_SIZE}; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::join_slices; use crate::util::{array_cast_size, array_cast_slice, ArrayCastingError}; @@ -231,7 +232,7 @@ pub enum AesDecryptError { #[error("PaddingError")] PaddingError(#[from] UnpadError), #[error("HmacError")] - HmacError, + HmacError(#[from] HmacError), } /// Result of decryption operation. @@ -302,9 +303,6 @@ pub const AES_256_KEY_SIZE: usize = 32; /// The size of an AES initialisation vector in bytes pub const IV_BYTE_SIZE: usize = 16; -/// Size of HMAC authentication added to the ciphertext -const MAC_SIZE: usize = 32; - /// Encrypts a plaintext without adding padding and returns the encrypted text as a vector fn encrypt_unpadded_vec_mut( encryptor: &mut cbc::Encryptor, @@ -333,13 +331,24 @@ type Aes256SubKeys = AesSubKeys; impl AesSubKeys { fn compute_mac(&self, iv: &[u8], ciphertext: &[u8]) -> [u8; MAC_SIZE] { - use hmac::Mac; - use sha2::Sha256; - - let mut hmac = hmac::Hmac::::new_from_slice(self.m_key.get_bytes()).unwrap(); - hmac.update(iv); - hmac.update(ciphertext); - hmac.finalize().into_bytes().into() + use crate::crypto::hmac::hmac_sha256; + // TODO get rid of the concat? + hmac_sha256(self.m_key.get_bytes(), [iv, ciphertext].concat().as_slice()) + } + + fn verify_mac( + &self, + iv: &[u8], + ciphertext: &[u8], + tag: [u8; MAC_SIZE], + ) -> Result<(), HmacError> { + use crate::crypto::hmac::verify_hmac_sha256; + verify_hmac_sha256( + self.m_key.get_bytes(), + // TODO get rid of the concat? + [iv, ciphertext].concat().as_slice(), + tag, + ) } } @@ -431,7 +440,7 @@ impl<'a> CiphertextWithAuthentication<'a> { // Incorrect size for Hmac if bytes.len() <= IV_BYTE_SIZE + MAC_SIZE { - return Err(AesDecryptError::HmacError); + return Err(AesDecryptError::HmacError(HmacError)); } // Split `bytes` into the MAC and combined ciphertext with iv @@ -442,8 +451,8 @@ impl<'a> CiphertextWithAuthentication<'a> { // Extract the iv from the ciphertext and return the extracted components let (iv, ciphertext) = ciphertext_without_mac.split_at(IV_BYTE_SIZE); - let mac: [u8; MAC_SIZE] = - array_cast_slice(provided_mac_bytes, "MAC").map_err(|_| AesDecryptError::HmacError)?; + let mac: [u8; MAC_SIZE] = array_cast_slice(provided_mac_bytes, "MAC") + .map_err(|_| AesDecryptError::HmacError(HmacError))?; Ok(Some(CiphertextWithAuthentication { iv, ciphertext, @@ -463,10 +472,6 @@ impl<'a> CiphertextWithAuthentication<'a> { } } - fn matches(&self, subkeys: &AesSubKeys) -> bool { - self.mac == subkeys.compute_mac(self.iv, self.ciphertext) - } - fn serialize(&self) -> Vec { // - marker that HMAC is there (a single byte with "1" in the front, this makes the length // un-even) @@ -499,9 +504,12 @@ fn aes_decrypt( let (key, iv_bytes, encrypted_bytes) = if let Some(ciphertext_with_auth) = CiphertextWithAuthentication::parse(encrypted_bytes)? { let subkeys = key.derive_subkeys(); - if !ciphertext_with_auth.matches(&subkeys) { - return Err(AesDecryptError::HmacError); - } + + subkeys.verify_mac( + ciphertext_with_auth.iv, + ciphertext_with_auth.ciphertext, + ciphertext_with_auth.mac, + )?; ( subkeys.c_key, @@ -509,7 +517,7 @@ fn aes_decrypt( ciphertext_with_auth.ciphertext, ) } else if enforce_mac == EnforceMac::EnforceMac { - return Err(AesDecryptError::HmacError); + return Err(AesDecryptError::HmacError(HmacError)); } else { // Separate and check both the initialisation vector let (iv_bytes, cipher_text) = encrypted_bytes.split_at(IV_BYTE_SIZE); diff --git a/tuta-sdk/rust/sdk/src/crypto/compatibility_test_utils.rs b/tuta-sdk/rust/sdk/src/crypto/compatibility_test_utils.rs index 82159fe5c2e7..e43f842c9df0 100644 --- a/tuta-sdk/rust/sdk/src/crypto/compatibility_test_utils.rs +++ b/tuta-sdk/rust/sdk/src/crypto/compatibility_test_utils.rs @@ -49,6 +49,17 @@ pub struct HkdfTest { pub length_in_bytes: usize, } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HmacTest { + #[serde(with = "const_hex")] + pub key_hex: Vec, + #[serde(with = "const_hex")] + pub data_hex: Vec, + #[serde(with = "const_hex")] + pub hmac_sha256_tag_hex: Vec, +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct Argon2Test { @@ -152,6 +163,7 @@ pub struct CompatibilityTestData { pub aes128_mac_tests: Vec, pub aes256_tests: Vec, pub hkdf_tests: Vec, + pub hmac_sha256_tests: Vec, pub argon2id_tests: Vec, pub x25519_tests: Vec, pub kyber_encryption_tests: Vec, diff --git a/tuta-sdk/rust/sdk/src/crypto/hmac.rs b/tuta-sdk/rust/sdk/src/crypto/hmac.rs new file mode 100644 index 000000000000..53d4612ff7f5 --- /dev/null +++ b/tuta-sdk/rust/sdk/src/crypto/hmac.rs @@ -0,0 +1,41 @@ +use hmac::Mac; +use sha2::Sha256; + +/// Size of HMAC authentication added to the ciphertext +pub const MAC_SIZE: usize = 32; + +#[derive(thiserror::Error, Debug)] +#[error("HmacError")] +pub struct HmacError; + +#[must_use] +pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; MAC_SIZE] { + let mut hmac = hmac::Hmac::::new_from_slice(key).unwrap(); + hmac.update(data); + hmac.finalize().into_bytes().into() +} + +pub fn verify_hmac_sha256(key: &[u8], data: &[u8], tag: [u8; MAC_SIZE]) -> Result<(), HmacError> { + if tag != hmac_sha256(key, data) { + Err(HmacError) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::crypto::compatibility_test_utils::get_compatibility_test_data; + use crate::crypto::hmac::verify_hmac_sha256; + use crate::crypto::hmac::{hmac_sha256, MAC_SIZE}; + + #[test] + fn compatibility_test() { + for td in get_compatibility_test_data().hmac_sha256_tests { + let result = hmac_sha256(&td.key_hex, &td.data_hex); + assert_eq!(td.hmac_sha256_tag_hex, result); + let tag: [u8; MAC_SIZE] = td.hmac_sha256_tag_hex.as_slice().try_into().unwrap(); + verify_hmac_sha256(&td.key_hex, &td.data_hex, tag).unwrap(); + } + } +}