From 053540cd6acd2a79299e7f6f4f986b802e7b02c0 Mon Sep 17 00:00:00 2001 From: wec Date: Wed, 19 Jun 2024 17:24:41 +0200 Subject: [PATCH 1/8] [SDK] Implement KeyCache and KeyLoaderFacade Co-authored-by: paw Co-authored-by: jat --- tuta-sdk/rust/Cargo.lock | 105 +++++++++++ tuta-sdk/rust/Cargo.toml | 1 + tuta-sdk/rust/src/crypto/aes.rs | 9 +- tuta-sdk/rust/src/crypto/crypto_facade.rs | 42 +++-- tuta-sdk/rust/src/crypto/key.rs | 28 ++- tuta-sdk/rust/src/crypto/key_encryption.rs | 83 +++++++++ tuta-sdk/rust/src/crypto/key_loader_facade.rs | 27 --- tuta-sdk/rust/src/crypto/mod.rs | 13 +- tuta-sdk/rust/src/crypto/rsa.rs | 44 ++++- tuta-sdk/rust/src/crypto_entity_client.rs | 1 + tuta-sdk/rust/src/entity_client.rs | 44 ++++- tuta-sdk/rust/src/generated_id.rs | 2 +- tuta-sdk/rust/src/key_cache.rs | 58 ++++++ tuta-sdk/rust/src/key_loader_facade.rs | 165 ++++++++++++++++++ tuta-sdk/rust/src/lib.rs | 12 +- tuta-sdk/rust/src/rest_client.rs | 3 +- tuta-sdk/rust/src/typed_entity_client.rs | 13 ++ tuta-sdk/rust/src/user_facade.rs | 138 +++++++++++++-- tuta-sdk/rust/src/util/mod.rs | 20 +++ tuta-sdk/rust/test_data/group_response.json | 22 ++- tuta-sdk/rust/test_data/user_response.json | 59 +++++++ 21 files changed, 797 insertions(+), 92 deletions(-) create mode 100644 tuta-sdk/rust/src/crypto/key_encryption.rs delete mode 100644 tuta-sdk/rust/src/crypto/key_loader_facade.rs create mode 100644 tuta-sdk/rust/src/key_cache.rs create mode 100644 tuta-sdk/rust/src/key_loader_facade.rs create mode 100644 tuta-sdk/rust/test_data/user_response.json diff --git a/tuta-sdk/rust/Cargo.lock b/tuta-sdk/rust/Cargo.lock index fc8e6b819f8..0f72bf6817a 100644 --- a/tuta-sdk/rust/Cargo.lock +++ b/tuta-sdk/rust/Cargo.lock @@ -538,6 +538,95 @@ dependencies = [ "autocfg", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -926,6 +1015,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs1" version = "0.7.5" @@ -1271,6 +1366,15 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1439,6 +1543,7 @@ dependencies = [ "cbc", "const-hex", "curve25519-dalek", + "futures", "hkdf", "hmac", "log 0.4.22", diff --git a/tuta-sdk/rust/Cargo.toml b/tuta-sdk/rust/Cargo.toml index 112fbe62b55..82a8bf9e29a 100644 --- a/tuta-sdk/rust/Cargo.toml +++ b/tuta-sdk/rust/Cargo.toml @@ -27,6 +27,7 @@ pqcrypto-traits = "0.3.4" rsa = "0.9.6" rand_core = "0.6.4" serde_bytes = "0.11.14" +futures = "0.3.30" mockall_double = "0.3.1" log = "0.4.22" simple_logger = "5.0.0" diff --git a/tuta-sdk/rust/src/crypto/aes.rs b/tuta-sdk/rust/src/crypto/aes.rs index 0ab488fedec..96f9e46eef1 100644 --- a/tuta-sdk/rust/src/crypto/aes.rs +++ b/tuta-sdk/rust/src/crypto/aes.rs @@ -312,8 +312,13 @@ struct CiphertextWithAuthentication<'a> { impl<'a> CiphertextWithAuthentication<'a> { fn parse(bytes: &'a [u8]) -> Result>, AesDecryptError> { - // Error if the bytes does not feature a MAC - if !has_mac(bytes) || bytes.len() <= IV_BYTE_SIZE + MAC_SIZE { + // No MAC + if !has_mac(bytes) { + return Ok(None); + } + + // Incorrect size for Hmac + if bytes.len() <= IV_BYTE_SIZE + MAC_SIZE { return Err(AesDecryptError::HmacError); } diff --git a/tuta-sdk/rust/src/crypto/crypto_facade.rs b/tuta-sdk/rust/src/crypto/crypto_facade.rs index ab7cc89ad97..337fa39fe1c 100644 --- a/tuta-sdk/rust/src/crypto/crypto_facade.rs +++ b/tuta-sdk/rust/src/crypto/crypto_facade.rs @@ -4,8 +4,8 @@ use crate::crypto::aes::Iv; use crate::crypto::ecc::EccPublicKey; use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; #[mockall_double::double] -use crate::crypto::key_loader_facade::KeyLoaderFacade; -use crate::crypto::key_loader_facade::VersionedAesKey; +use crate::key_loader_facade::KeyLoaderFacade; +use crate::key_loader_facade::VersionedAesKey; use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::rsa::RSAEncryptionError; use crate::crypto::tuta_crypt::{PQError, PQMessage}; @@ -42,14 +42,14 @@ pub struct CryptoFacade { impl CryptoFacade { /// Returns the session key from `entity` and resolves the bucket key fields contained inside /// if present - pub fn resolve_session_key(&self, entity: &mut ParsedEntity, model: &TypeModel) -> Result, SessionKeyResolutionError> { + pub async fn resolve_session_key(&self, entity: &mut ParsedEntity, model: &TypeModel) -> Result, SessionKeyResolutionError> { if !model.encrypted { return Ok(None); } // Derive the session key from the bucket key if entity.contains_key(BUCKET_KEY_FIELD) { - let resolved_key = self.resolve_bucket_key(entity, model)?; + let resolved_key = self.resolve_bucket_key(entity, model).await?; return Ok(Some(resolved_key)); } @@ -62,12 +62,13 @@ impl CryptoFacade { return Err(SessionKeyResolutionError { reason: "instance missing owner key/group data".to_string() }); }; - let group_key = self.key_loader_facade.get_group_key(owner_group, owner_key_version)?; - Ok(group_key.decrypt_key(owner_enc_session_key).map(|k| Some(k))?) + let group_key: GenericAesKey = self.key_loader_facade.load_sym_group_key(owner_group, owner_key_version, None).await?; + + Ok(group_key.decrypt_aes_key(owner_enc_session_key).map(|k| Some(k))?) } /// Resolves the bucket key fields inside `entity` and returns the session key - fn resolve_bucket_key(&self, entity: &mut ParsedEntity, model: &TypeModel) -> Result { + async fn resolve_bucket_key(&self, entity: &mut ParsedEntity, model: &TypeModel) -> Result { let Some(ElementValue::Dict(bucket_key_map)) = entity.get(BUCKET_KEY_FIELD) else { return Err(SessionKeyResolutionError { reason: format!("{BUCKET_KEY_FIELD} is not a dictionary type") }); }; @@ -82,18 +83,18 @@ impl CryptoFacade { return Err(SessionKeyResolutionError { reason: "entity has no ownerGroup".to_owned() }); }; - let VersionedAesKey { key: _key, version } = self.key_loader_facade.get_current_group_key(owner_group)?; + let VersionedAesKey { version, .. } = self.key_loader_facade.get_current_sym_group_key(owner_group).await?; let ResolvedBucketKey { decrypted_bucket_key, sender_identity_key: _sender_identity_key // TODO: Use when implementing authentication - } = self.decrypt_bucket_key(&bucket_key, owner_group, model)?; + } = self.decrypt_bucket_key(&bucket_key, owner_group, model).await?; let mut session_key_for_this_instance = None; let mut re_encrypted_session_keys = Vec::with_capacity(bucket_key.bucketEncSessionKeys.len()); for instance_session_key in bucket_key.bucketEncSessionKeys { - let decrypted_session_key = decrypted_bucket_key.decrypt_key(instance_session_key.symEncSessionKey.as_slice())?; + let decrypted_session_key = decrypted_bucket_key.decrypt_aes_key(instance_session_key.symEncSessionKey.as_slice())?; let iv = Iv::generate(self.randomizer_facade.as_ref()); let re_encrypted_session_key = decrypted_bucket_key.encrypt_key(&decrypted_session_key, iv); @@ -111,6 +112,7 @@ impl CryptoFacade { // TODO: authenticate + // TODO: Update owner and session keys let mut queue = self.update_queue.lock().unwrap(); for (instance_data, sym_enc_key) in re_encrypted_session_keys { queue.queue_update_instance_session_key( @@ -128,11 +130,11 @@ impl CryptoFacade { /// Decrypts a bucket key, using `owner_group` in the case of secure external. /// /// `model` should be the type model of the instance being decrypted (e.g. `Mail`). - fn decrypt_bucket_key(&self, bucket_key: &BucketKey, owner_group: &GeneratedId, model: &TypeModel) -> Result { + async fn decrypt_bucket_key(&self, bucket_key: &BucketKey, owner_group: &GeneratedId, model: &TypeModel) -> Result { let mut auth_status = None; let resolved_key = if let (Some(key_group), Some(pub_enc_bucket_key)) = (&bucket_key.keyGroup, &bucket_key.pubEncBucketKey) { - let keypair = self.key_loader_facade.get_asymmetric_key_pair(key_group, bucket_key.recipientKeyVersion)?; + let keypair = self.key_loader_facade.load_key_pair(key_group, bucket_key.recipientKeyVersion).await?; match keypair { AsymmetricKeyPair::PQKeyPairs(k) => { let decrypted_bucket_key = PQMessage::deserialize(pub_enc_bucket_key)?.decapsulate(&k)?.into(); @@ -149,6 +151,7 @@ impl CryptoFacade { sender_identity_key: None, } } + AsymmetricKeyPair::RsaEccKeyPair(_) => { todo!() } } } else if let Some(_group_enc_bucket_key) = &bucket_key.groupEncBucketKey { // TODO: to be used with secure external @@ -262,7 +265,7 @@ mod test { use crate::crypto::crypto_facade::CryptoFacade; use crate::crypto::ecc::EccKeyPair; use crate::crypto::key::GenericAesKey; - use crate::crypto::key_loader_facade::{MockKeyLoaderFacade, VersionedAesKey}; + use crate::key_loader_facade::{MockKeyLoaderFacade, VersionedAesKey}; use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::crypto::tuta_crypt::{PQKeyPairs, PQMessage}; use crate::entities::Entity; @@ -275,8 +278,8 @@ mod test { use crate::type_model_provider::init_type_model_provider; use crate::util::test_utils::create_test_entity; - #[test] - fn test_bucket_key_resolves() { + #[tokio::test] + async fn test_bucket_key_resolves() { let randomizer_facade = Arc::new(make_thread_rng_facade()); let mut update_queue = Box::new(MockOwnerEncSessionKeysUpdateQueue::new()); update_queue.expect_queue_update_instance_session_key() @@ -298,11 +301,11 @@ mod test { let group_key = group_key.clone(); let asymmetric_keypair_versioned = asymmetric_keypair.clone(); - let mut key_loader = MockKeyLoaderFacade::new(); - key_loader.expect_get_current_group_key() - .returning(move |_| Ok(VersionedAesKey { version: sender_key_version, key: group_key.clone().into() })) + let mut key_loader = MockKeyLoaderFacade::default(); + key_loader.expect_get_current_sym_group_key() + .returning(move |_| Ok(VersionedAesKey { version: sender_key_version, object: group_key.clone().into() })) .once(); - key_loader.expect_get_asymmetric_key_pair() + key_loader.expect_load_key_pair() .returning(move |_, _| Ok(asymmetric_keypair_versioned.clone().into())) .once(); key_loader @@ -367,6 +370,7 @@ mod test { let mail_type_model = provider.get_type_model(&mail_type_ref.app, &mail_type_ref.type_).unwrap(); let key = crypto_facade.resolve_session_key(&mut raw_mail, &mail_type_model) + .await .expect("should not have errored") .expect("where is the key"); diff --git a/tuta-sdk/rust/src/crypto/key.rs b/tuta-sdk/rust/src/crypto/key.rs index b6a069c6e6f..f399aab3800 100644 --- a/tuta-sdk/rust/src/crypto/key.rs +++ b/tuta-sdk/rust/src/crypto/key.rs @@ -1,4 +1,5 @@ use zeroize::Zeroizing; +use crate::ApiCallError; use crate::util::ArrayCastingError; use super::aes::*; use super::rsa::*; @@ -7,6 +8,7 @@ use super::tuta_crypt::*; #[derive(Clone)] pub enum AsymmetricKeyPair { RSAKeyPair(RSAKeyPair), + RsaEccKeyPair(RSAEccKeyPair), PQKeyPairs(PQKeyPairs), } @@ -29,8 +31,10 @@ pub enum GenericAesKey { } impl GenericAesKey { - /// Decrypts `encrypted_key` with this key. - pub fn decrypt_key(&self, encrypted_key: &[u8]) -> Result { + /// Decrypts the AES key: `encrypted_key` with this key. + /// + /// The returned AES key is zeroized on drop + pub fn decrypt_aes_key(&self, encrypted_key: &[u8]) -> Result { let decrypted = match self { Self::Aes128(key) => aes_128_decrypt_no_padding_fixed_iv(&key, encrypted_key)?, Self::Aes256(key) => aes_256_decrypt_no_padding(&key, encrypted_key)?, @@ -40,6 +44,17 @@ impl GenericAesKey { Self::from_bytes(decrypted.as_slice()).map_err(|error| error.into()) } + /// Decrypts `ciphertext` with this key. + /// + /// The return decrypted data is not zeroized + pub fn decrypt_data(&self, ciphertext: &[u8]) -> Result, AesDecryptError> { + let decrypted = match self { + Self::Aes128(key) => aes_128_decrypt(&key, ciphertext)?, + Self::Aes256(key) => aes_256_decrypt(&key, ciphertext)?, + }; + Ok(decrypted) + } + /// Encrypts `key_to_encrypt` with this key. pub fn encrypt_key(&self, key_to_encrypt: &GenericAesKey, iv: Iv) -> Vec { match self { @@ -57,7 +72,7 @@ impl GenericAesKey { } } - pub(in crate::crypto) fn as_bytes(&self) -> &[u8] { + pub(crate) fn as_bytes(&self) -> &[u8] { match self { Self::Aes128(n) => n.as_bytes(), Self::Aes256(n) => n.as_bytes() @@ -80,7 +95,7 @@ impl From for GenericAesKey { #[derive(thiserror::Error, Debug)] #[error("Failed to load key: {reason}")] pub struct KeyLoadError { - reason: String, + pub(crate) reason: String, } /// Used to convert key related error types to `KeyLoadError` @@ -95,3 +110,8 @@ impl From for KeyLoadError { impl KeyLoadErrorSubtype for AesDecryptError {} impl KeyLoadErrorSubtype for ArrayCastingError {} + +impl KeyLoadErrorSubtype for RSAKeyError {} + +/// Used to handle errors from the entity client +impl KeyLoadErrorSubtype for ApiCallError {} diff --git a/tuta-sdk/rust/src/crypto/key_encryption.rs b/tuta-sdk/rust/src/crypto/key_encryption.rs new file mode 100644 index 00000000000..938a36b8ff7 --- /dev/null +++ b/tuta-sdk/rust/src/crypto/key_encryption.rs @@ -0,0 +1,83 @@ +use zeroize::Zeroizing; +use crate::ApiCallError; +use crate::crypto::ecc::{EccKeyPair, EccPrivateKey, EccPublicKey}; +use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; +use crate::crypto::kyber::{KyberKeyPair, KyberPrivateKey, KyberPublicKey}; +use crate::crypto::rsa::{RSAEccKeyPair, RSAKeyPair, RSAPrivateKey, RSAPublicKey}; +use crate::crypto::tuta_crypt::PQKeyPairs; +use crate::entities::sys::KeyPair; + +pub fn decrypt_key_pair(encryption_key: &GenericAesKey, key_pair: &KeyPair) -> Result { + match key_pair.symEncPrivRsaKey { + Some(_) => decrypt_rsa_or_rsa_ecc_key_pair(encryption_key, key_pair), + None => decrypt_pq_key_pair(encryption_key, key_pair) + } +} + +fn mapped_error(e: E) -> ApiCallError { + ApiCallError::InternalSdkError { error_message: e.to_string() } +} + +fn require_field<'a>(field: &'a Option>, name: &str) -> Result<&'a [u8], KeyLoadError> { + field + .as_ref() + .ok_or_else(|| KeyLoadError { reason: format!("Missing field `{name}`") }) + .map(|k| k.as_slice()) +} + +macro_rules! require_field { + ($object:expr) => { + $object + .as_ref() + .ok_or_else(|| KeyLoadError { reason: format!("Missing field `{}`", stringify!($object)) }) + .map(|k| k.as_slice()) + }; +} + +fn decrypt_pq_key_pair(encryption_key: &GenericAesKey, key_pair: &KeyPair) -> Result { + if !matches!(encryption_key, GenericAesKey::Aes256(_)) { + return Err(KeyLoadError { reason: "Invalid AES key length for PQ key pair".to_owned() }); + } + + let ecc_public_key = require_field!(key_pair.pubEccKey)?; + let ecc_private_key_enc = require_field!(key_pair.symEncPrivEccKey)?; + let ecc_private_key = Zeroizing::new(encryption_key.decrypt_data(ecc_private_key_enc)?); + + let kyber_public_key = KyberPublicKey::deserialize(require_field!(key_pair.pubKyberKey)?).map_err(mapped_error)?; + let kyber_private_key_enc = require_field!(key_pair.symEncPrivKyberKey)?; + let kyber_private_key_raw = Zeroizing::new(encryption_key.decrypt_data(kyber_private_key_enc)?); + let kyber_private_key = KyberPrivateKey::deserialize(kyber_private_key_raw.as_slice()).map_err(mapped_error)?; + + Ok(AsymmetricKeyPair::PQKeyPairs(PQKeyPairs { + ecc_keys: EccKeyPair { public_key: EccPublicKey::from_bytes(ecc_public_key).map_err(mapped_error)?, private_key: EccPrivateKey::from_bytes(ecc_private_key.as_slice()).map_err(mapped_error)? }, + kyber_keys: KyberKeyPair { public_key: kyber_public_key, private_key: kyber_private_key }, + })) +} + +fn decrypt_rsa_or_rsa_ecc_key_pair(encryption_key: &GenericAesKey, key_pair: &KeyPair) -> Result { + let public_key_pem = String::from_utf8(require_field!(key_pair.pubRsaKey)?.to_owned()) + .map_err(|error| KeyLoadError { reason: format!("Failed to decode pubRsaKey: {error}") })?; + let public_key = RSAPublicKey::from_public_key_pem(public_key_pem.as_str())?; + + let sym_enc_priv_rsa_key = require_field!(key_pair.symEncPrivRsaKey)?; + let private_key = RSAPrivateKey::from_pkcs1_der(encryption_key.decrypt_data(sym_enc_priv_rsa_key)?.as_slice())?; + + let rsa_key_pair = RSAKeyPair { + public_key, + private_key, + }; + + if let Some(ecc_key) = key_pair.symEncPrivEccKey.as_ref() { + let public_ecc_key = require_field!(key_pair.pubEccKey)?; + let private_ecc_key = Zeroizing::new(encryption_key.decrypt_data(ecc_key)?); + Ok(AsymmetricKeyPair::RsaEccKeyPair(RSAEccKeyPair { + rsa_key_pair, + ecc_key_pair: EccKeyPair { + public_key: EccPublicKey::from_bytes(public_ecc_key)?, + private_key: EccPrivateKey::from_bytes(private_ecc_key.as_slice())?, + }, + })) + } else { + Ok(AsymmetricKeyPair::RSAKeyPair(rsa_key_pair)) + } +} diff --git a/tuta-sdk/rust/src/crypto/key_loader_facade.rs b/tuta-sdk/rust/src/crypto/key_loader_facade.rs deleted file mode 100644 index e8e164b4ca3..00000000000 --- a/tuta-sdk/rust/src/crypto/key_loader_facade.rs +++ /dev/null @@ -1,27 +0,0 @@ -#![allow(unused)] // TODO: remove this later -use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; -use crate::generated_id::GeneratedId; - -pub struct VersionedAesKey { - pub key: GenericAesKey, - pub version: i64, -} - -#[derive(uniffi::Object)] -pub(crate) struct KeyLoaderFacade {} - -#[cfg_attr(test, mockall::automock)] -impl KeyLoaderFacade { - pub fn get_current_group_key(&self, group: &GeneratedId) -> Result { - todo!() - } - pub fn get_current_asymmetric_key_pair(&self, group: &GeneratedId) -> Result { - todo!() - } - pub fn get_group_key(&self, group: &GeneratedId, version: i64) -> Result { - todo!() - } - pub fn get_asymmetric_key_pair(&self, group: &GeneratedId, version: i64) -> Result { - todo!() - } -} diff --git a/tuta-sdk/rust/src/crypto/mod.rs b/tuta-sdk/rust/src/crypto/mod.rs index 5684b4b83b0..f11190d7f5a 100644 --- a/tuta-sdk/rust/src/crypto/mod.rs +++ b/tuta-sdk/rust/src/crypto/mod.rs @@ -4,14 +4,13 @@ pub mod aes; -mod sha; -mod hkdf; +pub mod sha; +pub mod hkdf; mod argon2_id; -mod ecc; -mod kyber; -mod rsa; -mod tuta_crypt; -mod key_loader_facade; +pub mod ecc; +pub mod kyber; +pub mod rsa; +pub mod tuta_crypt; pub mod crypto_facade; pub mod key; diff --git a/tuta-sdk/rust/src/crypto/rsa.rs b/tuta-sdk/rust/src/crypto/rsa.rs index 3e8b144d906..0e5675befee 100644 --- a/tuta-sdk/rust/src/crypto/rsa.rs +++ b/tuta-sdk/rust/src/crypto/rsa.rs @@ -2,13 +2,30 @@ use std::ops::Deref; use rsa::{BigUint, Oaep}; use rsa::traits::{PrivateKeyParts, PublicKeyParts}; use sha2::Sha256; -use zeroize::Zeroizing; +use zeroize::{ZeroizeOnDrop, Zeroizing}; use crate::crypto::randomizer_facade::RandomizerFacade; +use crate::crypto::ecc::EccKeyPair; use crate::join_slices; #[derive(Clone)] pub struct RSAPublicKey(rsa::RsaPublicKey); +impl RSAPublicKey { + pub fn new(public_key: rsa::RsaPublicKey) -> Self { + RSAPublicKey { + 0: public_key, + } + } + + /// Create a key from a PEM-encoded ASN.1 SPKI + pub fn from_public_key_pem(s: &str) -> Result { + use rsa as rsa_package; + use rsa_package::pkcs8::DecodePublicKey; + let new_key = rsa_package::RsaPublicKey::from_public_key_pem(s).map_err(|error| RSAKeyError { reason: error.to_string() })?; + Ok(Self(new_key)) + } +} + const RSA_PUBLIC_EXPONENT: u32 = 65537; fn public_exponent() -> BigUint { @@ -51,15 +68,38 @@ impl RSAPublicKey { } } -#[derive(Clone)] +#[derive(Clone, ZeroizeOnDrop)] pub struct RSAPrivateKey(rsa::RsaPrivateKey); +impl RSAPrivateKey { + pub fn new(private_key: rsa::RsaPrivateKey) -> Self { + RSAPrivateKey { + 0: private_key, + } + } + /// Derives an PKCS1 RSA private key from an ASN.1-DER encoded private key + pub fn from_pkcs1_der(private_key: &[u8]) -> Result { + use rsa as rsa_package; + use rsa_package::pkcs1::DecodeRsaPrivateKey; + + let derived_key = rsa_package::RsaPrivateKey::from_pkcs1_der(private_key) + .map_err(|error| RSAKeyError { reason: error.to_string() })?; + Ok(Self(derived_key)) + } +} + #[derive(Clone)] pub struct RSAKeyPair { pub public_key: RSAPublicKey, pub private_key: RSAPrivateKey, } +#[derive(Clone)] +pub struct RSAEccKeyPair { + pub rsa_key_pair: RSAKeyPair, + pub ecc_key_pair: EccKeyPair, +} + impl RSAPrivateKey { /// Instantiate an RSAPrivateKey from its components. /// diff --git a/tuta-sdk/rust/src/crypto_entity_client.rs b/tuta-sdk/rust/src/crypto_entity_client.rs index b8926507fe5..9ce4da2abed 100644 --- a/tuta-sdk/rust/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/src/crypto_entity_client.rs @@ -40,6 +40,7 @@ impl CryptoEntityClient { if type_model.encrypted { let possible_session_key = self.crypto_facade .resolve_session_key(&mut parsed_entity, type_model) + .await .map_err(|error| ApiCallError::InternalSdkError { error_message: format!( diff --git a/tuta-sdk/rust/src/entity_client.rs b/tuta-sdk/rust/src/entity_client.rs index cbe8f3d7356..14400df190e 100644 --- a/tuta-sdk/rust/src/entity_client.rs +++ b/tuta-sdk/rust/src/entity_client.rs @@ -1,7 +1,8 @@ +use std::collections::HashMap; use std::fmt::Display; use std::sync::Arc; -use crate::{ApiCallError, AuthHeadersProvider, IdTuple, ListLoadDirection, RestClient, TypeRef}; +use crate::{ApiCallError, IdTuple, LoginState, ListLoadDirection, RestClient, SdkState, TypeRef}; use crate::element_value::{ElementValue, ParsedEntity}; use crate::generated_id::GeneratedId; use crate::json_serializer::JsonSerializer; @@ -10,6 +11,7 @@ use crate::metamodel::TypeModel; use crate::rest_client::{HttpMethod, RestClientOptions}; use crate::rest_error::{HttpError}; use crate::type_model_provider::{TypeModelProvider}; +use crate::user_facade::AuthHeadersProvider; /// Denotes an ID that can be serialised into a string pub trait IdType: Display {} @@ -30,6 +32,42 @@ pub struct EntityClient { type_model_provider: Arc, } +// TODO: Fix architecture of `AuthHeadersProvider` +impl AuthHeadersProvider for SdkState { + /// This version has client_version in header, unlike the LoginState version + fn create_auth_headers(&self, model_version: u32) -> HashMap { + let auth_state = self.login_state.read().unwrap(); + let mut headers = auth_state.create_auth_headers(model_version); + headers.insert("cv".to_owned(), self.client_version.to_owned()); + headers.insert("v".to_owned(), model_version.to_string()); + headers + } + + fn is_fully_logged_in(&self) -> bool { + let auth_state = self.login_state.read().unwrap(); + auth_state.is_fully_logged_in() + } +} + +impl AuthHeadersProvider for LoginState { + fn create_auth_headers(&self, _model_version: u32) -> HashMap { + match self { + LoginState::NotLoggedIn => HashMap::new(), + LoginState::LoggedIn { access_token } => HashMap::from([ + ("accessToken".to_string(), access_token.clone()), + ]) + } + } + + fn is_fully_logged_in(&self) -> bool { + return match self { + LoginState::NotLoggedIn => false, + LoginState::LoggedIn { .. } => true + }; + } +} + + // TODO: remove this allowance after completing the implementation of `EntityClient` #[allow(unused_variables)] impl EntityClient { @@ -63,7 +101,7 @@ impl EntityClient { })?; let options = RestClientOptions { body: None, - headers: self.auth_headers_provider.auth_headers(model_version), + headers: self.auth_headers_provider.create_auth_headers(model_version), }; let response = self .rest_client @@ -143,7 +181,7 @@ impl EntityClient { let body = serde_json::to_vec(&raw_entity).unwrap(); let options = RestClientOptions { body: Some(body), - headers: self.auth_headers_provider.auth_headers(model_version), + headers: self.auth_headers_provider.create_auth_headers(model_version), }; // FIXME we should look at type model whether it is ET or LET let url = format!( diff --git a/tuta-sdk/rust/src/generated_id.rs b/tuta-sdk/rust/src/generated_id.rs index d5ed42fbdcc..fcc6cea857a 100644 --- a/tuta-sdk/rust/src/generated_id.rs +++ b/tuta-sdk/rust/src/generated_id.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error, Visitor}; /// A fixed nine byte length generated ID of an entity/instance -#[derive(Clone, Default, PartialEq, PartialOrd)] +#[derive(Clone, Default, PartialEq, PartialOrd, Eq, Hash)] #[repr(transparent)] pub struct GeneratedId(pub String); diff --git a/tuta-sdk/rust/src/key_cache.rs b/tuta-sdk/rust/src/key_cache.rs new file mode 100644 index 00000000000..3cb7c4d5a03 --- /dev/null +++ b/tuta-sdk/rust/src/key_cache.rs @@ -0,0 +1,58 @@ +use std::collections::HashMap; +use std::sync::RwLock; +use crate::crypto::aes::Aes256Key; +use crate::entities::sys::User; +use crate::generated_id::GeneratedId; +use crate::key_loader_facade::VersionedAesKey; + +pub struct KeyCache { + current_group_keys: RwLock>, + current_user_group_key: RwLock>, + user_group_key_distribution_key: RwLock>, +} + +impl KeyCache { + pub fn new() -> Self { + KeyCache { + current_group_keys: RwLock::new(HashMap::new()), + current_user_group_key: RwLock::new(None), + user_group_key_distribution_key: RwLock::new(None), + } + } + + pub fn set_current_user_group_key(&self, new_user_group_key: VersionedAesKey) { + let mut current_user_group_key_lock = self.current_user_group_key.write().unwrap(); + if current_user_group_key_lock.as_ref().is_some_and(|k| k.version > new_user_group_key.version) { + // FIXME: add logging + return; + } + *current_user_group_key_lock = Some(new_user_group_key); + } + + pub fn get_current_user_group_key(&self) -> Option { + let referenced = self.current_user_group_key.read().unwrap(); + referenced.clone() + } + + pub fn set_user_group_key_distribution_key(&self, user_group_key_distribution_key: Aes256Key) { + *self.user_group_key_distribution_key.write().unwrap() = Some(user_group_key_distribution_key); + } + + pub fn get_current_group_key(&self, group_id: &GeneratedId) -> Option { + let lock = self.current_group_keys.read().unwrap(); + lock.get(group_id).cloned() + } + + pub fn put_group_key(&self, group_id: &GeneratedId, key: &VersionedAesKey) { + let mut lock = self.current_group_keys.write().unwrap(); + lock.insert(group_id.to_owned(), key.to_owned()); + } + + // TODO: Remove allowance after implementing + #[allow(unused_variables)] + pub async fn remove_outdated_group_keys(&self, user: &User) { + todo!() + } +} + +// FIXME: test Arc clone \ No newline at end of file diff --git a/tuta-sdk/rust/src/key_loader_facade.rs b/tuta-sdk/rust/src/key_loader_facade.rs new file mode 100644 index 00000000000..bbec81d7057 --- /dev/null +++ b/tuta-sdk/rust/src/key_loader_facade.rs @@ -0,0 +1,165 @@ +use std::sync::Arc; +use base64::Engine; +use futures::future::BoxFuture; +use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; +use crate::crypto::key_encryption::decrypt_key_pair; +use crate::entities::sys::{Group, GroupKey}; +use crate::generated_id::GeneratedId; +use crate::key_cache::KeyCache; +use crate::typed_entity_client::TypedEntityClient; +use crate::user_facade::UserFacade; +use crate::util::Versioned; + +pub struct KeyLoaderFacade { + key_cache: Arc, + user_facade: Arc, + entity_client: Arc, +} + +#[cfg_attr(test, mockall::automock)] +impl KeyLoaderFacade { + pub fn new( + key_cache: Arc, + user_facade: Arc, + entity_client: Arc, + ) -> Self { + KeyLoaderFacade { + key_cache, + user_facade, + entity_client, + } + } + + pub async fn load_sym_group_key(&self, group_id: &GeneratedId, version: i64, current_group_key: Option) -> Result { + let group_key = match current_group_key.clone() { + Some(n) => n, + None => self.get_current_sym_group_key(group_id).await? + }; + + return if group_key.version == version { + Ok(group_key.object) + } else { + let group: Group = self.entity_client.load(&group_id.as_str().to_owned()).await?; + let FormerGroupKey { symmetric_group_key, .. } = self.find_former_group_key(&group, &group_key, version).await?; + Ok(symmetric_group_key) + }; + } + + async fn find_former_group_key(&self, group: &Group, current_group_key: &VersionedAesKey, target_key_version: i64) -> Result { + let list_id = group.formerGroupKeys.clone().unwrap().list; + + let start_id = GeneratedId(base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(current_group_key.version.to_string())); + let amount_of_keys_including_target = (current_group_key.version - target_key_version) as usize; + + let former_keys: Vec = self.entity_client.load_range(&list_id, &start_id, amount_of_keys_including_target, true).await?; + + let VersionedAesKey { + version: mut last_version, + object: mut last_group_key + } = current_group_key.to_owned(); + + let mut last_group_key_instance: Option = None; + let retrieved_keys_count = former_keys.len(); + + for former_key in former_keys { + let version = self.decode_group_key_version(&former_key._id.element_id)?; + let next_version = version + 1; + + if next_version > last_version { + continue; + } else if next_version == last_version { + last_version = version; + last_group_key = last_group_key.decrypt_aes_key(&former_key.ownerEncGKey).map_err(|e| { + KeyLoadError { reason: e.to_string() } + })?; + last_group_key_instance = Some(former_key); + if last_version <= target_key_version { + break; + } + } else { + return Err(KeyLoadError { reason: format!("Unexpected group key version {version}; expected {last_version}") }); + } + } + + if last_version != target_key_version || last_group_key_instance.is_none() { + return Err(KeyLoadError { reason: format!("Could not get last version (last version is {last_version} of {retrieved_keys_count} key(s) loaded from list {list_id}") }); + } + + Ok(FormerGroupKey { symmetric_group_key: last_group_key, group_key_instance: last_group_key_instance.unwrap() }) + } + + // TODO: Remove allowance after implementing + #[allow(unused_variables)] + fn decode_group_key_version(&self, element_id: &GeneratedId) -> Result { + todo!() + } + + pub async fn get_current_sym_group_key(&self, group_id: &GeneratedId) -> Result { + if *group_id == self.user_facade.get_user_group_id() { + return self.get_current_sym_user_group_key().ok_or_else(|| KeyLoadError { reason: "no current group key".to_owned() }); + } + + if let Some(key) = self.key_cache.get_current_group_key(group_id) { + return Ok(key); + } + + // The call leads to recursive calls down the chain, so BoxFuture is used to wrap the recursive async calls + fn get_key_for_version<'a>(facade: &'a KeyLoaderFacade, group_id: &'a GeneratedId) -> BoxFuture<'a, Result> { + Box::pin(facade.load_and_decrypt_current_sym_group_key(&group_id)) + } + + let key = get_key_for_version(self, &group_id).await?; + self.key_cache.put_group_key(&group_id, &key); + Ok(key) + } + + async fn load_and_decrypt_current_sym_group_key(&self, group_id: &GeneratedId) -> Result { + let group_membership = self.user_facade.get_membership(group_id)?; + let required_user_group_key = self.load_sym_user_group_key(group_membership.symKeyVersion).await?; + let version = group_membership.groupKeyVersion; + let object = required_user_group_key.decrypt_aes_key(&group_membership.symEncGKey).map_err(|e| { + KeyLoadError { reason: e.to_string() } + })?; + Ok(VersionedAesKey { version, object }) + } + + async fn load_sym_user_group_key(&self, user_group_key_version: i64) -> Result { + self.load_sym_group_key( + &self.user_facade.get_user_group_id(), + user_group_key_version, + Some(self.user_facade.get_current_user_group_key()?), + ).await + } + + fn get_current_sym_user_group_key(&self) -> Option { + self.user_facade.get_current_user_group_key().ok() + } + + pub async fn load_key_pair(&self, key_pair_group_id: &GeneratedId, group_key_version: i64) -> Result { + let group: Group = self.entity_client.load(&key_pair_group_id.to_string()).await?; + let group_key = self.get_current_sym_group_key(&group._id).await?; + + if group_key.version == group_key_version { + return self.get_and_decrypt_key_pair(&group, &group_key.object); + } + let FormerGroupKey { symmetric_group_key, group_key_instance: GroupKey { keyPair: key_pair, .. }, .. } = self.find_former_group_key(&group, &group_key, group_key_version).await?; + if let Some(key) = key_pair { + decrypt_key_pair(&symmetric_group_key, &key) + } else { + Err(KeyLoadError { reason: format!("key pair not found for group {key_pair_group_id} and version {group_key_version}") }) + } + } + fn get_and_decrypt_key_pair(&self, group: &Group, group_key: &GenericAesKey) -> Result { + return match &group.currentKeys { + Some(keys) => decrypt_key_pair(group_key, keys), + _ => Err(KeyLoadError { reason: format!("no key pair on group {}", group._id) }) + }; + } +} + +pub type VersionedAesKey = Versioned; + +struct FormerGroupKey { + symmetric_group_key: GenericAesKey, + group_key_instance: GroupKey, +} \ No newline at end of file diff --git a/tuta-sdk/rust/src/lib.rs b/tuta-sdk/rust/src/lib.rs index 0431d3e3969..1be889cd961 100644 --- a/tuta-sdk/rust/src/lib.rs +++ b/tuta-sdk/rust/src/lib.rs @@ -33,6 +33,8 @@ mod owner_enc_session_keys_update_queue; mod entities; mod instance_mapper; mod typed_entity_client; +mod key_loader_facade; +mod key_cache; pub mod date; pub mod generated_id; mod custom_id; @@ -67,11 +69,6 @@ impl Display for TypeRef { } } -trait AuthHeadersProvider { - /// Gets the HTTP request headers used for authorizing REST requests - fn auth_headers(&self, model_version: u32) -> HashMap; -} - /// The authorization status and credentials of the SDK enum LoginState { NotLoggedIn, @@ -84,6 +81,7 @@ struct SdkState { client_version: String, } + /// The external facing interface used by the consuming code via FFI #[derive(uniffi::Object)] pub struct Sdk { @@ -142,11 +140,11 @@ impl Sdk { } pub fn user_facade(&self) -> UserFacade { - UserFacade::new(self.unencrypted_entity_client.clone()) + todo!() } } -impl AuthHeadersProvider for SdkState { +impl SdkState { fn auth_headers(&self, model_version: u32) -> HashMap { let g = self.login_state.read().unwrap(); match g.deref() { diff --git a/tuta-sdk/rust/src/rest_client.rs b/tuta-sdk/rust/src/rest_client.rs index 9e7a9fe680c..d6e7adad721 100644 --- a/tuta-sdk/rust/src/rest_client.rs +++ b/tuta-sdk/rust/src/rest_client.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use thiserror::Error; -#[derive(uniffi::Enum)] +#[derive(uniffi::Enum, Debug, PartialEq)] pub enum HttpMethod { GET, POST, @@ -34,6 +34,7 @@ pub struct RestResponse { /// Provides a Rust SDK level interface for performing REST requests /// using the HTTP client injected by calling code (Kotlin/Swift/JavaScript) #[uniffi::export(with_foreign)] +#[cfg_attr(test, mockall::automock)] #[async_trait::async_trait] pub trait RestClient: Send + Sync { /// Performs an HTTP request with binary data in its body using the injected HTTP client diff --git a/tuta-sdk/rust/src/typed_entity_client.rs b/tuta-sdk/rust/src/typed_entity_client.rs index f3ac84beb12..d53214d351a 100644 --- a/tuta-sdk/rust/src/typed_entity_client.rs +++ b/tuta-sdk/rust/src/typed_entity_client.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use crate::ApiCallError; use crate::entities::Entity; use crate::entity_client::{EntityClient, IdType}; +use crate::generated_id::GeneratedId; use crate::instance_mapper::InstanceMapper; pub struct TypedEntityClient { @@ -39,6 +40,18 @@ impl TypedEntityClient { })?; Ok(typed_entity) } + + // TODO: Remove allowance after implementing + #[allow(unused_variables)] + pub async fn load_range>( + &self, + list_id: &GeneratedId, + start_id: &GeneratedId, + amount: usize, + reverse: bool, + ) -> Result, ApiCallError> { + todo!() + } } diff --git a/tuta-sdk/rust/src/user_facade.rs b/tuta-sdk/rust/src/user_facade.rs index a5023b15db1..20786a369a2 100644 --- a/tuta-sdk/rust/src/user_facade.rs +++ b/tuta-sdk/rust/src/user_facade.rs @@ -1,26 +1,140 @@ -use std::sync::Arc; +use std::borrow::ToOwned; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use base64::Engine; +use base64::prelude::BASE64_STANDARD; use crate::ApiCallError; -use crate::entities::sys::User; +use crate::crypto::aes::{Aes256Key, AES_256_KEY_SIZE}; +use crate::crypto::hkdf::hkdf; +use crate::crypto::sha::sha256; +use crate::entities::sys::{GroupMembership, User}; +use crate::key_cache::KeyCache; +use crate::crypto::key::GenericAesKey; use crate::generated_id::GeneratedId; -use crate::typed_entity_client::TypedEntityClient; +use crate::key_loader_facade::VersionedAesKey; +use crate::util::Versioned; +pub trait AuthHeadersProvider { + /// Gets the HTTP request headers used for authorizing REST requests + fn create_auth_headers(&self, model_version: u32) -> HashMap; + fn is_fully_logged_in(&self) -> bool; +} + +const USER_GROUP_KEY_DISTRIBUTION_KEY_INFO: &str = "userGroupKeyDistributionKey"; /// FIXME: for testing unencrypted entity downloading. Remove after everything works together. #[derive(uniffi::Object)] pub struct UserFacade { - entity_client: Arc, + user: RwLock>, + key_cache: Arc, } impl UserFacade { - pub fn new(entity_client: Arc) -> Self { - UserFacade { entity_client } + // FIXME: Do we pass in user or not + pub fn new(key_cache: Arc, user: User) -> Self { + UserFacade { + user: RwLock::new(Arc::new(user)), + key_cache, + } + } + + pub fn set_user(&mut self, user: User) { + *self.user.write().unwrap() = Arc::new(user); + } + + pub fn unlock_user_group_key(&mut self, user_passphrase_key: GenericAesKey) -> Result<(), ApiCallError> { + let user = self.get_user(); + let user_group_membership = &user.userGroup; + let current_user_group_key = Versioned::new( + user_passphrase_key.decrypt_aes_key(&user_group_membership.symEncGKey).map_err(|e| { + ApiCallError::InternalSdkError { error_message: e.to_string() } + })?, + user_group_membership.groupKeyVersion, + ); + self.key_cache.set_current_user_group_key(current_user_group_key); + self.set_user_group_key_distribution_key(user_passphrase_key) + } + + fn set_user_group_key_distribution_key(&mut self, user_passphrase_key: GenericAesKey) -> Result<(), ApiCallError> { + let user = self.get_user(); + let user_group_membership = &user.userGroup; + let user_group_key_distribution_key = self.derive_user_group_key_distribution_key(&user_group_membership.group, user_passphrase_key)?; + match user_group_key_distribution_key { + GenericAesKey::Aes128(_) => { + Err(ApiCallError::InternalSdkError { error_message: "invalid derived key size".to_owned() }) + } + GenericAesKey::Aes256(key) => { + Ok(self.key_cache.set_user_group_key_distribution_key(key)) + } + } + } + + // FIXME: Check uint8ArrayToBase64 is correct; + // there is a max length in the ts version, it seems to be a js thing, can we forego it here? + fn derive_user_group_key_distribution_key(&self, user_group_id: &GeneratedId, user_passphrase_key: GenericAesKey) -> Result { + // we prepare a key to encrypt potential user group key rotations with + // when passwords are changed clients are logged-out of other sessions + // this key is only needed by the logged-in clients, so it should be reliable enough to assume that userPassphraseKey is in sync + let user_group_id_hash = sha256(user_group_id.as_str().as_bytes()); + // we bind this to userGroupId and the domain separator USER_GROUP_KEY_DISTRIBUTION_KEY_INFO + // the hkdf salt does not have to be secret but should be unique per user and carry some additional entropy which sha256 ensures + let aes_key = + hkdf(user_group_id_hash.as_slice(), + BASE64_STANDARD.encode(user_passphrase_key.as_bytes()).as_bytes(), + USER_GROUP_KEY_DISTRIBUTION_KEY_INFO.as_bytes(), AES_256_KEY_SIZE); + return match aes_key.len() { + AES_256_KEY_SIZE => { + Ok(GenericAesKey::Aes256(Aes256Key::from_bytes(aes_key.as_slice()).expect("invalid derived key size"))) + } + _ => { + Err(ApiCallError::InternalSdkError { error_message: "invalid derived key size".to_owned() }) + } + }; + } + + + pub async fn update_user(&self, user: User) { + let user = Arc::new(user); + *self.user.write().unwrap() = user.clone(); + self.key_cache.remove_outdated_group_keys(user.as_ref()).await; + } + + pub fn get_user(&self) -> Arc { + self.user.read().unwrap().clone() + } + + pub fn get_user_group_id(&self) -> GeneratedId { + self.get_user().userGroup.group.clone() + } + + #[allow(dead_code)] // Remove when implementing `generateUserAreaGroupData()` + fn get_all_group_ids(&self) -> Vec { + let mut groups: Vec = self.get_user().memberships.iter().map(|membership| membership.group.clone()).collect(); + groups.push(self.get_user().userGroup.group.clone()); + groups + } + + pub fn get_current_user_group_key(&self) -> Result { + self.key_cache.get_current_user_group_key() + .ok_or_else(|| ApiCallError::InternalSdkError { error_message: "userGroupKey not available".to_owned() }) + } + + pub(crate) fn get_membership(&self, group_id: &GeneratedId) -> Result { + let memberships = &self.get_user().memberships; + memberships.iter().find(|g| g.group == *group_id) + .map(|m| m.to_owned()) + .ok_or_else(|| ApiCallError::InternalSdkError { error_message: format!("No group with groupId {} found!", group_id) }) } } -#[uniffi::export] -impl UserFacade { - /// Gets a user (an entity/instance of `User`) from the backend - pub async fn load_user_by_id(&self, id: &GeneratedId) -> Result { - self.entity_client.load(id).await +// TODO: Remove allowance after implementing +#[allow(unused_variables)] +impl AuthHeadersProvider for UserFacade { + fn create_auth_headers(&self, model_version: u32) -> HashMap { + todo!() } -} \ No newline at end of file + + fn is_fully_logged_in(&self) -> bool { + todo!() + } +} diff --git a/tuta-sdk/rust/src/util/mod.rs b/tuta-sdk/rust/src/util/mod.rs index 85e68b0974c..4fb16b5d43c 100644 --- a/tuta-sdk/rust/src/util/mod.rs +++ b/tuta-sdk/rust/src/util/mod.rs @@ -2,6 +2,26 @@ pub mod test_utils; pub mod entity_test_utils; +pub struct Versioned { + pub object: T, + pub version: i64 +} + +impl Clone for Versioned where T: Clone { + fn clone(&self) -> Self { + Versioned { object: self.object.clone(), version: self.version } + } +} + +impl Versioned { + pub fn new(object: T, version: i64) -> Versioned { + Versioned { + object, + version + } + } +} + /// Combine multiple slices into one Vec. /// /// Each slice must have the same object type, and the object must implement Copy. This makes it suitable for byte arrays. diff --git a/tuta-sdk/rust/test_data/group_response.json b/tuta-sdk/rust/test_data/group_response.json index 23fbfc10303..7ff20ca46f8 100644 --- a/tuta-sdk/rust/test_data/group_response.json +++ b/tuta-sdk/rust/test_data/group_response.json @@ -1,7 +1,7 @@ { "_format": "0", - "_id": "LIopQQN--N-0", - "_ownerGroup": "LIopQQN--N-0", + "_id": "O0IlmJq----0", + "_ownerGroup": "O0IlmJq----0", "_permissions": "LIopQQN--R-0", "adminGroupEncGKey": "", "adminGroupKeyVersion": "0", @@ -10,15 +10,23 @@ "groupKeyVersion": "0", "pubAdminGroupEncGKey": null, "type": "5", - "admin": "LIopQQI--c-0", + "admin": "O0IlmJn--c-0", "administratedGroups": null, "archives": [], - "currentKeys": null, - "customer": "LIopQQI--7-0", - "formerGroupKeys": null, + "currentKeys": { + "_id": "nS9dTQ", + "pubEccKey": null, + "pubKyberKey": null, + "pubRsaKey": null, + "symEncPrivEccKey": null, + "symEncPrivKyberKey": null, + "symEncPrivRsaKey": null + }, + "customer": "O0IlmJn--7-0", + "formerGroupKeys": { "_id": "123", "list": "list" }, "groupInfo": ["LIopQQI--k-0", "LIopQQN--c-0"], "invitations": "LIopQQN--V-0", "members": "LIopQQN--Z-0", "storageCounter": "NWJVVC_----0", - "user": "LIopQQI----0" + "user": "O0IlmJn----0" } diff --git a/tuta-sdk/rust/test_data/user_response.json b/tuta-sdk/rust/test_data/user_response.json new file mode 100644 index 00000000000..293f104e3a5 --- /dev/null +++ b/tuta-sdk/rust/test_data/user_response.json @@ -0,0 +1,59 @@ +{ + "_format": "0", + "_id": "O0IlmJn----0", + "_ownerGroup": "O0IlmJq----0", + "_permissions": "O0IlmJq--k-0", + "accountType": "1", + "enabled": "1", + "kdfVersion": "0", + "requirePasswordUpdate": "0", + "salt": "AAECAwQFBgcICQoLDA0ODw==", + "verifier": "vl8lWD9PaOR5z3qOd5SHQsbJEDWFVa7dMUtUSP9l9C0=", + "alarmInfoList": { + "_id": "h3I_yg", + "alarms": "O0IlmJq-0B-0" + }, + "auth": { + "_id": "COxyWw", + "recoverCode": "O0IlmJq-0F-0", + "secondFactors": "O0IlmJq-07-0", + "sessions": "O0IlmJq-03-0" + }, + "authenticatedDevices": [], + "customer": "O0IlmJn--7-0", + "externalAuthInfo": null, + "failedLogins": "O0IlmJq--s-0", + "memberships": [ + { + "_id": "x6JM7w", + "admin": "0", + "capability": null, + "groupKeyVersion": "0", + "groupType": "3", + "symEncGKey": "AWahQP4tJIRsTaPw4WtwY9XndnIn55vO/hlntGe3/ot2+CR2WGhkHpWtUvbt9r4VEi9oNHvpPC1348Y+hn0qdzCevj5ORBU7ybOJEyikvunM", + "symKeyVersion": "0", + "group": "O0IlmJn--g-0", + "groupInfo": ["O0IlmJn--J-0", "O0IlmJp--B-0"], + "groupMember": ["O0IlmJp--7-0", "O0IlmJr--F-0"] + } + ], + "phoneNumbers": [], + "pushIdentifierList": { + "_id": "63qUow", + "list": "O0IlmJq-0--0" + }, + "secondFactorAuthentications": "O0IlmJq--w-0", + "successfulLogins": "O0IlmJq--o-0", + "userGroup": { + "_id": "4i5wHQ", + "admin": "0", + "capability": null, + "groupKeyVersion": "0", + "groupType": "0", + "symEncGKey": "3GMv8X6ItBoE9+ippSw0KXH5VqPvxD5UGwd/0QnuDqU=", + "symKeyVersion": "0", + "group": "O0IlmJq----0", + "groupInfo": ["O0IlmJn--N-0", "O0IlmJq--F-0"], + "groupMember": ["O0IlmJq--B-0", "O0IlmJq-0N-0"] + } +} From 57068576d0f2e1eca36039e4baa2d16c789fb904 Mon Sep 17 00:00:00 2001 From: jat Date: Wed, 3 Jul 2024 16:04:35 +0200 Subject: [PATCH 2/8] [SDK] Remove dependency on aes in `entity_facade` Co-authored-by: paw --- tuta-sdk/rust/src/entities/entity_facade.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tuta-sdk/rust/src/entities/entity_facade.rs b/tuta-sdk/rust/src/entities/entity_facade.rs index c6b3ebd6c92..480a41a8a63 100644 --- a/tuta-sdk/rust/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/src/entities/entity_facade.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::SystemTime; use crate::ApiCallError; -use crate::crypto::aes::{aes_128_decrypt, aes_256_decrypt, IV_BYTE_SIZE}; +use crate::crypto::aes::IV_BYTE_SIZE; use crate::date::DateTime; use crate::crypto::key::GenericAesKey; use crate::element_value::{ElementValue, ParsedEntity}; @@ -147,10 +147,7 @@ impl EntityFacade { } if model_value.encrypted { - let decrypted_value = match session_key { - GenericAesKey::Aes128(k) => aes_128_decrypt(k, value.assert_bytes().as_slice()), - GenericAesKey::Aes256(k) => aes_256_decrypt(k, value.assert_bytes().as_slice()) - }; + let decrypted_value = session_key.decrypt_data(value.assert_bytes().as_slice()); let mut errors: HashMap = Default::default(); let element_value = match decrypted_value { From bf37deff5a003204b0a57d945210805bf9c27c54 Mon Sep 17 00:00:00 2001 From: jat Date: Wed, 3 Jul 2024 17:13:12 +0200 Subject: [PATCH 3/8] [SDK] Hide `crypto`'s internals We should try to use high level abstractions over the objects in `crypto` where possible. Co-authored-by: paw --- tuta-sdk/rust/src/crypto/aes.rs | 11 ++++++++ tuta-sdk/rust/src/crypto/key.rs | 9 +++++++ tuta-sdk/rust/src/crypto/mod.rs | 29 ++++++++++++++------- tuta-sdk/rust/src/crypto_entity_client.rs | 2 +- tuta-sdk/rust/src/entities/entity_facade.rs | 4 +-- tuta-sdk/rust/src/key_cache.rs | 2 +- tuta-sdk/rust/src/user_facade.rs | 6 ++--- tuta-sdk/rust/src/util/entity_test_utils.rs | 9 ++----- tuta-sdk/rust/src/util/mod.rs | 1 + 9 files changed, 50 insertions(+), 23 deletions(-) diff --git a/tuta-sdk/rust/src/crypto/aes.rs b/tuta-sdk/rust/src/crypto/aes.rs index 96f9e46eef1..91344b0ce4c 100644 --- a/tuta-sdk/rust/src/crypto/aes.rs +++ b/tuta-sdk/rust/src/crypto/aes.rs @@ -133,6 +133,17 @@ trait AesKey: Clone { /// An initialisation vector for AES encryption pub struct Iv([u8; IV_BYTE_SIZE]); +#[cfg(test)] +impl Clone for Iv { + /// Clone the initialization vector + /// + /// This is implemented so that entity_facade_test_utils will work. You should never, ever, ever + /// re-use an IV, as this can lead to information leakage. + fn clone(&self) -> Self { + Iv(self.0.clone()) + } +} + impl Iv { /// Generate an initialisation vector. pub fn generate(randomizer_facade: &RandomizerFacade) -> Self { diff --git a/tuta-sdk/rust/src/crypto/key.rs b/tuta-sdk/rust/src/crypto/key.rs index f399aab3800..83b16daa722 100644 --- a/tuta-sdk/rust/src/crypto/key.rs +++ b/tuta-sdk/rust/src/crypto/key.rs @@ -63,6 +63,15 @@ impl GenericAesKey { } } + /// Encrypts `text` with this key. + pub fn encrypt_data(&self, text: &[u8], iv: Iv) -> Result, AesEncryptError> { + let ciphertext = match self { + Self::Aes128(key) => aes_128_encrypt(&key, text, &iv, PaddingMode::WithPadding, MacMode::WithMac)?, + Self::Aes256(key) => aes_256_encrypt(&key, text, &iv, PaddingMode::WithPadding)?, + }; + Ok(ciphertext) + } + pub fn from_bytes(bytes: &[u8]) -> Result { match bytes.len() { // The unwraps here are optimised away diff --git a/tuta-sdk/rust/src/crypto/mod.rs b/tuta-sdk/rust/src/crypto/mod.rs index f11190d7f5a..2551257a0a0 100644 --- a/tuta-sdk/rust/src/crypto/mod.rs +++ b/tuta-sdk/rust/src/crypto/mod.rs @@ -3,17 +3,28 @@ // TODO: Remove the above allowance when starting to implement higher level functions -pub mod aes; -pub mod sha; -pub mod hkdf; +mod aes; + +#[cfg(test)] +pub use aes::Iv; +pub use aes::{Aes256Key, Aes128Key, AES_256_KEY_SIZE, AES_128_KEY_SIZE, IV_BYTE_SIZE}; + +mod sha; + +pub use sha::{sha256, sha512}; + +mod hkdf; + +pub use hkdf::hkdf; + mod argon2_id; -pub mod ecc; -pub mod kyber; -pub mod rsa; -pub mod tuta_crypt; +mod ecc; +mod kyber; +mod rsa; +mod tuta_crypt; +pub mod key_encryption; pub mod crypto_facade; pub mod key; - +pub mod randomizer_facade; #[cfg(test)] mod compatibility_test_utils; -pub mod randomizer_facade; diff --git a/tuta-sdk/rust/src/crypto_entity_client.rs b/tuta-sdk/rust/src/crypto_entity_client.rs index 9ce4da2abed..a3f740d61b3 100644 --- a/tuta-sdk/rust/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/src/crypto_entity_client.rs @@ -88,7 +88,7 @@ impl CryptoEntityClient { mod tests { use std::sync::Arc; use rand::random; - use crate::crypto::aes::{Aes256Key, Iv}; + use crate::crypto::{Aes256Key, Iv}; use crate::crypto::crypto_facade::MockCryptoFacade; use crate::crypto::key::GenericAesKey; use crate::crypto_entity_client::CryptoEntityClient; diff --git a/tuta-sdk/rust/src/entities/entity_facade.rs b/tuta-sdk/rust/src/entities/entity_facade.rs index 480a41a8a63..79d40dee403 100644 --- a/tuta-sdk/rust/src/entities/entity_facade.rs +++ b/tuta-sdk/rust/src/entities/entity_facade.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use std::time::SystemTime; use crate::ApiCallError; -use crate::crypto::aes::IV_BYTE_SIZE; +use crate::crypto::IV_BYTE_SIZE; use crate::date::DateTime; use crate::crypto::key::GenericAesKey; use crate::element_value::{ElementValue, ParsedEntity}; @@ -242,7 +242,7 @@ mod tests { use rand::random; - use crate::crypto::aes::{Aes256Key, Iv}; + use crate::crypto::{Aes256Key, Iv}; use crate::crypto::key::GenericAesKey; use crate::entities::entity_facade::EntityFacade; use crate::util::entity_test_utils::{assert_decrypted_mail, generate_email_entity}; diff --git a/tuta-sdk/rust/src/key_cache.rs b/tuta-sdk/rust/src/key_cache.rs index 3cb7c4d5a03..5ecaff5f5c1 100644 --- a/tuta-sdk/rust/src/key_cache.rs +++ b/tuta-sdk/rust/src/key_cache.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::RwLock; -use crate::crypto::aes::Aes256Key; +use crate::crypto::Aes256Key; use crate::entities::sys::User; use crate::generated_id::GeneratedId; use crate::key_loader_facade::VersionedAesKey; diff --git a/tuta-sdk/rust/src/user_facade.rs b/tuta-sdk/rust/src/user_facade.rs index 20786a369a2..db4422b57ad 100644 --- a/tuta-sdk/rust/src/user_facade.rs +++ b/tuta-sdk/rust/src/user_facade.rs @@ -4,9 +4,9 @@ use std::sync::{Arc, RwLock}; use base64::Engine; use base64::prelude::BASE64_STANDARD; use crate::ApiCallError; -use crate::crypto::aes::{Aes256Key, AES_256_KEY_SIZE}; -use crate::crypto::hkdf::hkdf; -use crate::crypto::sha::sha256; +use crate::crypto::{Aes256Key, AES_256_KEY_SIZE}; +use crate::crypto::hkdf; +use crate::crypto::sha256; use crate::entities::sys::{GroupMembership, User}; use crate::key_cache::KeyCache; use crate::crypto::key::GenericAesKey; diff --git a/tuta-sdk/rust/src/util/entity_test_utils.rs b/tuta-sdk/rust/src/util/entity_test_utils.rs index 7f79becaca8..573e343e290 100644 --- a/tuta-sdk/rust/src/util/entity_test_utils.rs +++ b/tuta-sdk/rust/src/util/entity_test_utils.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use crate::crypto::aes::{aes_128_encrypt, aes_256_encrypt, Iv, MacMode, PaddingMode}; +use crate::crypto::Iv; use crate::crypto::key::GenericAesKey; use crate::element_value::{ElementValue, ParsedEntity}; use crate::generated_id::GeneratedId; @@ -20,12 +20,7 @@ pub fn assert_decrypted_mail(result: &ParsedEntity, plaintext_mail: &ParsedEntit } pub fn encrypt_bytes(encryption_key: &GenericAesKey, bytes: &[u8], iv: &Iv) -> Vec { - let encrypted_bytes = match encryption_key { - GenericAesKey::Aes128(key) => aes_128_encrypt(key, bytes, iv, PaddingMode::WithPadding, MacMode::WithMac), - GenericAesKey::Aes256(key) => aes_256_encrypt(key, bytes, iv, PaddingMode::WithPadding), - }; - - encrypted_bytes.unwrap() + encryption_key.encrypt_data(bytes, iv.to_owned()).unwrap() } /// Generates and returns an encrypted Mail ParsedEntity. It also returns the decrypted Mail for comparison diff --git a/tuta-sdk/rust/src/util/mod.rs b/tuta-sdk/rust/src/util/mod.rs index 4fb16b5d43c..c42a2c12359 100644 --- a/tuta-sdk/rust/src/util/mod.rs +++ b/tuta-sdk/rust/src/util/mod.rs @@ -1,5 +1,6 @@ #[cfg(test)] pub mod test_utils; +#[cfg(test)] pub mod entity_test_utils; pub struct Versioned { From c6228692b4795762a26c90bafd10ac3ed0cf01ca Mon Sep 17 00:00:00 2001 From: jat Date: Wed, 3 Jul 2024 17:37:18 +0200 Subject: [PATCH 4/8] [SDK] Fix the implementations of the AES keys colliding --- tuta-sdk/rust/src/crypto/aes.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tuta-sdk/rust/src/crypto/aes.rs b/tuta-sdk/rust/src/crypto/aes.rs index 91344b0ce4c..6942df35d0f 100644 --- a/tuta-sdk/rust/src/crypto/aes.rs +++ b/tuta-sdk/rust/src/crypto/aes.rs @@ -115,7 +115,7 @@ aes_key!( aes_key!( Aes256Key, - "Aes128Key", + "Aes256Key", AES_256_KEY_SIZE, aes::Aes256, sha2::Sha512 From 9601237b6a3c4ff986836a6c9f3253c6e784271e Mon Sep 17 00:00:00 2001 From: jat Date: Wed, 3 Jul 2024 18:07:10 +0200 Subject: [PATCH 5/8] [SDK] Remove unused file --- tuta-sdk/rust/src/util/test.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tuta-sdk/rust/src/util/test.rs diff --git a/tuta-sdk/rust/src/util/test.rs b/tuta-sdk/rust/src/util/test.rs deleted file mode 100644 index e69de29bb2d..00000000000 From 2c77121575804cce6934367e259389803bb61bb9 Mon Sep 17 00:00:00 2001 From: jat Date: Mon, 8 Jul 2024 15:23:21 +0200 Subject: [PATCH 6/8] [SDK] Expand byte array encoding tests Co-authored-by: ivk --- tuta-sdk/rust/src/util/mod.rs | 41 +++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/tuta-sdk/rust/src/util/mod.rs b/tuta-sdk/rust/src/util/mod.rs index c42a2c12359..92f2af26b07 100644 --- a/tuta-sdk/rust/src/util/mod.rs +++ b/tuta-sdk/rust/src/util/mod.rs @@ -5,7 +5,7 @@ pub mod entity_test_utils; pub struct Versioned { pub object: T, - pub version: i64 + pub version: i64, } impl Clone for Versioned where T: Clone { @@ -18,7 +18,7 @@ impl Versioned { pub fn new(object: T, version: i64) -> Versioned { Versioned { object, - version + version, } } } @@ -144,6 +144,19 @@ pub fn array_cast_size(arr: [u8; ARR_S mod test { use super::*; + /// Returns a handwritten encoded byte array and its decoded equivalent + const fn get_test_byte_arrays<'a>() -> ([u8; 11], [&'a [u8]; 2]) { + let encoded_byte_arrays = [ + 0, 5, 123, 45, 67, 89, 10, + 0, 2, 22, 23 + ]; + let decoded_byte_arrays = [ + [123, 45, 67, 89, 10].as_slice(), + [22, 23].as_slice() + ]; + (encoded_byte_arrays, decoded_byte_arrays) + } + #[test] fn combine_slices() { let a = &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]; @@ -173,20 +186,24 @@ mod test { } #[test] - fn test_encoded_byte_arrays() { - let encoded_byte_arrays = [ - 0, 5, 123, 45, 67, 89, 10, - 0, 2, 22, 23 - ]; - let decoded_byte_arrays = [ - [123, 45, 67, 89, 10].as_slice(), - [22, 23].as_slice() - ]; + fn test_encode_byte_arrays() { + let (encoded_byte_arrays, decoded_byte_arrays) = get_test_byte_arrays(); + let encoded = encode_byte_arrays(&decoded_byte_arrays).unwrap(); + assert_eq!(encoded_byte_arrays, encoded.as_slice()); + } + #[test] + fn test_decode_byte_arrays() { + let (encoded_byte_arrays, decoded_byte_arrays) = get_test_byte_arrays(); let decoded = decode_byte_arrays::<2>(&encoded_byte_arrays).unwrap(); assert_eq!(decoded_byte_arrays, decoded); + } + #[test] + fn test_byte_arrays_encoding_roundtrip() { + let (_, decoded_byte_arrays) = get_test_byte_arrays(); let encoded = encode_byte_arrays(&decoded_byte_arrays).unwrap(); - assert_eq!(encoded_byte_arrays, encoded.as_slice()); + let decoded = decode_byte_arrays::<2>(&encoded).unwrap(); + assert_eq!(decoded_byte_arrays, decoded); } } \ No newline at end of file From 3c33af10220e8a3a257cbc626be6467274a4e3bd Mon Sep 17 00:00:00 2001 From: jat Date: Mon, 8 Jul 2024 15:57:01 +0200 Subject: [PATCH 7/8] [SDK] Expand kyber tests --- tuta-sdk/rust/src/crypto/kyber.rs | 64 +++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/tuta-sdk/rust/src/crypto/kyber.rs b/tuta-sdk/rust/src/crypto/kyber.rs index 6f0df55bbc2..9ec8456c18d 100644 --- a/tuta-sdk/rust/src/crypto/kyber.rs +++ b/tuta-sdk/rust/src/crypto/kyber.rs @@ -1,5 +1,6 @@ //! Contains code to handle Kyber-1024 encapsulation and decapsulation. +use std::fmt::{Debug, Formatter}; use pqcrypto_kyber::{kyber1024_decapsulate, kyber1024_encapsulate}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use crate::util::{ArrayCastingError, decode_byte_arrays, encode_byte_arrays, array_cast_slice}; @@ -24,11 +25,24 @@ const KYBER_PUBLIC_KEY_LEN: usize = KYBER_POLYVECBYTES + KYBER_SYMBYTES; const KYBER_SECRET_KEY_LEN: usize = 2 * KYBER_POLYVECBYTES + 3 * KYBER_SYMBYTES; /// Key used for performing encapsulation, owned by the recipient. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct KyberPublicKey { public_key: PQCryptoKyber1024PublicKey, } +impl KyberPublicKey { + pub fn as_bytes(&self) -> &[u8] { + self.public_key.as_bytes() + } +} + +#[cfg(test)] // only allow Debug in tests because this prints the key! +impl Debug for KyberPublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.public_key.as_bytes().fmt(f) + } +} + impl KyberPublicKey { /// Instantiate a public key from encoded byte arrays. /// @@ -80,11 +94,25 @@ impl From for KyberPublicKey { } /// Key used for performing decapsulation, owned by the recipient. -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct KyberPrivateKey { private_key: PQCryptoKyber1024SecretKey, } +impl KyberPrivateKey { + /// Returns this private key as a slice of bytes + pub fn as_bytes(&self) -> &[u8] { + self.private_key.as_bytes() + } +} + +#[cfg(test)] // only allow Debug in tests because this prints the key! +impl Debug for KyberPrivateKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.private_key.as_bytes().fmt(f) + } +} + impl KyberPrivateKey { /// Instantiate a private key from encoded byte arrays. /// @@ -195,7 +223,8 @@ impl TryFrom<&[u8]> for KyberCiphertext { } /// Shared secret generated from either [`KyberPublicKey::encapsulate`] or [`KyberPrivateKey::decapsulate`]. -#[derive(Zeroize, ZeroizeOnDrop)] +#[derive(Zeroize, ZeroizeOnDrop, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the secret! pub struct KyberSharedSecret([u8; KYBER_SHARED_SECRET_LEN]); impl KyberSharedSecret { @@ -212,7 +241,8 @@ pub struct KyberEncapsulation { pub shared_secret: KyberSharedSecret, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct KyberKeyPair { pub public_key: KyberPublicKey, pub private_key: KyberPrivateKey, @@ -262,4 +292,30 @@ mod tests { assert_eq!(public_key.public_key.as_bytes(), private_key.get_public_key().public_key.as_bytes()); } } + + #[test] + fn test_kyber_encoding_roundtrip() { + // Generate some raw unencoded kyber keys + let KyberKeyPair { public_key, private_key } = KyberKeyPair::generate(); + + // Encode the kyber keys + let encoded_public_key = public_key.serialize(); + let encoded_private_key = private_key.serialize(); + + // Decode the encoded kyber keys which should give us the raw kyber keys back + let decoded_public_key = KyberPublicKey::deserialize(encoded_public_key.as_slice()).unwrap(); + let decoded_private_key = KyberPrivateKey::deserialize(encoded_private_key.as_slice()).unwrap(); + + assert_eq!(public_key, decoded_public_key); + assert_eq!(private_key, decoded_private_key); + } + + #[test] + fn test_kyber_encryption_roundtrip() { + let key_pair = KyberKeyPair::generate(); + let encapsulated = key_pair.public_key.encapsulate(); + let shared_secret_alice = encapsulated.shared_secret; + let shared_secret_bob = key_pair.private_key.decapsulate(&encapsulated.ciphertext).unwrap(); + assert_eq!(shared_secret_alice, shared_secret_bob) + } } From 3529b00496d170fcb23fc9684c881aa2bc6165a2 Mon Sep 17 00:00:00 2001 From: jat Date: Thu, 4 Jul 2024 16:46:39 +0200 Subject: [PATCH 8/8] [SDK] Port some key loader tests These should be reintroduced and completed by #7217. The rest of the tests from `4d035f0` should also be ported. TODO: Clean up mock initialisation into functions Co-authored-by: ivk --- tuta-sdk/rust/src/crypto/aes.rs | 3 +- tuta-sdk/rust/src/crypto/ecc.rs | 9 +- tuta-sdk/rust/src/crypto/key.rs | 45 +- tuta-sdk/rust/src/crypto/key_encryption.rs | 38 ++ tuta-sdk/rust/src/crypto/mod.rs | 3 + tuta-sdk/rust/src/crypto/rsa.rs | 12 +- tuta-sdk/rust/src/crypto/tuta_crypt.rs | 3 +- tuta-sdk/rust/src/crypto_entity_client.rs | 5 +- tuta-sdk/rust/src/custom_id.rs | 6 + tuta-sdk/rust/src/entity_client.rs | 2 +- tuta-sdk/rust/src/instance_mapper.rs | 41 +- tuta-sdk/rust/src/key_cache.rs | 1 + tuta-sdk/rust/src/key_loader_facade.rs | 500 ++++++++++++++++++++- tuta-sdk/rust/src/typed_entity_client.rs | 1 + tuta-sdk/rust/src/user_facade.rs | 22 +- tuta-sdk/rust/src/util/mod.rs | 7 + tuta-sdk/rust/src/util/test_utils.rs | 63 ++- 17 files changed, 691 insertions(+), 70 deletions(-) diff --git a/tuta-sdk/rust/src/crypto/aes.rs b/tuta-sdk/rust/src/crypto/aes.rs index 6942df35d0f..c4c96579be2 100644 --- a/tuta-sdk/rust/src/crypto/aes.rs +++ b/tuta-sdk/rust/src/crypto/aes.rs @@ -47,7 +47,8 @@ pub enum EnforceMac { /// - $subkey_digest: The type of SHA hasher from the `sha2` dependency to use. macro_rules! aes_key { ($name:tt, $type_name:literal, $size:expr, $cbc:ty, $subkey_digest:ty) => { - #[derive(Clone, ZeroizeOnDrop)] + #[derive(Clone, ZeroizeOnDrop, PartialEq)] + #[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct $name([u8; $size]); impl $name { diff --git a/tuta-sdk/rust/src/crypto/ecc.rs b/tuta-sdk/rust/src/crypto/ecc.rs index b9e531fe064..b2adbc269df 100644 --- a/tuta-sdk/rust/src/crypto/ecc.rs +++ b/tuta-sdk/rust/src/crypto/ecc.rs @@ -5,7 +5,8 @@ use crate::util::{ArrayCastingError, array_cast_slice}; const ECC_KEY_SIZE: usize = 32; -#[derive(ZeroizeOnDrop, Clone)] +#[derive(ZeroizeOnDrop, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct EccPrivateKey([u8; ECC_KEY_SIZE]); impl EccPrivateKey { @@ -40,10 +41,12 @@ impl EccPrivateKey { } } -#[derive(ZeroizeOnDrop, Clone)] +#[derive(ZeroizeOnDrop, Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct EccPublicKey([u8; ECC_KEY_SIZE]); -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct EccKeyPair { pub public_key: EccPublicKey, pub private_key: EccPrivateKey, diff --git a/tuta-sdk/rust/src/crypto/key.rs b/tuta-sdk/rust/src/crypto/key.rs index 83b16daa722..d0a8108fb50 100644 --- a/tuta-sdk/rust/src/crypto/key.rs +++ b/tuta-sdk/rust/src/crypto/key.rs @@ -5,7 +5,8 @@ use super::aes::*; use super::rsa::*; use super::tuta_crypt::*; -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub enum AsymmetricKeyPair { RSAKeyPair(RSAKeyPair), RsaEccKeyPair(RSAEccKeyPair), @@ -24,7 +25,8 @@ impl From for AsymmetricKeyPair { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub enum GenericAesKey { Aes128(Aes128Key), Aes256(Aes256Key), @@ -124,3 +126,42 @@ impl KeyLoadErrorSubtype for RSAKeyError {} /// Used to handle errors from the entity client impl KeyLoadErrorSubtype for ApiCallError {} + + +#[cfg(test)] +mod tests { + use crate::crypto::Aes128Key; + use super::*; + use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; + use crate::util::test_utils::generate_random_string; + + #[test] + fn encrypt_data_aes128_roundtrip() { + let randomizer = make_thread_rng_facade(); + + let random_string = generate_random_string::<10>(); + let raw_text = random_string.as_bytes(); + let iv = Iv::generate(&randomizer); + let key: GenericAesKey = Aes128Key::generate(&randomizer).into(); + + let ciphertext = key.encrypt_data(raw_text, iv).unwrap(); + let text = key.decrypt_data(ciphertext.as_slice()).unwrap(); + + assert_eq!(raw_text, text.as_slice()); + } + + #[test] + fn encrypt_data_aes256_roundtrip() { + let randomizer = make_thread_rng_facade(); + + let random_string = generate_random_string::<10>(); + let raw_text = random_string.as_bytes(); + let iv = Iv::generate(&randomizer); + let key: GenericAesKey = Aes256Key::generate(&randomizer).into(); + + let ciphertext = key.encrypt_data(raw_text, iv).unwrap(); + let text = key.decrypt_data(ciphertext.as_slice()).unwrap(); + + assert_eq!(raw_text, text.as_slice()); + } +} \ No newline at end of file diff --git a/tuta-sdk/rust/src/crypto/key_encryption.rs b/tuta-sdk/rust/src/crypto/key_encryption.rs index 938a36b8ff7..32b06af0e9f 100644 --- a/tuta-sdk/rust/src/crypto/key_encryption.rs +++ b/tuta-sdk/rust/src/crypto/key_encryption.rs @@ -81,3 +81,41 @@ fn decrypt_rsa_or_rsa_ecc_key_pair(encryption_key: &GenericAesKey, key_pair: &Ke Ok(AsymmetricKeyPair::RSAKeyPair(rsa_key_pair)) } } + +#[cfg(test)] +mod tests { + use crate::crypto::{Aes256Key, Iv, PQKeyPairs}; + use crate::crypto::ecc::EccKeyPair; + use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey}; + use crate::crypto::key_encryption::decrypt_pq_key_pair; + use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; + use crate::entities::sys::KeyPair; + use crate::util::test_utils::generate_random_string; + + #[test] + fn roundtrip() { + let randomizer = make_thread_rng_facade(); + let pq_key_pair = PQKeyPairs::generate(&randomizer); + let parent_key: GenericAesKey = Aes256Key::generate(&randomizer).into(); + + let junk_ecc_pair = EccKeyPair::generate(&randomizer); + let encrypted_key_pair = KeyPair { + _id: Default::default(), + pubEccKey: Some(pq_key_pair.ecc_keys.public_key.as_bytes().to_vec()), + pubKyberKey: Some(pq_key_pair.kyber_keys.public_key.serialize()), + pubRsaKey: Some(generate_random_string::<17>().as_bytes().to_vec()), + symEncPrivEccKey: Some(parent_key.encrypt_data(junk_ecc_pair.private_key.as_bytes(), Iv::generate(&randomizer)).unwrap()), + symEncPrivKyberKey: Some(parent_key.encrypt_data(&pq_key_pair.kyber_keys.private_key.serialize(), Iv::generate(&randomizer)).unwrap()), + symEncPrivRsaKey: Some(generate_random_string::<17>().as_bytes().to_vec()), + }; + + let decrypted_kyber_key = decrypt_pq_key_pair(&parent_key, &encrypted_key_pair).unwrap(); + + match decrypted_kyber_key { + AsymmetricKeyPair::PQKeyPairs(decrypted_key_pair) => { + assert_eq!(pq_key_pair.kyber_keys.public_key, decrypted_key_pair.kyber_keys.public_key) + } + _ => panic!() + } + } +} diff --git a/tuta-sdk/rust/src/crypto/mod.rs b/tuta-sdk/rust/src/crypto/mod.rs index 2551257a0a0..b83718f42be 100644 --- a/tuta-sdk/rust/src/crypto/mod.rs +++ b/tuta-sdk/rust/src/crypto/mod.rs @@ -22,6 +22,9 @@ mod ecc; mod kyber; mod rsa; mod tuta_crypt; + +pub use tuta_crypt::PQKeyPairs; + pub mod key_encryption; pub mod crypto_facade; pub mod key; diff --git a/tuta-sdk/rust/src/crypto/rsa.rs b/tuta-sdk/rust/src/crypto/rsa.rs index 0e5675befee..dd1175b5cc6 100644 --- a/tuta-sdk/rust/src/crypto/rsa.rs +++ b/tuta-sdk/rust/src/crypto/rsa.rs @@ -7,7 +7,8 @@ use crate::crypto::randomizer_facade::RandomizerFacade; use crate::crypto::ecc::EccKeyPair; use crate::join_slices; -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct RSAPublicKey(rsa::RsaPublicKey); impl RSAPublicKey { @@ -68,7 +69,8 @@ impl RSAPublicKey { } } -#[derive(Clone, ZeroizeOnDrop)] +#[derive(Clone, ZeroizeOnDrop, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct RSAPrivateKey(rsa::RsaPrivateKey); impl RSAPrivateKey { @@ -88,13 +90,15 @@ impl RSAPrivateKey { } } -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct RSAKeyPair { pub public_key: RSAPublicKey, pub private_key: RSAPrivateKey, } -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct RSAEccKeyPair { pub rsa_key_pair: RSAKeyPair, pub ecc_key_pair: EccKeyPair, diff --git a/tuta-sdk/rust/src/crypto/tuta_crypt.rs b/tuta-sdk/rust/src/crypto/tuta_crypt.rs index 15fb769719f..9b7e3e5bf23 100644 --- a/tuta-sdk/rust/src/crypto/tuta_crypt.rs +++ b/tuta-sdk/rust/src/crypto/tuta_crypt.rs @@ -148,7 +148,8 @@ fn derive_pq_kek( Aes256Key::try_from(kek_bytes).unwrap() } -#[derive(Clone)] +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] // only allow Debug in tests because this prints the key! pub struct PQKeyPairs { pub ecc_keys: EccKeyPair, pub kyber_keys: KyberKeyPair, diff --git a/tuta-sdk/rust/src/crypto_entity_client.rs b/tuta-sdk/rust/src/crypto_entity_client.rs index a3f740d61b3..f3249d6b844 100644 --- a/tuta-sdk/rust/src/crypto_entity_client.rs +++ b/tuta-sdk/rust/src/crypto_entity_client.rs @@ -103,6 +103,7 @@ mod tests { use crate::{IdTuple, TypeRef}; use crate::custom_id::CustomId; use crate::generated_id::GeneratedId; + use crate::util::test_utils::leak; #[tokio::test] async fn can_load_mail() { @@ -126,9 +127,7 @@ mod tests { // We cause a deliberate memory leak to convert the mail type's lifetime to static because // the callback to `returning` requires returned references to have a static lifetime - let my_favorite_leak: &'static TypeModelProvider = Box::leak( - Box::new(init_type_model_provider()) - ); + let my_favorite_leak: &'static TypeModelProvider = leak(init_type_model_provider()); let raw_mail_id = encrypted_mail.get("_id").unwrap().assert_tuple_id(); let mail_id = IdTuple::new( diff --git a/tuta-sdk/rust/src/custom_id.rs b/tuta-sdk/rust/src/custom_id.rs index fc4f54942e3..5a236e62f18 100644 --- a/tuta-sdk/rust/src/custom_id.rs +++ b/tuta-sdk/rust/src/custom_id.rs @@ -1,4 +1,5 @@ use std::fmt::{Debug, Display, Formatter}; +use base64::Engine; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error, Visitor}; @@ -12,6 +13,11 @@ impl CustomId { &self.0 } + /// Create a CustomId from an arbitrary (unencoded) string + pub fn from_custom_string(custom_string: &str) -> Self { + Self(base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(custom_string)) + } + /// Generates and returns a random `CustomId` #[cfg(test)] pub fn test_random() -> Self { diff --git a/tuta-sdk/rust/src/entity_client.rs b/tuta-sdk/rust/src/entity_client.rs index 14400df190e..fd490ad73d8 100644 --- a/tuta-sdk/rust/src/entity_client.rs +++ b/tuta-sdk/rust/src/entity_client.rs @@ -14,7 +14,7 @@ use crate::type_model_provider::{TypeModelProvider}; use crate::user_facade::AuthHeadersProvider; /// Denotes an ID that can be serialised into a string -pub trait IdType: Display {} +pub trait IdType: Display + 'static {} impl IdType for String {} diff --git a/tuta-sdk/rust/src/instance_mapper.rs b/tuta-sdk/rust/src/instance_mapper.rs index 550d6fe8a1b..761b541b207 100644 --- a/tuta-sdk/rust/src/instance_mapper.rs +++ b/tuta-sdk/rust/src/instance_mapper.rs @@ -459,13 +459,14 @@ impl ElementValue { #[cfg(test)] mod tests { - use crate::entities::sys::{ArchiveRef, ArchiveType, Group, GroupInfo, TypeInfo}; + use crate::entities::sys::{Group, GroupInfo}; use crate::entities::tutanota::{MailboxGroupRoot, OutOfOfficeNotificationRecipientList}; use crate::json_element::RawEntity; use crate::json_serializer::JsonSerializer; use crate::type_model_provider::init_type_model_provider; use std::sync::Arc; use crate::generated_id::GeneratedId; + use crate::util::test_utils::generate_random_group; use super::*; @@ -530,43 +531,7 @@ mod tests { #[test] fn test_ser_group() { - let group_root = Group { - _format: 0, - _id: GeneratedId::test_random(), - _ownerGroup: None, - _permissions: GeneratedId::test_random(), - groupInfo: IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random()), - administratedGroups: None, - archives: vec![ArchiveType { - _id: CustomId::test_random(), - active: ArchiveRef { - _id: CustomId::test_random(), - archiveId: GeneratedId::test_random(), - }, - inactive: vec![], - r#type: TypeInfo { - _id: CustomId::test_random(), - application: "app".to_string(), - typeId: 1, - }, - }], - currentKeys: None, - customer: None, - formerGroupKeys: None, - invitations: GeneratedId::test_random(), - members: GeneratedId::test_random(), - groupKeyVersion: 1, - admin: None, - r#type: 46, - adminGroupEncGKey: None, - adminGroupKeyVersion: None, - enabled: true, - external: false, - pubAdminGroupEncGKey: Some(vec![1, 2, 3]), - storageCounter: None, - user: None, - - }; + let group_root = generate_random_group(None, None); let mapper = InstanceMapper::new(); let result = mapper.serialize_entity(group_root.clone()).unwrap(); assert_eq!(&ElementValue::Number(0), result.get("_format").unwrap()); diff --git a/tuta-sdk/rust/src/key_cache.rs b/tuta-sdk/rust/src/key_cache.rs index 5ecaff5f5c1..9f9fda88b54 100644 --- a/tuta-sdk/rust/src/key_cache.rs +++ b/tuta-sdk/rust/src/key_cache.rs @@ -11,6 +11,7 @@ pub struct KeyCache { user_group_key_distribution_key: RwLock>, } +#[cfg_attr(test, mockall::automock)] impl KeyCache { pub fn new() -> Self { KeyCache { diff --git a/tuta-sdk/rust/src/key_loader_facade.rs b/tuta-sdk/rust/src/key_loader_facade.rs index bbec81d7057..b595ed22463 100644 --- a/tuta-sdk/rust/src/key_loader_facade.rs +++ b/tuta-sdk/rust/src/key_loader_facade.rs @@ -5,8 +5,11 @@ use crate::crypto::key::{AsymmetricKeyPair, GenericAesKey, KeyLoadError}; use crate::crypto::key_encryption::decrypt_key_pair; use crate::entities::sys::{Group, GroupKey}; use crate::generated_id::GeneratedId; +#[mockall_double::double] use crate::key_cache::KeyCache; +#[mockall_double::double] use crate::typed_entity_client::TypedEntityClient; +#[mockall_double::double] use crate::user_facade::UserFacade; use crate::util::Versioned; @@ -31,15 +34,21 @@ impl KeyLoaderFacade { } pub async fn load_sym_group_key(&self, group_id: &GeneratedId, version: i64, current_group_key: Option) -> Result { - let group_key = match current_group_key.clone() { - Some(n) => n, + let group_key = match current_group_key { + Some(n) => { + let group_key_version = n.version; + if group_key_version < version { + return Err(KeyLoadError { reason: format!("Provided current group key is too old (${group_key_version}) to load the requested version ${version} for group ${group_id}") }); + } + n + } None => self.get_current_sym_group_key(group_id).await? }; return if group_key.version == version { Ok(group_key.object) } else { - let group: Group = self.entity_client.load(&group_id.as_str().to_owned()).await?; + let group: Group = self.entity_client.load(&group_id.to_owned()).await?; let FormerGroupKey { symmetric_group_key, .. } = self.find_former_group_key(&group, &group_key, version).await?; Ok(symmetric_group_key) }; @@ -88,10 +97,12 @@ impl KeyLoaderFacade { Ok(FormerGroupKey { symmetric_group_key: last_group_key, group_key_instance: last_group_key_instance.unwrap() }) } - // TODO: Remove allowance after implementing - #[allow(unused_variables)] fn decode_group_key_version(&self, element_id: &GeneratedId) -> Result { - todo!() + element_id.as_str().parse().map_err(|_| + KeyLoadError { + reason: format!("Failed to decode group key version: {}", element_id) + } + ) } pub async fn get_current_sym_group_key(&self, group_id: &GeneratedId) -> Result { @@ -127,16 +138,16 @@ impl KeyLoaderFacade { self.load_sym_group_key( &self.user_facade.get_user_group_id(), user_group_key_version, - Some(self.user_facade.get_current_user_group_key()?), + Some(self.user_facade.get_current_user_group_key().ok_or_else(|| KeyLoadError { reason: "No use group key loaded".to_string() })?), ).await } fn get_current_sym_user_group_key(&self) -> Option { - self.user_facade.get_current_user_group_key().ok() + self.user_facade.get_current_user_group_key() } pub async fn load_key_pair(&self, key_pair_group_id: &GeneratedId, group_key_version: i64) -> Result { - let group: Group = self.entity_client.load(&key_pair_group_id.to_string()).await?; + let group: Group = self.entity_client.load(key_pair_group_id).await?; let group_key = self.get_current_sym_group_key(&group._id).await?; if group_key.version == group_key_version { @@ -162,4 +173,475 @@ pub type VersionedAesKey = Versioned; struct FormerGroupKey { symmetric_group_key: GenericAesKey, group_key_instance: GroupKey, +} + +#[cfg(test)] +mod tests { + use std::array::from_fn; + use crate::IdTuple; + use crate::crypto::{Aes256Key, Iv, PQKeyPairs}; + use crate::crypto::randomizer_facade::RandomizerFacade; + use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; + use crate::entities::sys::{GroupKeysRef, GroupMembership, KeyPair}; + use crate::key_cache::MockKeyCache; + use crate::typed_entity_client::MockTypedEntityClient; + use crate::user_facade::MockUserFacade; + use super::*; + use crate::util::test_utils::{generate_random_group, random_aes256_key}; + use mockall::{predicate}; + use crate::custom_id::CustomId; + use crate::util::get_vec_reversed; + + fn generate_group_key(version: i64) -> VersionedAesKey { + VersionedAesKey { object: random_aes256_key().into(), version } + } + + fn generate_group_data() -> (Group, VersionedAesKey) { + ( + generate_random_group(None, None), + generate_group_key(1) + ) + } + + fn generate_group_with_keys(current_key_pair: &PQKeyPairs, current_group_key: &VersionedAesKey, randomizer_facade: &RandomizerFacade) -> Group { + let PQKeyPairs { ecc_keys, kyber_keys } = current_key_pair; + let group_key = ¤t_group_key.object; + let sym_enc_priv_ecc_key = group_key.encrypt_data(ecc_keys.private_key.as_bytes(), Iv::generate(randomizer_facade)).unwrap(); + println!("{:?} enc priv ecc key (ggwk) {:?} as {:?}", group_key, ecc_keys.private_key.as_bytes(), sym_enc_priv_ecc_key); + let sync_enc_priv_kyber_key = group_key.encrypt_data(&kyber_keys.private_key.serialize(), Iv::generate(randomizer_facade)).unwrap(); + generate_random_group( + Some( + KeyPair { + _id: Default::default(), + pubEccKey: Some(ecc_keys.public_key.as_bytes().to_vec()), + pubKyberKey: Some(kyber_keys.public_key.serialize()), + pubRsaKey: None, + symEncPrivEccKey: Some(sym_enc_priv_ecc_key), + symEncPrivKyberKey: Some(sync_enc_priv_kyber_key), + symEncPrivRsaKey: None, + } + ), + Some( + GroupKeysRef { + _id: Default::default(), + list: GeneratedId("list".to_owned()), // Refers to `former_keys` + } + ), + ) + } + + + const FORMER_KEYS: usize = 2; + + /// Returns `(former_keys, former_key_pairs_decrypted, former_keys_decrypted)` + fn generate_former_keys(current_group_key: &VersionedAesKey, randomizer_facade: &RandomizerFacade) -> ([GroupKey; FORMER_KEYS], [PQKeyPairs; FORMER_KEYS], [Aes256Key; FORMER_KEYS]) { + // Using `from_fn` has the same performance as using mutable vecs but less memory usage + let former_keys_decrypted: [Aes256Key; FORMER_KEYS] = from_fn(|_| { + random_aes256_key() + }); + let former_key_pairs_decrypted: [PQKeyPairs; FORMER_KEYS] = from_fn(|_| { + PQKeyPairs::generate(&make_thread_rng_facade()) + }); + + let mut former_keys = Vec::with_capacity(FORMER_KEYS); + let mut last_key = current_group_key.object.clone(); + + for (i, current_key) in former_keys_decrypted.iter().enumerate().rev() { + let pq_key_pair = &former_key_pairs_decrypted[i]; + // Get the previous key to use as the owner key + let current_key: &GenericAesKey = ¤t_key.clone().into(); + + let owner_enc_g_key = last_key.encrypt_key(current_key, Iv::generate(randomizer_facade)).as_slice().to_vec(); + let sym_enc_priv_ecc_key = current_key.encrypt_data( + &pq_key_pair.ecc_keys.private_key.clone().as_bytes().to_vec(), + Iv::generate(randomizer_facade)).unwrap(); + + former_keys.insert(0, GroupKey { + _format: 0, + _id: IdTuple { + list_id: GeneratedId("list".to_owned()), + element_id: GeneratedId(i.to_string()), + }, + _ownerGroup: None, + _permissions: Default::default(), + adminGroupEncGKey: None, + adminGroupKeyVersion: None, + ownerEncGKey: owner_enc_g_key, + ownerKeyVersion: 0, + pubAdminGroupEncGKey: None, + keyPair: Some(KeyPair { + _id: Default::default(), + pubEccKey: Some(pq_key_pair.ecc_keys.public_key.as_bytes().to_vec()), + pubKyberKey: Some(pq_key_pair.kyber_keys.public_key.serialize()), + pubRsaKey: None, + symEncPrivEccKey: Some(sym_enc_priv_ecc_key + ), + symEncPrivKyberKey: Some(current_key.encrypt_data( + pq_key_pair.kyber_keys.private_key.serialize().as_slice(), + Iv::generate(randomizer_facade)).unwrap() + ), + symEncPrivRsaKey: None, + }), + }); + last_key = current_key.clone().into(); + } + + (former_keys.try_into().unwrap_or_else(|_| panic!()), former_key_pairs_decrypted, former_keys_decrypted) + } + + fn get_mocks_for_former_tests(group: &Group, current_group_key: &VersionedAesKey, former_keys: &[GroupKey; FORMER_KEYS], randomizer: &RandomizerFacade) -> KeyLoaderFacade { + let user_group_key = generate_group_key(0); + let user_group = generate_random_group(None, None); + + let mut key_cache_mock = MockKeyCache::default(); + { + let current_group_key = current_group_key.clone(); + key_cache_mock.expect_get_current_group_key().returning(move |_| Some(current_group_key.clone())); + } + key_cache_mock.expect_put_group_key().return_const(()); + + let mut user_facade_mock = MockUserFacade::default(); + { + let user_group_key = user_group_key.clone(); + user_facade_mock.expect_get_current_user_group_key() + .returning(move || Some(user_group_key.clone())); + } + { + let user_group_id = user_group._id.clone(); + let sym_enc_g_key = user_group_key.object.encrypt_key( + ¤t_group_key.object, + Iv::generate(&randomizer), + ); + let current_group_key = current_group_key.clone(); + user_facade_mock.expect_get_membership() + .with(predicate::eq(user_group_id.clone())) + .returning(move |_| Ok(GroupMembership { + _id: CustomId(user_group_id.clone().to_string()), + admin: false, + capability: None, + groupKeyVersion: current_group_key.clone().version, + groupType: None, + symEncGKey: sym_enc_g_key.clone(), + symKeyVersion: user_group_key.version, + group: user_group_id.clone(), + groupInfo: IdTuple { list_id: Default::default(), element_id: Default::default() }, + groupMember: IdTuple { list_id: Default::default(), element_id: Default::default() }, + })); + } + { + let user_group_id = user_group._id.clone(); + user_facade_mock.expect_get_user_group_id().returning(move || user_group_id.clone()); + } + + let mut typed_entity_client_mock = MockTypedEntityClient::default(); + { + let group = group.clone(); + typed_entity_client_mock.expect_load::() + .with(predicate::eq(group._id.clone())) + .returning(move |_| Ok(group.clone())); + } + { + for i in 0..FORMER_KEYS { + let group = group.clone(); + let former_keys = former_keys.clone(); + + let returned_keys = get_vec_reversed(former_keys[i..].to_vec()); + typed_entity_client_mock.expect_load_range::() + .with( + predicate::eq(group.formerGroupKeys.unwrap().list), + predicate::eq(GeneratedId( + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(current_group_key.version.to_string()) + )), + predicate::eq(FORMER_KEYS - i), + predicate::eq(true), + ) + .returning(move |_, _, _, _| Ok(returned_keys.clone())) + .times(1); + } + } + KeyLoaderFacade::new( + Arc::new(key_cache_mock), + Arc::new(user_facade_mock), + Arc::new(typed_entity_client_mock), + ) + } + + #[tokio::test] + async fn get_user_group_key() { + let (user_group, user_group_key) = generate_group_data(); + + let mut key_cache_mock = MockKeyCache::default(); + { + let user_group_key = user_group_key.clone(); + key_cache_mock.expect_get_current_user_group_key().returning(move || Some(user_group_key.clone())); + } + key_cache_mock.expect_put_group_key().return_const(()); + + let mut user_facade_mock = MockUserFacade::default(); + { + let user_group = user_group.clone(); + user_facade_mock.expect_get_user_group_id().returning(move || user_group._id.clone()); + } + { + let user_group_key = user_group_key.clone(); + user_facade_mock.expect_get_current_user_group_key() + .returning(move || Some(user_group_key.clone())) + .times(2); + } + + let typed_entity_client_mock = MockTypedEntityClient::default(); + + let key_loader_facade = KeyLoaderFacade::new(Arc::new(key_cache_mock), Arc::new(user_facade_mock), Arc::new(typed_entity_client_mock)); + + let current_user_group_key = key_loader_facade.get_current_sym_group_key(&user_group._id).await.unwrap(); + assert_eq!(current_user_group_key.version, user_group.groupKeyVersion); + assert_eq!(current_user_group_key.object, user_group_key.object); + + let _ = key_loader_facade.get_current_sym_group_key(&user_group._id).await;// should not be cached + } + + #[tokio::test] + async fn get_non_user_group_key() { + let (group, current_group_key) = generate_group_data(); + + let mut key_cache_mock = MockKeyCache::default(); + { + let user_group_key = current_group_key.clone(); + key_cache_mock.expect_get_current_user_group_key().returning(move || Some(user_group_key.clone())); + } + key_cache_mock.expect_put_group_key().return_const(()); + + let mut user_facade_mock = MockUserFacade::default(); + { + let user_group = group.clone(); + user_facade_mock.expect_get_user_group_id().returning(move || user_group._id.clone()); + } + { + let user_group_key = current_group_key.clone(); + user_facade_mock.expect_get_current_user_group_key() + .returning(move || Some(user_group_key.clone())); + } + + let typed_entity_client_mock = MockTypedEntityClient::default(); + + let key_loader_facade = KeyLoaderFacade::new(Arc::new(key_cache_mock), Arc::new(user_facade_mock), Arc::new(typed_entity_client_mock)); + + let group_key = key_loader_facade.get_current_sym_group_key(&group._id).await.unwrap(); + assert_eq!(group_key.version, group.groupKeyVersion); + assert_eq!(group_key.object, current_group_key.object) + } + + #[tokio::test] + async fn load_former_key_pairs() { + let randomizer = make_thread_rng_facade(); + + // Same as the length of former_keys_deprecated + let current_group_key_version = FORMER_KEYS as i64; + let current_group_key = generate_group_key(current_group_key_version); + let current_key_pair = PQKeyPairs::generate(&randomizer); + + let group = generate_group_with_keys(¤t_key_pair, ¤t_group_key, &randomizer); + + let (former_keys, former_key_pairs_decrypted, _) = generate_former_keys(¤t_group_key, &randomizer); + + let key_loader_facade = get_mocks_for_former_tests(&group, ¤t_group_key, &former_keys, &randomizer); + + for i in 0..FORMER_KEYS { + let keypair = key_loader_facade.load_key_pair(&group._id, i as i64).await.unwrap(); + match keypair { + AsymmetricKeyPair::RSAKeyPair(_) => panic!("key_loader_facade.load_key_pair() returned an RSAKeyPair! Expected PQKeyPairs."), + AsymmetricKeyPair::RsaEccKeyPair(_) => panic!("key_loader_facade.load_key_pair() returned an RSAEccKeyPair! Expected PQKeyPairs."), + AsymmetricKeyPair::PQKeyPairs(pq_key_pair) => { + assert_eq!(pq_key_pair, former_key_pairs_decrypted[i]) + } + } + } + } + + #[tokio::test] + async fn load_current_key_pair() { + let user_group_key = generate_group_key(1); + let randomizer = make_thread_rng_facade(); + let current_key_pair = PQKeyPairs::generate(&randomizer); + let user_group = generate_group_with_keys( + ¤t_key_pair, + &user_group_key, + &randomizer, + ); + + let mut key_cache_mock = MockKeyCache::default(); + { + let user_group_key = user_group_key.clone(); + key_cache_mock.expect_get_current_user_group_key().returning(move || Some(user_group_key.clone())); + } + key_cache_mock.expect_put_group_key().return_const(()); + + let mut user_facade_mock = MockUserFacade::default(); + { + let user_group_id = user_group._id.clone(); + user_facade_mock.expect_get_user_group_id().returning(move || user_group_id.clone()); + } + { + let user_group_key = user_group_key.clone(); + user_facade_mock.expect_get_current_user_group_key() + .returning(move || Some(user_group_key.clone())); + } + + let mut typed_entity_client_mock = MockTypedEntityClient::default(); + { + let user_group = user_group.clone(); + let group_id = user_group._id.clone(); + typed_entity_client_mock.expect_load::() + .withf(move |id| *id == group_id.clone()) + .returning(move |_| Ok(user_group.clone())); + } + + let key_loader_facade = KeyLoaderFacade::new(Arc::new(key_cache_mock), Arc::new(user_facade_mock), Arc::new(typed_entity_client_mock)); + + let loaded_current_key_pair = key_loader_facade.load_key_pair(&user_group._id, user_group.groupKeyVersion) + .await + .unwrap(); + + match loaded_current_key_pair { + AsymmetricKeyPair::RSAKeyPair(_) => panic!("Expected PQ key pair!"), + AsymmetricKeyPair::RsaEccKeyPair(_) => panic!("Expected PQ key pair!"), + AsymmetricKeyPair::PQKeyPairs(loaded_current_key_pair) => { + assert_eq!(loaded_current_key_pair, current_key_pair); + } + } + } + + #[tokio::test] + async fn load_and_decrypt_former_group_key() { + let randomizer = make_thread_rng_facade(); + + // Same as the length of former_keys_deprecated + let current_group_key_version = FORMER_KEYS as i64; + let current_group_key = generate_group_key(current_group_key_version); + let (former_keys, _, former_keys_decrypted) = generate_former_keys(¤t_group_key, &randomizer); + + let current_key_pair = PQKeyPairs::generate(&randomizer); + let group = generate_group_with_keys(¤t_key_pair, ¤t_group_key, &randomizer); + + let key_loader_facade = get_mocks_for_former_tests(&group, ¤t_group_key, &former_keys, &randomizer); + for i in 0..FORMER_KEYS { + let keypair = key_loader_facade.load_sym_group_key(&group._id, i as i64, None).await.unwrap(); + match keypair { + GenericAesKey::Aes128(_) => panic!("key_loader_facade.load_sym_group_key() returned an AES128 key! Expected an AES256 key."), + GenericAesKey::Aes256(returned_group_key) => { + assert_eq!(returned_group_key, former_keys_decrypted[i]) + } + } + } + } + + #[tokio::test] + async fn load_and_decrypt_current_group_key() { + let randomizer = make_thread_rng_facade(); + + // Same as the length of former_keys_deprecated + let current_group_key_version = FORMER_KEYS as i64; + let current_group_key = generate_group_key(current_group_key_version); + + let current_key_pair = PQKeyPairs::generate(&randomizer); + let group = generate_group_with_keys(¤t_key_pair, ¤t_group_key, &randomizer); + + + let user_group_key = generate_group_key(0); + let user_group = generate_random_group(None, None); + + let mut key_cache_mock = MockKeyCache::default(); + { + let current_group_key = current_group_key.clone(); + key_cache_mock.expect_get_current_group_key().returning(move |_| Some(current_group_key.clone())); + } + key_cache_mock.expect_put_group_key().return_const(()); + + let mut user_facade_mock = MockUserFacade::default(); + { + let user_group_key = user_group_key.clone(); + user_facade_mock.expect_get_current_user_group_key() + .returning(move || Some(user_group_key.clone())); + } + { + let user_group_id = user_group._id.clone(); + let sym_enc_g_key = user_group_key.object.encrypt_key( + ¤t_group_key.object, + Iv::generate(&randomizer), + ); + let current_group_key = current_group_key.clone(); + user_facade_mock.expect_get_membership() + .with(predicate::eq(user_group_id.clone())) + .returning(move |_| Ok(GroupMembership { + _id: CustomId(user_group_id.clone().to_string()), + admin: false, + capability: None, + groupKeyVersion: current_group_key.clone().version, + groupType: None, + symEncGKey: sym_enc_g_key.clone(), + symKeyVersion: user_group_key.version, + group: user_group_id.clone(), + groupInfo: IdTuple { list_id: Default::default(), element_id: Default::default() }, + groupMember: IdTuple { list_id: Default::default(), element_id: Default::default() }, + })); + } + { + let user_group_id = user_group._id.clone(); + user_facade_mock.expect_get_user_group_id().returning(move || user_group_id.clone()); + } + + let mut typed_entity_client_mock = MockTypedEntityClient::default(); + { + let group = group.clone(); + typed_entity_client_mock.expect_load::() + .with(predicate::eq(group._id.clone())) + .returning(move |_| Ok(group.clone())); + } + + let key_loader_facade = KeyLoaderFacade::new( + Arc::new(key_cache_mock), + Arc::new(user_facade_mock), + Arc::new(typed_entity_client_mock), + ); + + let returned_key = key_loader_facade.load_sym_group_key(&group._id, current_group_key_version, None).await.unwrap(); + + assert_eq!(returned_key, current_group_key.object) + } + + #[tokio::test] + async fn outdated_current_group_key_errors() { + let randomizer = make_thread_rng_facade(); + + // Same as the length of former_keys_deprecated + let current_group_key_version = FORMER_KEYS as i64; + let current_group_key = generate_group_key(current_group_key_version); + + let current_key_pair = PQKeyPairs::generate(&randomizer); + let group = generate_group_with_keys(¤t_key_pair, ¤t_group_key, &randomizer); + + let (_, _, former_keys_decrypted) = generate_former_keys(¤t_group_key, &randomizer); + + let outdated_current_group_key_version = current_group_key_version - 1; + let outdated_current_group_key = VersionedAesKey { + object: former_keys_decrypted[outdated_current_group_key_version as usize].clone().into(), + version: outdated_current_group_key_version, + }; + + let key_cache_mock = MockKeyCache::default(); + let user_facade_mock = MockUserFacade::default(); + let typed_entity_client_mock = MockTypedEntityClient::default(); + + let key_loader_facade = KeyLoaderFacade::new( + Arc::new(key_cache_mock), + Arc::new(user_facade_mock), + Arc::new(typed_entity_client_mock), + ); + + key_loader_facade.load_sym_group_key( + &group._id, + current_group_key_version, + Some(outdated_current_group_key), + ).await.expect_err("Did not error with outdated group key"); + } } \ No newline at end of file diff --git a/tuta-sdk/rust/src/typed_entity_client.rs b/tuta-sdk/rust/src/typed_entity_client.rs index d53214d351a..5c2686a1a81 100644 --- a/tuta-sdk/rust/src/typed_entity_client.rs +++ b/tuta-sdk/rust/src/typed_entity_client.rs @@ -12,6 +12,7 @@ pub struct TypedEntityClient { } /// Similar to EntityClient, but return a typed object instead of a generic Map +#[cfg_attr(test, mockall::automock)] impl TypedEntityClient { pub(crate) fn new( entity_client: Arc, diff --git a/tuta-sdk/rust/src/user_facade.rs b/tuta-sdk/rust/src/user_facade.rs index db4422b57ad..38a239a2ab1 100644 --- a/tuta-sdk/rust/src/user_facade.rs +++ b/tuta-sdk/rust/src/user_facade.rs @@ -114,9 +114,8 @@ impl UserFacade { groups } - pub fn get_current_user_group_key(&self) -> Result { + pub fn get_current_user_group_key(&self) -> Option { self.key_cache.get_current_user_group_key() - .ok_or_else(|| ApiCallError::InternalSdkError { error_message: "userGroupKey not available".to_owned() }) } pub(crate) fn get_membership(&self, group_id: &GeneratedId) -> Result { @@ -138,3 +137,22 @@ impl AuthHeadersProvider for UserFacade { todo!() } } + +#[cfg(test)] +mockall::mock!( + pub UserFacade { + pub fn set_user(&mut self, user: User); + pub fn unlock_user_group_key(&mut self, user_passphrase_key: GenericAesKey) + -> Result<(), ApiCallError>; + pub async fn update_user(&self, user: User); + pub fn get_user(&self) -> Arc; + pub fn get_user_group_id(&self) -> GeneratedId; + pub fn get_current_user_group_key(&self) -> Option; + pub(crate) fn get_membership(&self, group_id: &GeneratedId) + -> Result; + } + impl AuthHeadersProvider for UserFacade { + fn create_auth_headers(&self, model_version: u32) -> HashMap; + fn is_fully_logged_in(&self) -> bool; + } +); \ No newline at end of file diff --git a/tuta-sdk/rust/src/util/mod.rs b/tuta-sdk/rust/src/util/mod.rs index 92f2af26b07..b00f31be6c9 100644 --- a/tuta-sdk/rust/src/util/mod.rs +++ b/tuta-sdk/rust/src/util/mod.rs @@ -140,6 +140,13 @@ pub fn array_cast_size(arr: [u8; ARR_S } } +/// A functional style wrapper around `vec.reverse()` +pub fn get_vec_reversed(vec: Vec) -> Vec { + let mut copy = vec.clone(); + copy.reverse(); + copy +} + #[cfg(test)] mod test { use super::*; diff --git a/tuta-sdk/rust/src/util/test_utils.rs b/tuta-sdk/rust/src/util/test_utils.rs index 64b4b942d2f..72e1bde855a 100644 --- a/tuta-sdk/rust/src/util/test_utils.rs +++ b/tuta-sdk/rust/src/util/test_utils.rs @@ -1,5 +1,7 @@ //! General purpose functions for testing various objects +use rand::random; +use crate::crypto::Aes256Key; use crate::crypto::randomizer_facade::test_util::make_thread_rng_facade; use crate::custom_id::CustomId; use crate::element_value::{ElementValue, ParsedEntity}; @@ -9,6 +11,7 @@ use crate::IdTuple; use crate::instance_mapper::InstanceMapper; use crate::metamodel::{AssociationType, Cardinality, ElementType, ValueType}; use crate::type_model_provider::{init_type_model_provider, TypeModelProvider}; +use crate::entities::sys::{ArchiveRef, ArchiveType, Group, GroupKeysRef, KeyPair, TypeInfo}; /// Generates a URL-safe random string of length `Size`. #[must_use] @@ -18,6 +21,55 @@ pub fn generate_random_string() -> String { base64::engine::general_purpose::URL_SAFE.encode(random_bytes) } +pub fn generate_random_group(current_keys: Option, former_keys: Option) -> Group { + Group { + _format: 0, + _id: GeneratedId::test_random(), + _ownerGroup: None, + _permissions: GeneratedId::test_random(), + groupInfo: IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random()), + administratedGroups: None, + archives: vec![ArchiveType { + _id: CustomId::test_random(), + active: ArchiveRef { + _id: CustomId::test_random(), + archiveId: GeneratedId::test_random(), + }, + inactive: vec![], + r#type: TypeInfo { + _id: CustomId::test_random(), + application: "app".to_string(), + typeId: 1, + }, + }], + currentKeys: current_keys, + customer: None, + formerGroupKeys: former_keys, + invitations: GeneratedId::test_random(), + members: GeneratedId::test_random(), + groupKeyVersion: 1, + admin: None, + r#type: 46, + adminGroupEncGKey: None, + adminGroupKeyVersion: None, + enabled: true, + external: false, + pubAdminGroupEncGKey: Some(vec![1, 2, 3]), + storageCounter: None, + user: None, + } +} + +pub fn random_aes256_key() -> Aes256Key { + Aes256Key::from_bytes(&random::<[u8; 32]>()).unwrap() +} + +/// Moves the object T into heap and leaks it. +#[inline(always)] +pub fn leak(what: T) -> &'static T { + Box::leak(Box::new(what)) +} + /// Generate a test entity. /// /// The values will be set to these defaults: @@ -46,7 +98,7 @@ pub fn create_test_entity<'a, T: Entity + serde::Deserialize<'a>>() -> T { let entity = create_test_entity_dict(&provider, type_ref.app, type_ref.type_); match mapper.parse_entity(entity) { Ok(n) => n, - Err(e) => panic!("Failed to create test entity {app}/{type_}: parse error {e}", app=type_ref.app, type_=type_ref.type_) + Err(e) => panic!("Failed to create test entity {app}/{type_}: parse error {e}", app = type_ref.app, type_ = type_ref.type_) } } @@ -57,7 +109,7 @@ fn create_test_entity_dict(provider: &TypeModelProvider, app: &str, type_: &str) let mut object = ParsedEntity::new(); for (&name, value) in &model.values { - let element_value = match value.cardinality { + let element_value = match value.cardinality { Cardinality::ZeroOrOne => ElementValue::Null, Cardinality::Any => ElementValue::Array(Vec::new()), Cardinality::One => { @@ -70,11 +122,10 @@ fn create_test_entity_dict(provider: &TypeModelProvider, app: &str, type_: &str) ValueType::GeneratedId => { if name == "_id" && (model.element_type == ElementType::ListElement || model.element_type == ElementType::BlobElement) { ElementValue::IdTupleId(IdTuple::new(GeneratedId::test_random(), GeneratedId::test_random())) - } - else { + } else { ElementValue::IdGeneratedId(GeneratedId::test_random()) } - }, + } ValueType::CustomId => ElementValue::IdCustomId(CustomId::test_random()), ValueType::CompressedString => todo!("Failed to create test entity {app}/{type_}: Compressed strings ({name}) are not yet supported!"), } @@ -85,7 +136,7 @@ fn create_test_entity_dict(provider: &TypeModelProvider, app: &str, type_: &str) } for (&name, value) in &model.associations { - let association_value = match value.cardinality { + let association_value = match value.cardinality { Cardinality::ZeroOrOne => ElementValue::Null, Cardinality::Any => ElementValue::Array(Vec::new()), Cardinality::One => {