diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 7ad625a4..a0219f13 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -556,14 +556,45 @@ use super::sends::{update_send_from_data, SendData}; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct KeyData { + account_unlock_data: RotateAccountUnlockData, + account_keys: RotateAccountKeys, + account_data: RotateAccountData, + old_master_key_authentication_hash: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RotateAccountUnlockData { + emergency_access_unlock_data: Vec, + master_password_unlock_data: MasterPasswordUnlockData, + organization_account_recovery_unlock_data: Vec, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MasterPasswordUnlockData { + kdf_type: i32, + kdf_iterations: i32, + kdf_parallelism: Option, + kdf_memory: Option, + email: String, + master_key_authentication_hash: String, + master_key_encrypted_user_key: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RotateAccountKeys { + user_key_encrypted_account_private_key: String, + account_public_key: String, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RotateAccountData { ciphers: Vec, folders: Vec, sends: Vec, - emergency_access_keys: Vec, - reset_password_keys: Vec, - key: String, - master_password_hash: String, - private_key: String, } fn validate_keydata( @@ -573,10 +604,24 @@ fn validate_keydata( existing_emergency_access: &[EmergencyAccess], existing_memberships: &[Membership], existing_sends: &[Send], + user: &User, ) -> EmptyResult { + if user.client_kdf_type != data.account_unlock_data.master_password_unlock_data.kdf_type + || user.client_kdf_iter != data.account_unlock_data.master_password_unlock_data.kdf_iterations + || user.client_kdf_memory != data.account_unlock_data.master_password_unlock_data.kdf_memory + || user.client_kdf_parallelism != data.account_unlock_data.master_password_unlock_data.kdf_parallelism + || user.email != data.account_unlock_data.master_password_unlock_data.email + { + err!("Changing the kdf variant or email is not supported during key rotation"); + } + if user.public_key.as_ref() != Some(&data.account_keys.account_public_key) { + err!("Changing the asymmetric keypair is not possible during key rotation") + } + // Check that we're correctly rotating all the user's ciphers let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::>(); let provided_cipher_ids = data + .account_data .ciphers .iter() .filter(|c| c.organization_id.is_none()) @@ -588,7 +633,8 @@ fn validate_keydata( // Check that we're correctly rotating all the user's folders let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::>(); - let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::>(); + let provided_folder_ids = + data.account_data.folders.iter().filter_map(|f| f.id.as_ref()).collect::>(); if !provided_folder_ids.is_superset(&existing_folder_ids) { err!("All existing folders must be included in the rotation") } @@ -596,8 +642,12 @@ fn validate_keydata( // Check that we're correctly rotating all the user's emergency access keys let existing_emergency_access_ids = existing_emergency_access.iter().map(|ea| &ea.uuid).collect::>(); - let provided_emergency_access_ids = - data.emergency_access_keys.iter().map(|ea| &ea.id).collect::>(); + let provided_emergency_access_ids = data + .account_unlock_data + .emergency_access_unlock_data + .iter() + .map(|ea| &ea.id) + .collect::>(); if !provided_emergency_access_ids.is_superset(&existing_emergency_access_ids) { err!("All existing emergency access keys must be included in the rotation") } @@ -605,15 +655,19 @@ fn validate_keydata( // Check that we're correctly rotating all the user's reset password keys let existing_reset_password_ids = existing_memberships.iter().map(|m| &m.org_uuid).collect::>(); - let provided_reset_password_ids = - data.reset_password_keys.iter().map(|rp| &rp.organization_id).collect::>(); + let provided_reset_password_ids = data + .account_unlock_data + .organization_account_recovery_unlock_data + .iter() + .map(|rp| &rp.organization_id) + .collect::>(); if !provided_reset_password_ids.is_superset(&existing_reset_password_ids) { err!("All existing reset password keys must be included in the rotation") } // Check that we're correctly rotating all the user's sends let existing_send_ids = existing_sends.iter().map(|s| &s.uuid).collect::>(); - let provided_send_ids = data.sends.iter().filter_map(|s| s.id.as_ref()).collect::>(); + let provided_send_ids = data.account_data.sends.iter().filter_map(|s| s.id.as_ref()).collect::>(); if !provided_send_ids.is_superset(&existing_send_ids) { err!("All existing sends must be included in the rotation") } @@ -621,12 +675,12 @@ fn validate_keydata( Ok(()) } -#[post("/accounts/key", data = "")] +#[post("/accounts/key-management/rotate-user-account-keys", data = "")] async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { // TODO: See if we can wrap everything within a SQL Transaction. If something fails it should revert everything. let data: KeyData = data.into_inner(); - if !headers.user.check_valid_password(&data.master_password_hash) { + if !headers.user.check_valid_password(&data.old_master_key_authentication_hash) { err!("Invalid password") } @@ -634,7 +688,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Bitwarden does not process the import if there is one item invalid. // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it. // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks. - Cipher::validate_cipher_data(&data.ciphers)?; + Cipher::validate_cipher_data(&data.account_data.ciphers)?; let user_id = &headers.user.uuid; @@ -655,10 +709,11 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, &existing_emergency_access, &existing_memberships, &existing_sends, + &headers.user, )?; // Update folder data - for folder_data in data.folders { + for folder_data in data.account_data.folders { // Skip `null` folder id entries. // See: https://github.com/bitwarden/clients/issues/8453 if let Some(folder_id) = folder_data.id { @@ -672,7 +727,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, } // Update emergency access data - for emergency_access_data in data.emergency_access_keys { + for emergency_access_data in data.account_unlock_data.emergency_access_unlock_data { let Some(saved_emergency_access) = existing_emergency_access.iter_mut().find(|ea| ea.uuid == emergency_access_data.id) else { @@ -684,7 +739,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, } // Update reset password data - for reset_password_data in data.reset_password_keys { + for reset_password_data in data.account_unlock_data.organization_account_recovery_unlock_data { let Some(membership) = existing_memberships.iter_mut().find(|m| m.org_uuid == reset_password_data.organization_id) else { @@ -696,7 +751,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, } // Update send data - for send_data in data.sends { + for send_data in data.account_data.sends { let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else { err!("Send doesn't exist") }; @@ -707,7 +762,7 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update cipher data use super::ciphers::update_cipher_from_data; - for cipher_data in data.ciphers { + for cipher_data in data.account_data.ciphers { if cipher_data.organization_id.is_none() { let Some(saved_cipher) = existing_ciphers.iter_mut().find(|c| &c.uuid == cipher_data.id.as_ref().unwrap()) else { @@ -724,9 +779,13 @@ async fn post_rotatekey(data: Json, headers: Headers, mut conn: DbConn, // Update user data let mut user = headers.user; - user.akey = data.key; - user.private_key = Some(data.private_key); - user.reset_security_stamp(); + user.private_key = Some(data.account_keys.user_key_encrypted_account_private_key); + user.set_password( + &data.account_unlock_data.master_password_unlock_data.master_key_authentication_hash, + Some(data.account_unlock_data.master_password_unlock_data.master_key_encrypted_user_key), + true, + None, + ); let save_result = user.save(&mut conn).await;