Skip to content

Commit

Permalink
fix opening mail preview from notification on mobile after key rotation,
Browse files Browse the repository at this point in the history
#tutadb1913

We did not set the owner key version properly on an instance when decrypting it. This was fine for owner key version 0 which is the default fallback but it does not work after a key rotation. We now set this properly, so that we can preview the mail without loading it from the server again when opening the app by tapping the notification.
  • Loading branch information
vaf-hub authored and charlag committed Nov 26, 2024
1 parent 0400c53 commit 9043a51
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 67 deletions.
3 changes: 1 addition & 2 deletions app-android/.run/app-debug-emulator.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
<option name="ALWAYS_INSTALL_WITH_PM" value="false" />
<option name="CLEAR_APP_STORAGE" value="false" />
<option name="DYNAMIC_FEATURES_DISABLED_LIST" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="" />
<option name="ACTIVITY_EXTRA_FLAGS" value="-dns-server 127.0.0.1" />
<option name="MODE" value="default_activity" />
<option name="CLEAR_LOGCAT" value="true" />
<option name="SHOW_LOGCAT_AUTOMATICALLY" value="true" />
<option name="INSPECTION_WITHOUT_ACTIVITY_RESTART" value="false" />
<option name="TARGET_SELECTION_MODE" value="DEVICE_AND_SNAPSHOT_COMBO_BOX" />
<option name="SELECTED_CLOUD_MATRIX_CONFIGURATION_ID" value="-1" />
<option name="SELECTED_CLOUD_MATRIX_PROJECT_ID" value="" />
Expand Down
26 changes: 11 additions & 15 deletions tuta-sdk/rust/sdk/src/crypto/crypto_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use crate::crypto::rsa::RSAEncryptionError;
use crate::crypto::tuta_crypt::PQError;
use crate::crypto::Aes256Key;
use crate::element_value::{ElementValue, ParsedEntity};
use crate::entities::entity_facade::{
BUCKET_KEY_FIELD, ID_FIELD, OWNER_ENC_SESSION_KEY_FIELD, OWNER_GROUP_FIELD,
OWNER_KEY_VERSION_FIELD,
};
use crate::entities::generated::sys::BucketKey;
use crate::instance_mapper::InstanceMapper;
#[cfg_attr(test, mockall_double::double)]
Expand All @@ -21,18 +25,6 @@ use base64::prelude::BASE64_URL_SAFE_NO_PAD;
use base64::Engine;
use std::sync::Arc;

/// The name of the field that contains the session key encrypted
/// by the owner group's key in an entity
const OWNER_ENC_SESSION_FIELD: &str = "_ownerEncSessionKey";
/// The name of the owner-encrypted session key version field in an entity
const OWNER_KEY_VERSION_FIELD: &str = "_ownerKeyVersion";
/// The name of the owner group field in an entity
const OWNER_GROUP_FIELD: &str = "_ownerGroup";
/// The name of the ID field in an entity
const ID_FIELD: &str = "_id";
/// The name of the bucket key field in an entity
const BUCKET_KEY_FIELD: &str = "bucketKey";

#[derive(uniffi::Object)]
pub struct CryptoFacade {
key_loader_facade: Arc<KeyLoaderFacade>,
Expand All @@ -47,6 +39,7 @@ pub struct CryptoFacade {
pub struct ResolvedSessionKey {
pub session_key: GenericAesKey,
pub owner_enc_session_key: Vec<u8>,
pub owner_key_version: i64,
}

#[cfg_attr(test, mockall::automock)]
Expand Down Expand Up @@ -115,6 +108,7 @@ impl CryptoFacade {
Ok(Some(ResolvedSessionKey {
session_key,
owner_enc_session_key: owner_enc_session_key.clone(),
owner_key_version,
}))
}

Expand Down Expand Up @@ -196,17 +190,18 @@ impl CryptoFacade {
};

// TODO: authenticate
let versioned_key = self
let versioned_owner_group_key = self
.key_loader_facade
.get_current_sym_group_key(owner_group)
.await?;

let owner_enc_session_key = versioned_key
let owner_enc_session_key = versioned_owner_group_key
.object
.encrypt_key(&session_key, Iv::generate(&self.randomizer_facade));
Ok(ResolvedSessionKey {
session_key,
owner_enc_session_key,
owner_key_version: versioned_owner_group_key.version,
})
}
}
Expand Down Expand Up @@ -252,7 +247,8 @@ impl<'a> EntityOwnerKeyData<'a> {
};
}

let owner_enc_session_key = get_nullable_field!(entity, OWNER_ENC_SESSION_FIELD, Bytes)?;
let owner_enc_session_key =
get_nullable_field!(entity, OWNER_ENC_SESSION_KEY_FIELD, Bytes)?;
let owner_key_version =
get_nullable_field!(entity, OWNER_KEY_VERSION_FIELD, Number)?.copied();
let owner_group = get_nullable_field!(entity, OWNER_GROUP_FIELD, IdGeneratedId)?;
Expand Down
9 changes: 5 additions & 4 deletions tuta-sdk/rust/sdk/src/crypto_entity_client.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#[cfg_attr(test, mockall_double::double)]
use crate::crypto::crypto_facade::CryptoFacade;
use crate::element_value::ParsedEntity;
use crate::entities::entity_facade::EntityFacade;
use crate::entities::entity_facade::{EntityFacade, ID_FIELD};
use crate::entities::Entity;
#[cfg_attr(test, mockall_double::double)]
use crate::entity_client::EntityClient;
Expand Down Expand Up @@ -74,7 +74,7 @@ impl CryptoEntityClient {
.resolve_session_key(&mut parsed_entity, type_model)
.await
.map_err(|error| {
let id = parsed_entity.get("_id");
let id = parsed_entity.get(ID_FIELD);
ApiCallError::InternalSdkError {
error_message: format!(
"Failed to resolve session key for entity '{}' with ID: {:?}; {}",
Expand Down Expand Up @@ -153,7 +153,7 @@ mod tests {
use crate::crypto::{aes::Iv, Aes256Key};
use crate::crypto_entity_client::CryptoEntityClient;
use crate::date::DateTime;
use crate::entities::entity_facade::EntityFacadeImpl;
use crate::entities::entity_facade::{EntityFacadeImpl, ID_FIELD};
use crate::entities::generated::tutanota::Mail;
use crate::entity_client::MockEntityClient;
use crate::instance_mapper::InstanceMapper;
Expand Down Expand Up @@ -188,7 +188,7 @@ mod tests {
let my_favorite_leak: &'static TypeModelProvider = leak(init_type_model_provider());

let raw_mail_id = encrypted_mail
.get("_id")
.get(ID_FIELD)
.unwrap()
.assert_tuple_id_generated();
let mail_id =
Expand Down Expand Up @@ -218,6 +218,7 @@ mod tests {
Ok(Some(ResolvedSessionKey {
session_key: sk.clone(),
owner_enc_session_key: vec![1, 2, 3],
owner_key_version: 0i64,
}))
});

Expand Down
68 changes: 49 additions & 19 deletions tuta-sdk/rust/sdk/src/entities/entity_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ use std::borrow::Borrow;
use std::collections::HashMap;
use std::sync::Arc;

/// The name of the field that contains the session key encrypted
/// by the owner group's key in an entity
pub const OWNER_ENC_SESSION_KEY_FIELD: &str = "_ownerEncSessionKey";
/// The name of the owner-encrypted session key version field in an entity
pub const OWNER_KEY_VERSION_FIELD: &str = "_ownerKeyVersion";
/// The name of the owner group field in an entity
pub const OWNER_GROUP_FIELD: &str = "_ownerGroup";
/// The name of the ID field in an entity
pub const ID_FIELD: &str = "_id";
/// The name of the permissions field in an entity
pub const PERMISSIONS_FIELD: &str = "_permissions";
/// The name of the format field in an entity
pub const FORMAT_FIELD: &str = "_format";
/// The name of the bucket key field in an entity
pub const BUCKET_KEY_FIELD: &str = "bucketKey";
pub const MAX_UNCOMPRESSED_INPUT_LZ4: usize = 0x7e000000;

/// Provides high level functions to handle encryption/decryption of entities
Expand Down Expand Up @@ -187,11 +202,11 @@ impl EntityFacadeImpl {
encrypted.insert(key.to_string(), encrypted_value);
}

if type_model.element_type == ElementType::Aggregated && !encrypted.contains_key("_id") {
if type_model.element_type == ElementType::Aggregated && !encrypted.contains_key(ID_FIELD) {
let new_id = self.randomizer_facade.generate_random_array::<4>();

encrypted.insert(
String::from("_id"),
String::from(ID_FIELD),
ElementValue::String(BASE64_URL_SAFE_NO_PAD.encode(BASE64_STANDARD.encode(new_id))),
);
}
Expand Down Expand Up @@ -600,9 +615,13 @@ impl EntityFacade for EntityFacadeImpl {
let mut mapped_decrypted =
self.decrypt_and_map_inner(type_model, entity, &resolved_session_key.session_key)?;
mapped_decrypted.insert(
"_ownerEncSessionKey".to_owned(),
OWNER_ENC_SESSION_KEY_FIELD.to_owned(),
ElementValue::Bytes(resolved_session_key.owner_enc_session_key.clone()),
);
mapped_decrypted.insert(
OWNER_KEY_VERSION_FIELD.to_owned(),
ElementValue::Number(resolved_session_key.owner_key_version),
);
Ok(mapped_decrypted)
}

Expand Down Expand Up @@ -701,7 +720,9 @@ mod tests {
use crate::date::DateTime;
use crate::element_value::{ElementValue, ParsedEntity};
use crate::entities::entity_facade::{
EntityFacade, EntityFacadeImpl, MappedValue, MAX_UNCOMPRESSED_INPUT_LZ4,
EntityFacade, EntityFacadeImpl, MappedValue, BUCKET_KEY_FIELD, FORMAT_FIELD, ID_FIELD,
MAX_UNCOMPRESSED_INPUT_LZ4, OWNER_ENC_SESSION_KEY_FIELD, OWNER_GROUP_FIELD,
OWNER_KEY_VERSION_FIELD, PERMISSIONS_FIELD,
};
use crate::entities::generated::sys::CustomerAccountTerminationRequest;
use crate::entities::generated::tutanota::Mail;
Expand Down Expand Up @@ -737,6 +758,7 @@ mod tests {
fn test_decrypt_mail() {
let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap());
let owner_enc_session_key = vec![0, 1, 2];
let owner_key_version = 0i64;
let type_model_provider = Arc::new(init_type_model_provider());
let raw_entity: RawEntity = make_json_entity();
let json_serializer = JsonSerializer::new(type_model_provider.clone());
Expand All @@ -760,6 +782,7 @@ mod tests {
ResolvedSessionKey {
session_key: sk,
owner_enc_session_key,
owner_key_version,
},
)
.unwrap();
Expand Down Expand Up @@ -1063,6 +1086,7 @@ mod tests {
fn encrypt_instance() {
let sk = GenericAesKey::Aes256(Aes256Key::from_bytes(KNOWN_SK.as_slice()).unwrap());
let owner_enc_session_key = [0, 1, 2];
let owner_key_version = 0i64;

let deterministic_rng = DeterministicRng(20);
let iv = Iv::generate(&RandomizerFacade::from_core(deterministic_rng.clone()));
Expand Down Expand Up @@ -1120,6 +1144,7 @@ mod tests {
ResolvedSessionKey {
session_key: sk.clone(),
owner_enc_session_key: owner_enc_session_key.to_vec(),
owner_key_version,
},
)
.unwrap();
Expand All @@ -1130,9 +1155,14 @@ mod tests {

assert_eq!(
Some(&ElementValue::Bytes(owner_enc_session_key.to_vec())),
decrypted_mail.get("_ownerEncSessionKey"),
decrypted_mail.get(OWNER_ENC_SESSION_KEY_FIELD),
);
decrypted_mail.insert(OWNER_ENC_SESSION_KEY_FIELD.to_string(), ElementValue::Null);
assert_eq!(
Some(&ElementValue::Number(owner_key_version)),
decrypted_mail.get(OWNER_KEY_VERSION_FIELD),
);
decrypted_mail.insert("_ownerEncSessionKey".to_string(), ElementValue::Null);
decrypted_mail.insert(OWNER_KEY_VERSION_FIELD.to_string(), ElementValue::Null);

assert_eq!(
Some(ElementValue::Dict(HashMap::new())),
Expand Down Expand Up @@ -1164,10 +1194,10 @@ mod tests {

let dummy_date = DateTime::from_system_time(SystemTime::now());
let instance: RawEntity = collection! {
"_format" => JsonElement::String("0".to_string()),
"_id" => JsonElement::Array(vec![JsonElement::String("O1RT2Dj--3-0".to_string()); 2]),
"_ownerGroup" => JsonElement::Null,
"_permissions" => JsonElement::String("O2TT2Aj--2-1".to_string()),
FORMAT_FIELD => JsonElement::String("0".to_string()),
ID_FIELD => JsonElement::Array(vec![JsonElement::String("O1RT2Dj--3-0".to_string()); 2]),
OWNER_GROUP_FIELD => JsonElement::Null,
PERMISSIONS_FIELD => JsonElement::String("O2TT2Aj--2-1".to_string()),
"terminationDate" => JsonElement::String(dummy_date.as_millis().to_string()),
"terminationRequestDate" => JsonElement::String(dummy_date.as_millis().to_string()),
"customer" => JsonElement::String("customId".to_string()),
Expand Down Expand Up @@ -1316,13 +1346,13 @@ mod tests {
fn make_json_entity() -> RawEntity {
collection! {
"sentDate"=> JsonElement::Null,
"_ownerEncSessionKey"=> JsonElement::String(
OWNER_ENC_SESSION_KEY_FIELD=> JsonElement::String(
"AbK4PO4dConOew4jXt7UcmL9I73z1NA14EgbpBEw8J9ipgjD3i92SakgAv7SFXOE59VlWQ5dw3whqqSzkwoQavWWkDeJep1JzdP4ZyzNFMO7".to_string(),
),
"method"=> JsonElement::String(
"AROQNb+N33nEk9+C+fCuy0vPwMWzqDcnZP48St2Jm1obAvKux3xZwnq1mdqpZmcUQEUL3USwYoJ80Ef8gmqmFgk=".to_string(),
),
"bucketKey"=> JsonElement::Null,
BUCKET_KEY_FIELD=> JsonElement::Null,
"conversationEntry"=> JsonElement::Array(
vec![
JsonElement::String(
Expand All @@ -1333,7 +1363,7 @@ mod tests {
),
],
),
"_permissions"=> JsonElement::String(
PERMISSIONS_FIELD=> JsonElement::String(
"O1RT2Dj--g-0".to_string(),
),
"mailDetailsDraft"=> JsonElement::Null,
Expand All @@ -1343,7 +1373,7 @@ mod tests {
"map-free@tutanota.de".to_string(),
),
"contact"=> JsonElement::Null,
"_id"=> JsonElement::String(
ID_FIELD=> JsonElement::String(
"0y7Pgw".to_string(),
),
"name"=> JsonElement::String(
Expand All @@ -1360,7 +1390,7 @@ mod tests {
"state"=> JsonElement::String(
"2".to_string(),
),
"_ownerKeyVersion"=> JsonElement::String(
OWNER_KEY_VERSION_FIELD=> JsonElement::String(
"0".to_string(),
),
"replyTos"=> JsonElement::Array(
Expand All @@ -1376,7 +1406,7 @@ mod tests {
"address"=> JsonElement::String(
"bed-free@tutanota.de".to_string(),
),
"_id"=> JsonElement::String(
ID_FIELD=> JsonElement::String(
"yPeInQ".to_string(),
),
"name"=> JsonElement::String(
Expand All @@ -1392,7 +1422,7 @@ mod tests {
"attachments"=> JsonElement::Array(
vec![],
),
"_id"=> JsonElement::Array(
ID_FIELD=> JsonElement::Array(
vec![
JsonElement::String(
"O1RT1m6-0R-0".to_string(),
Expand All @@ -1409,7 +1439,7 @@ mod tests {
"receivedDate"=> JsonElement::String(
"1720612041643".to_string(),
),
"_ownerGroup"=> JsonElement::String(
OWNER_GROUP_FIELD=> JsonElement::String(
"O1RT1m4-0s-0".to_string(),
),
"replyType"=> JsonElement::String(
Expand All @@ -1418,7 +1448,7 @@ mod tests {
"phishingStatus"=> JsonElement::String(
"0".to_string(),
),
"_format"=> JsonElement::String(
FORMAT_FIELD=> JsonElement::String(
"0".to_string(),
),
"recipientCount"=> JsonElement::String(
Expand Down
Loading

0 comments on commit 9043a51

Please sign in to comment.