diff --git a/Cargo.lock b/Cargo.lock index d2eb06438..2b5fd0d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -929,6 +929,7 @@ dependencies = [ "uuid", "wasm-bindgen", "wasm-bindgen-futures", + "wiremock", ] [[package]] @@ -1014,10 +1015,14 @@ version = "0.0.2" dependencies = [ "base64", "bat", + "bitwarden-api-api", "bitwarden-cli", + "bitwarden-collections", "bitwarden-core", + "bitwarden-crypto", "bitwarden-generators", "bitwarden-pm", + "bitwarden-test", "bitwarden-vault", "clap", "clap_complete", @@ -1029,7 +1034,10 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "thiserror 1.0.69", "tokio", + "uuid", + "wiremock", ] [[package]] diff --git a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs index 104298922..2a1fbc9a3 100644 --- a/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_success_response.rs @@ -38,7 +38,7 @@ pub(crate) struct IdentityTokenSuccessResponse { key_connector_url: Option, #[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")] - user_decryption_options: Option, + pub(crate) user_decryption_options: Option, /// Stores unknown api response fields extra: Option>, diff --git a/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs b/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs index 70ba509a0..0035d38d8 100644 --- a/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs @@ -10,6 +10,7 @@ pub(crate) struct UserDecryptionOptionsResponseModel { /// None when user have no master password. #[serde( rename = "masterPasswordUnlock", + alias = "MasterPasswordUnlock", skip_serializing_if = "Option::is_none" )] pub(crate) master_password_unlock: Option, diff --git a/crates/bitwarden-core/src/auth/login/access_token.rs b/crates/bitwarden-core/src/auth/login/access_token.rs index b3ef0ee3d..e7cb8726a 100644 --- a/crates/bitwarden-core/src/auth/login/access_token.rs +++ b/crates/bitwarden-core/src/auth/login/access_token.rs @@ -85,6 +85,11 @@ pub(crate) async fn login_access_token( r.refresh_token.clone(), r.expires_in, ); + + client + .internal + .initialize_crypto_single_org_key(organization_id, encryption_key); + client .internal .set_login_method(LoginMethod::ServiceAccount( @@ -94,10 +99,6 @@ pub(crate) async fn login_access_token( state_file: input.state_file.clone(), }, )); - - client - .internal - .initialize_crypto_single_org_key(organization_id, encryption_key); } AccessTokenLoginResponse::process_response(response) diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 9573637e0..7212ab8ad 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -1,15 +1,15 @@ -use bitwarden_crypto::{EncString, MasterKey}; +use bitwarden_crypto::EncString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::{ Client, auth::{ - JwtToken, api::{request::ApiTokenRequest, response::IdentityTokenResponse}, login::{LoginError, PasswordLoginResponse, response::two_factor::TwoFactorProviders}, }, client::{LoginMethod, UserLoginMethod, internal::UserKeyState}, + key_management::UserDecryptionData, require, }; @@ -23,44 +23,44 @@ pub(crate) async fn login_api_key( let response = request_api_identity_tokens(client, input).await?; if let IdentityTokenResponse::Authenticated(r) = &response { - let access_token_obj: JwtToken = r.access_token.parse()?; - - // This should always be Some() when logging in with an api key - let email = access_token_obj - .email - .ok_or(LoginError::JwtTokenMissingEmail)?; - - let kdf = client.auth().prelogin(email.clone()).await?; - client.internal.set_tokens( r.access_token.clone(), r.refresh_token.clone(), r.expires_in, ); - let master_key = MasterKey::derive(&input.password, &email, &kdf)?; - - client - .internal - .set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { - client_id: input.client_id.to_owned(), - client_secret: input.client_secret.to_owned(), - email, - kdf, - })); - - let user_key: EncString = require!(r.key.as_deref()).parse()?; - let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - - client.internal.initialize_user_crypto_master_key( - master_key, - user_key, - UserKeyState { - private_key, - signing_key: None, - security_state: None, - }, - )?; + let private_key: EncString = require!(&r.private_key).parse()?; + + let user_key_state = UserKeyState { + private_key, + signing_key: None, + security_state: None, + }; + + let master_password_unlock = r + .user_decryption_options + .as_ref() + .map(UserDecryptionData::try_from) + .transpose()? + .and_then(|user_decryption| user_decryption.master_password_unlock); + if let Some(master_password_unlock) = master_password_unlock { + client + .internal + .initialize_user_crypto_master_password_unlock( + input.password.clone(), + master_password_unlock.clone(), + user_key_state, + )?; + + client + .internal + .set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { + client_id: input.client_id.clone(), + client_secret: input.client_secret.clone(), + email: master_password_unlock.salt, + kdf: master_password_unlock.kdf, + })); + } } Ok(ApiKeyLoginResponse::process_response(response)) diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 71f1abf5a..bc2f142d9 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -10,8 +10,10 @@ use crate::{ api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, auth_request::new_auth_request, }, - client::{LoginMethod, UserLoginMethod}, - key_management::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + key_management::{ + UserDecryptionData, + crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + }, require, }; @@ -88,20 +90,11 @@ pub(crate) async fn complete_auth_request( .await?; if let IdentityTokenResponse::Authenticated(r) = response { - let kdf = Kdf::default(); - client.internal.set_tokens( r.access_token.clone(), r.refresh_token.clone(), r.expires_in, ); - client - .internal - .set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "web".to_owned(), - email: auth_req.email.to_owned(), - kdf: kdf.clone(), - })); let method = match res.master_password_hash { Some(_) => AuthRequestMethod::MasterKey { @@ -113,12 +106,27 @@ pub(crate) async fn complete_auth_request( }, }; + let master_password_unlock = r + .user_decryption_options + .as_ref() + .map(UserDecryptionData::try_from) + .transpose()? + .and_then(|user_decryption| user_decryption.master_password_unlock); + let kdf = master_password_unlock + .as_ref() + .map(|mpu| mpu.kdf.clone()) + .unwrap_or_else(Kdf::default); + let salt = master_password_unlock + .as_ref() + .map(|mpu| mpu.salt.clone()) + .unwrap_or_else(|| auth_req.email.clone()); + client .crypto() .initialize_user_crypto(InitUserCryptoRequest { user_id: None, kdf_params: kdf, - email: auth_req.email, + email: salt, private_key: require!(r.private_key).parse()?, signing_key: None, security_state: None, diff --git a/crates/bitwarden-core/src/auth/login/mod.rs b/crates/bitwarden-core/src/auth/login/mod.rs index 1b1182661..9e02d5df1 100644 --- a/crates/bitwarden-core/src/auth/login/mod.rs +++ b/crates/bitwarden-core/src/auth/login/mod.rs @@ -77,4 +77,8 @@ pub enum LoginError { #[error("Failed to authenticate")] AuthenticationFailed, + + #[cfg(feature = "internal")] + #[error(transparent)] + MasterPassword(#[from] crate::key_management::MasterPasswordError), } diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 950c68f7b..c89a0c709 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -1,6 +1,4 @@ #[cfg(feature = "internal")] -use bitwarden_crypto::Kdf; -#[cfg(feature = "internal")] use log::info; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -13,6 +11,7 @@ use crate::{ Client, auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest}, client::LoginMethod, + key_management::{MasterPasswordAuthenticationData, UserDecryptionData}, }; #[cfg(feature = "internal")] @@ -20,7 +19,7 @@ pub(crate) async fn login_password( client: &Client, input: &PasswordLoginRequest, ) -> Result { - use bitwarden_crypto::{EncString, HashPurpose, MasterKey}; + use bitwarden_crypto::EncString; use crate::{ client::{UserLoginMethod, internal::UserKeyState}, @@ -29,11 +28,16 @@ pub(crate) async fn login_password( info!("password logging in"); - let master_key = MasterKey::derive(&input.password, &input.email, &input.kdf)?; - let password_hash = master_key - .derive_master_key_hash(input.password.as_bytes(), HashPurpose::ServerAuthorization); + let kdf = client.auth().prelogin(input.email.clone()).await?; + + let master_password_authentication = + MasterPasswordAuthenticationData::derive(&input.password, &kdf, &input.email)?; + + let password_hash = master_password_authentication + .master_password_authentication_hash + .to_string(); - let response = request_identity_tokens(client, input, &password_hash.to_string()).await?; + let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { client.internal.set_tokens( @@ -41,26 +45,38 @@ pub(crate) async fn login_password( r.refresh_token.clone(), r.expires_in, ); - client - .internal - .set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: "web".to_owned(), - email: input.email.to_owned(), - kdf: input.kdf.to_owned(), - })); - - let user_key: EncString = require!(r.key.as_deref()).parse()?; - let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - - client.internal.initialize_user_crypto_master_key( - master_key, - user_key, - UserKeyState { - private_key, - signing_key: None, - security_state: None, - }, - )?; + + let private_key: EncString = require!(&r.private_key).parse()?; + + let user_key_state = UserKeyState { + private_key, + signing_key: None, + security_state: None, + }; + + let master_password_unlock = r + .user_decryption_options + .as_ref() + .map(UserDecryptionData::try_from) + .transpose()? + .and_then(|user_decryption| user_decryption.master_password_unlock); + if let Some(master_password_unlock) = master_password_unlock { + client + .internal + .initialize_user_crypto_master_password_unlock( + input.password.clone(), + master_password_unlock.clone(), + user_key_state, + )?; + + client + .internal + .set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "web".to_owned(), + email: master_password_unlock.salt, + kdf: master_password_unlock.kdf, + })); + } } Ok(PasswordLoginResponse::process_response(response)) @@ -97,8 +113,6 @@ pub struct PasswordLoginRequest { pub password: String, /// Two-factor authentication pub two_factor: Option, - /// Kdf from prelogin - pub kdf: Kdf, } #[allow(missing_docs)] diff --git a/crates/bitwarden-core/src/auth/tde.rs b/crates/bitwarden-core/src/auth/tde.rs index a9ae6c0f2..650de14e3 100644 --- a/crates/bitwarden-core/src/auth/tde.rs +++ b/crates/bitwarden-core/src/auth/tde.rs @@ -32,15 +32,6 @@ pub(super) fn make_register_tde_keys( None }; - client - .internal - .set_login_method(crate::client::LoginMethod::User( - crate::client::UserLoginMethod::Username { - client_id: "".to_owned(), - email, - kdf: Kdf::default(), - }, - )); client.internal.initialize_user_crypto_decrypted_key( user_key.0, UserKeyState { @@ -52,6 +43,16 @@ pub(super) fn make_register_tde_keys( }, )?; + client + .internal + .set_login_method(crate::client::LoginMethod::User( + crate::client::UserLoginMethod::Username { + client_id: "".to_owned(), + email, + kdf: Kdf::default(), + }, + )); + Ok(RegisterTdeKeyResponse { private_key: key_pair.private, public_key: key_pair.public, diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 591041177..a04d94864 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -26,7 +26,7 @@ use crate::{ }, error::NotAuthenticatedError, key_management::{ - PasswordProtectedKeyEnvelope, SecurityState, SignedSecurityState, + MasterPasswordUnlockData, PasswordProtectedKeyEnvelope, SecurityState, SignedSecurityState, crypto::InitUserCryptoRequest, }, }; @@ -390,11 +390,80 @@ impl InternalClient { ) -> Result<(), EncryptionSettingsError> { EncryptionSettings::set_org_keys(org_keys, &self.key_store) } + + #[cfg(feature = "internal")] + pub(crate) fn initialize_user_crypto_master_password_unlock( + &self, + password: String, + master_password_unlock: MasterPasswordUnlockData, + key_state: UserKeyState, + ) -> Result<(), EncryptionSettingsError> { + let master_key = MasterKey::derive( + &password, + &master_password_unlock.salt, + &master_password_unlock.kdf, + )?; + let user_key = + master_key.decrypt_user_key(master_password_unlock.master_key_wrapped_user_key)?; + self.initialize_user_crypto_decrypted_key(user_key, key_state) + } + + /// Sets the local KDF state for the master password unlock login method. + /// Salt and user key update is not supported yet. + #[cfg(feature = "internal")] + pub fn set_user_master_password_unlock( + &self, + master_password_unlock: MasterPasswordUnlockData, + ) -> Result<(), NotAuthenticatedError> { + let new_kdf = master_password_unlock.kdf; + + let login_method = self.get_login_method().ok_or(NotAuthenticatedError)?; + + let kdf = self.get_kdf()?; + + if kdf != new_kdf { + match login_method.as_ref() { + LoginMethod::User(UserLoginMethod::Username { + client_id, email, .. + }) => self.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: client_id.to_owned(), + email: email.to_owned(), + kdf: new_kdf, + })), + LoginMethod::User(UserLoginMethod::ApiKey { + client_id, + client_secret, + email, + .. + }) => self.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { + client_id: client_id.to_owned(), + client_secret: client_secret.to_owned(), + email: email.to_owned(), + kdf: new_kdf, + })), + #[cfg(feature = "secrets")] + LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError), + }; + } + + Ok(()) + } } #[cfg(test)] mod tests { - use crate::Client; + use std::num::NonZeroU32; + + use bitwarden_crypto::{EncString, Kdf, MasterKey}; + + use crate::{ + Client, + client::{LoginMethod, UserLoginMethod, test_accounts::test_bitwarden_com_account}, + key_management::MasterPasswordUnlockData, + }; + + const TEST_ACCOUNT_EMAIL: &str = "test@bitwarden.com"; + const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; #[test] fn initializing_user_multiple_times() { @@ -414,4 +483,66 @@ mod tests { let different_user_id = UserId::new_v4(); assert!(client.internal.init_user_id(different_user_id).is_err()); } + + #[tokio::test] + async fn test_set_user_master_password_unlock_kdf_updated() { + let new_kdf = Kdf::Argon2id { + iterations: NonZeroU32::new(4).unwrap(), + memory: NonZeroU32::new(65).unwrap(), + parallelism: NonZeroU32::new(5).unwrap(), + }; + + let user_key: EncString = TEST_ACCOUNT_USER_KEY.parse().expect("Invalid user key"); + let email = TEST_ACCOUNT_EMAIL.to_owned(); + + let client = Client::init_test_account(test_bitwarden_com_account()).await; + + client + .internal + .set_user_master_password_unlock(MasterPasswordUnlockData { + kdf: new_kdf.clone(), + master_key_wrapped_user_key: user_key, + salt: email, + }) + .unwrap(); + + let kdf = client.internal.get_kdf().unwrap(); + assert_eq!(kdf, new_kdf); + } + + #[tokio::test] + async fn test_set_user_master_password_unlock_email_and_keys_not_updated() { + let password = "asdfasdfasdf".to_string(); + let new_email = format!("{}@example.com", uuid::Uuid::new_v4()); + let kdf = Kdf::default(); + let expected_email = TEST_ACCOUNT_EMAIL.to_owned(); + + let (new_user_key, new_encrypted_user_key) = { + let master_key = MasterKey::derive(&password, &new_email, &kdf).unwrap(); + master_key.make_user_key().unwrap() + }; + + let client = Client::init_test_account(test_bitwarden_com_account()).await; + + client + .internal + .set_user_master_password_unlock(MasterPasswordUnlockData { + kdf, + master_key_wrapped_user_key: new_encrypted_user_key, + salt: new_email, + }) + .unwrap(); + + let login_method = client.internal.get_login_method().unwrap(); + match login_method.as_ref() { + LoginMethod::User(UserLoginMethod::Username { email, .. }) => { + assert_eq!(*email, expected_email); + } + _ => panic!("Expected username login method"), + } + + let user_key = client.crypto().get_user_encryption_key().await.unwrap(); + + assert_ne!(user_key, new_user_key.0.to_base64()); + } } diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index aa53b9245..b7af57bb0 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -81,6 +81,13 @@ pub enum InitUserCryptoMethod { /// The user's encrypted symmetric crypto key user_key: EncString, }, + /// Master Password Unlock + MasterPasswordUnlock { + /// The user's master password + password: String, + /// Contains the data needed to unlock with the master password + master_password_unlock: MasterPasswordUnlockData, + }, /// Never lock and/or biometric unlock DecryptedKey { /// The user's decrypted encryption key, obtained using `get_user_encryption_key` @@ -168,6 +175,18 @@ pub(super) async fn initialize_user_crypto( .internal .initialize_user_crypto_master_key(master_key, user_key, key_state)?; } + InitUserCryptoMethod::MasterPasswordUnlock { + password, + master_password_unlock, + } => { + client + .internal + .initialize_user_crypto_master_password_unlock( + password, + master_password_unlock, + key_state, + )?; + } InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => { let user_key = SymmetricCryptoKey::try_from(decrypted_user_key)?; client @@ -244,13 +263,11 @@ pub(super) async fn initialize_user_crypto( client .internal - .set_login_method(crate::client::LoginMethod::User( - crate::client::UserLoginMethod::Username { - client_id: "".to_string(), - email: req.email, - kdf: req.kdf_params, - }, - )); + .set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: "".to_string(), + email: req.email, + kdf: req.kdf_params, + })); Ok(()) } @@ -840,6 +857,11 @@ mod tests { "pgEBAlAmkP0QgfdMVbIujX55W/yNAycEgQIgBiFYIEM6JxBmjWQTruAm3s6BTaJy1q6BzQetMBacNeRJ0kxR"; const TEST_VECTOR_SECURITY_STATE_V2: &str = "hFgepAEnAxg8BFAmkP0QgfdMVbIujX55W/yNOgABOH8CoFgkomhlbnRpdHlJZFBHOOw2BI9OQoNq+Vl1xZZKZ3ZlcnNpb24CWEAlchbJR0vmRfShG8On7Q2gknjkw4Dd6MYBLiH4u+/CmfQdmjNZdf6kozgW/6NXyKVNu8dAsKsin+xxXkDyVZoG"; + const TEST_USER_EMAIL: &str = "test@bitwarden.com"; + const TEST_USER_PASSWORD: &str = "asdfasdfasdf"; + const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_ACCOUNT_PRIVATE_KEY: &str = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4="; + #[tokio::test] async fn test_update_kdf() { let client = Client::new(None); @@ -945,7 +967,7 @@ mod tests { }; initialize_user_crypto( - & client, + &client, InitUserCryptoRequest { user_id: Some(UserId::new_v4()), kdf_params: kdf.clone(), @@ -959,8 +981,8 @@ mod tests { }, }, ) - .await - .unwrap(); + .await + .unwrap(); let new_password_response = make_update_password(&client, "123412341234".into()).unwrap(); @@ -1025,7 +1047,7 @@ mod tests { let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap(); initialize_user_crypto( - & client, + &client, InitUserCryptoRequest { user_id: Some(UserId::new_v4()), kdf_params: Kdf::PBKDF2 { @@ -1041,8 +1063,8 @@ mod tests { }, }, ) - .await - .unwrap(); + .await + .unwrap(); let pin_key = derive_pin_key(&client, "1234".into()).unwrap(); @@ -1204,7 +1226,7 @@ mod tests { .unwrap(); let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap(); - let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); + let private_key = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap(); client .internal .initialize_user_crypto_master_key( @@ -1354,7 +1376,7 @@ mod tests { let client = Client::new(None); let priv_key: EncString = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=".parse().unwrap(); - let encrypted_userkey: EncString = "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(); + let encrypted_userkey: EncString = "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".parse().unwrap(); initialize_user_crypto( &client, @@ -1513,4 +1535,59 @@ mod tests { assert!(get_v2_rotated_account_keys(&client).is_ok()); } + + #[tokio::test] + async fn test_initialize_user_crypto_master_password_unlock() { + let client = Client::new(None); + + initialize_user_crypto( + &client, + InitUserCryptoRequest { + user_id: Some(UserId::new_v4()), + kdf_params: Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), + }, + email: TEST_USER_EMAIL.to_string(), + private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(), + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::MasterPasswordUnlock { + password: TEST_USER_PASSWORD.to_string(), + master_password_unlock: MasterPasswordUnlockData { + kdf: Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), + }, + master_key_wrapped_user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(), + salt: TEST_USER_EMAIL.to_string(), + }, + }, + }, + ) + .await + .unwrap(); + + let key_store = client.internal.get_key_store(); + let context = key_store.context(); + assert!(context.has_symmetric_key(SymmetricKeyId::User)); + assert!(context.has_asymmetric_key(AsymmetricKeyId::UserPrivateKey)); + let login_method = client.internal.get_login_method().unwrap(); + if let LoginMethod::User(UserLoginMethod::Username { + email, + kdf, + client_id, + .. + }) = login_method.as_ref() + { + assert_eq!(*email, TEST_USER_EMAIL); + assert_eq!( + *kdf, + Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), + } + ); + assert_eq!(*client_id, ""); + } else { + panic!("Expected username login method"); + } + } } diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 20074e158..514fbaf5e 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -35,7 +35,7 @@ pub enum MasterPasswordError { } /// Represents the data required to unlock with the master password. -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr( @@ -73,10 +73,10 @@ impl MasterPasswordUnlockData { } } -impl TryFrom for MasterPasswordUnlockData { +impl TryFrom<&MasterPasswordUnlockResponseModel> for MasterPasswordUnlockData { type Error = MasterPasswordError; - fn try_from(response: MasterPasswordUnlockResponseModel) -> Result { + fn try_from(response: &MasterPasswordUnlockResponseModel) -> Result { let kdf = match response.kdf.kdf_type { KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?, @@ -88,14 +88,14 @@ impl TryFrom for MasterPasswordUnlockData { }, }; - let master_key_encrypted_user_key = require!(response.master_key_encrypted_user_key); - let salt = require!(response.salt); + let master_key_wrapped_user_key = require!(&response.master_key_encrypted_user_key) + .parse() + .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?; + let salt = require!(&response.salt).clone(); Ok(MasterPasswordUnlockData { kdf, - master_key_wrapped_user_key: master_key_encrypted_user_key - .parse() - .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?, + master_key_wrapped_user_key, salt, }) } @@ -220,7 +220,7 @@ mod tests { 600_000, ); - let data = MasterPasswordUnlockData::try_from(response).unwrap(); + let data = MasterPasswordUnlockData::try_from(&response).unwrap(); if let Kdf::PBKDF2 { iterations } = data.kdf { assert_eq!(iterations.get(), 600_000); @@ -245,7 +245,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let data = MasterPasswordUnlockData::try_from(response).unwrap(); + let data = MasterPasswordUnlockData::try_from(&response).unwrap(); if let Kdf::Argon2id { iterations, @@ -273,7 +273,7 @@ mod tests { 600_000, ); - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!( result, Err(MasterPasswordError::EncryptionKeyMalformed) @@ -284,11 +284,11 @@ mod tests { fn test_try_from_master_password_unlock_response_model_user_key_none_missing_field_error() { let response = create_pbkdf2_response(None, Some(TEST_SALT.to_string()), 600_000); - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( - "response.master_key_encrypted_user_key" + "&response.master_key_encrypted_user_key" ))) )); } @@ -297,11 +297,11 @@ mod tests { fn test_try_from_master_password_unlock_response_model_salt_none_missing_field_error() { let response = create_pbkdf2_response(Some(TEST_USER_KEY.to_string()), None, 600_000); - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( - "response.salt" + "&response.salt" ))) )); } @@ -320,7 +320,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( @@ -343,7 +343,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } @@ -361,7 +361,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( @@ -384,7 +384,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } @@ -397,7 +397,7 @@ mod tests { 0, ); - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } @@ -415,7 +415,7 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); + let result = MasterPasswordUnlockData::try_from(&response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } } diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index a7718cdfe..f8ed1a320 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -22,15 +22,21 @@ pub use crypto_client::CryptoClient; #[cfg(feature = "internal")] mod master_password; #[cfg(feature = "internal")] +pub use master_password::MasterPasswordError; +#[cfg(feature = "internal")] +pub(crate) use master_password::{MasterPasswordAuthenticationData, MasterPasswordUnlockData}; +#[cfg(feature = "internal")] mod non_generic_wrappers; #[cfg(feature = "internal")] pub(crate) use non_generic_wrappers::*; #[cfg(feature = "internal")] mod security_state; #[cfg(feature = "internal")] +pub use security_state::{SecurityState, SignedSecurityState}; +#[cfg(feature = "internal")] mod user_decryption; #[cfg(feature = "internal")] -pub use security_state::{SecurityState, SignedSecurityState}; +pub use user_decryption::UserDecryptionData; use crate::OrganizationId; diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 34b553194..dbf536933 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -11,18 +11,19 @@ use crate::{ #[allow(dead_code)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -struct UserDecryptionData { +pub struct UserDecryptionData { /// Optional master password unlock data. - master_password_unlock: Option, + pub master_password_unlock: Option, } -impl TryFrom for UserDecryptionData { +impl TryFrom<&UserDecryptionResponseModel> for UserDecryptionData { type Error = MasterPasswordError; - fn try_from(response: UserDecryptionResponseModel) -> Result { + fn try_from(response: &UserDecryptionResponseModel) -> Result { let master_password_unlock = response .master_password_unlock - .map(|response| MasterPasswordUnlockData::try_from(*response)) + .as_deref() + .map(MasterPasswordUnlockData::try_from) .transpose()?; Ok(UserDecryptionData { @@ -31,12 +32,13 @@ impl TryFrom for UserDecryptionData { } } -impl TryFrom for UserDecryptionData { +impl TryFrom<&UserDecryptionOptionsResponseModel> for UserDecryptionData { type Error = MasterPasswordError; - fn try_from(response: UserDecryptionOptionsResponseModel) -> Result { + fn try_from(response: &UserDecryptionOptionsResponseModel) -> Result { let master_password_unlock = response .master_password_unlock + .as_ref() .map(MasterPasswordUnlockData::try_from) .transpose()?; diff --git a/crates/bitwarden-crypto/src/keys/kdf.rs b/crates/bitwarden-crypto/src/keys/kdf.rs index 429b3d66c..0476e9608 100644 --- a/crates/bitwarden-crypto/src/keys/kdf.rs +++ b/crates/bitwarden-crypto/src/keys/kdf.rs @@ -119,7 +119,7 @@ pub fn dangerous_derive_kdf_material( /// In Bitwarden accounts can use multiple KDFs to derive their master key from their password. This /// Enum represents all the possible KDFs. #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, PartialEq)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Enum))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 9ae609694..5a40fa78f 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -60,6 +60,7 @@ wasm-bindgen-futures = { workspace = true, optional = true } bitwarden-api-api = { workspace = true, features = ["mockall"] } bitwarden-test = { workspace = true } tokio = { workspace = true, features = ["rt"] } +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-vault/src/lib.rs b/crates/bitwarden-vault/src/lib.rs index 61d5b099f..a165dde1d 100644 --- a/crates/bitwarden-vault/src/lib.rs +++ b/crates/bitwarden-vault/src/lib.rs @@ -24,9 +24,6 @@ pub use error::{DecryptError, EncryptError, VaultParseError}; mod vault_client; pub use vault_client::{VaultClient, VaultClientExt}; -mod sync; -pub use sync::{SyncRequest, SyncResponse}; - #[allow(missing_docs)] pub mod collection_client; mod totp_client; diff --git a/crates/bitwarden-vault/src/sync.rs b/crates/bitwarden-vault/src/sync.rs deleted file mode 100644 index e0290c96a..000000000 --- a/crates/bitwarden-vault/src/sync.rs +++ /dev/null @@ -1,170 +0,0 @@ -use bitwarden_api_api::models::{ - DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel, -}; -use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; -use bitwarden_core::{ - Client, MissingFieldError, OrganizationId, UserId, - client::encryption_settings::EncryptionSettingsError, require, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -use crate::{Cipher, Folder, GlobalDomains, VaultParseError}; - -#[derive(Debug, Error)] -pub enum SyncError { - #[error(transparent)] - Api(#[from] bitwarden_core::ApiError), - #[error(transparent)] - MissingField(#[from] MissingFieldError), - #[error(transparent)] - VaultParse(#[from] VaultParseError), - #[error(transparent)] - CollectionParse(#[from] CollectionsParseError), - #[error(transparent)] - EncryptionSettings(#[from] EncryptionSettingsError), -} - -#[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SyncRequest { - /// Exclude the subdomains from the response, defaults to false - pub exclude_subdomains: Option, -} - -pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result { - let config = client.internal.get_api_configurations().await; - let sync = config - .api_client - .sync_api() - .get(input.exclude_subdomains) - .await - .map_err(|e| SyncError::Api(e.into()))?; - - let org_keys: Vec<_> = require!(sync.profile.as_ref()) - .organizations - .as_deref() - .unwrap_or_default() - .iter() - .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok()))) - .map(|(id, key)| (OrganizationId::new(id), key)) - .collect(); - - client.internal.initialize_org_crypto(org_keys)?; - - SyncResponse::process_response(sync) -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ProfileResponse { - pub id: UserId, - pub name: String, - pub email: String, - - //key: String, - //private_key: String, - pub organizations: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct ProfileOrganizationResponse { - pub id: OrganizationId, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct DomainResponse { - pub equivalent_domains: Vec>, - pub global_equivalent_domains: Vec, -} - -#[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase", deny_unknown_fields)] -pub struct SyncResponse { - /// Data about the user, including their encryption keys and the organizations they are a part - /// of - pub profile: ProfileResponse, - pub folders: Vec, - pub collections: Vec, - /// List of ciphers accessible by the user - pub ciphers: Vec, - pub domains: Option, - //pub policies: Vec, - //pub sends: Vec, -} - -impl SyncResponse { - pub(crate) fn process_response(response: SyncResponseModel) -> Result { - let profile = require!(response.profile); - let ciphers = require!(response.ciphers); - - fn try_into_iter(iter: In) -> Result - where - In: IntoIterator, - InItem: TryInto, - Out: FromIterator, - { - iter.into_iter().map(|i| i.try_into()).collect() - } - - Ok(SyncResponse { - profile: ProfileResponse::process_response(*profile)?, - folders: try_into_iter(require!(response.folders))?, - collections: try_into_iter(require!(response.collections))?, - ciphers: try_into_iter(ciphers)?, - domains: response.domains.map(|d| (*d).try_into()).transpose()?, - //policies: try_into_iter(require!(response.policies))?, - //sends: try_into_iter(require!(response.sends))?, - }) - } -} - -impl ProfileOrganizationResponse { - fn process_response( - response: ProfileOrganizationResponseModel, - ) -> Result { - Ok(ProfileOrganizationResponse { - id: OrganizationId::new(require!(response.id)), - }) - } -} - -impl ProfileResponse { - fn process_response( - response: ProfileResponseModel, - ) -> Result { - Ok(ProfileResponse { - id: UserId::new(require!(response.id)), - name: require!(response.name), - email: require!(response.email), - //key: response.key, - //private_key: response.private_key, - organizations: response - .organizations - .unwrap_or_default() - .into_iter() - .map(ProfileOrganizationResponse::process_response) - .collect::>()?, - }) - } -} - -impl TryFrom for DomainResponse { - type Error = SyncError; - - fn try_from(value: DomainsResponseModel) -> Result { - Ok(Self { - equivalent_domains: value.equivalent_domains.unwrap_or_default(), - global_equivalent_domains: value - .global_equivalent_domains - .unwrap_or_default() - .into_iter() - .map(|s| s.try_into()) - .collect::, _>>()?, - }) - } -} diff --git a/crates/bitwarden-vault/src/vault_client.rs b/crates/bitwarden-vault/src/vault_client.rs index 2fd6a513e..5f9eb47b8 100644 --- a/crates/bitwarden-vault/src/vault_client.rs +++ b/crates/bitwarden-vault/src/vault_client.rs @@ -3,10 +3,8 @@ use bitwarden_core::Client; use wasm_bindgen::prelude::*; use crate::{ - AttachmentsClient, CiphersClient, FoldersClient, PasswordHistoryClient, SyncRequest, - SyncResponse, TotpClient, + AttachmentsClient, CiphersClient, FoldersClient, PasswordHistoryClient, TotpClient, collection_client::CollectionsClient, - sync::{SyncError, sync}, }; #[allow(missing_docs)] @@ -21,11 +19,6 @@ impl VaultClient { Self { client } } - #[allow(missing_docs)] - pub async fn sync(&self, input: &SyncRequest) -> Result { - sync(&self.client, input).await - } - /// Password history related operations. pub fn password_history(&self) -> PasswordHistoryClient { PasswordHistoryClient { diff --git a/crates/bw/Cargo.toml b/crates/bw/Cargo.toml index 174f4df7e..d32804670 100644 --- a/crates/bw/Cargo.toml +++ b/crates/bw/Cargo.toml @@ -19,7 +19,9 @@ base64 = ">=0.22.1, <0.23" bat = { version = "0.25.0", features = [ "regex-fancy", ], default-features = false } +bitwarden-api-api = { workspace = true } bitwarden-cli = { workspace = true } +bitwarden-collections = { workspace = true } bitwarden-core = { workspace = true } bitwarden-generators = { workspace = true } bitwarden-pm = { workspace = true } @@ -34,7 +36,14 @@ log = "0.4.20" serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9.33" +thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } +[dev-dependencies] +bitwarden-crypto = { workspace = true } +bitwarden-test = { workspace = true } +uuid = { workspace = true } +wiremock = { workspace = true } + [lints] workspace = true diff --git a/crates/bw/src/auth/login.rs b/crates/bw/src/auth/login.rs index c5d50af38..e07eb7605 100644 --- a/crates/bw/src/auth/login.rs +++ b/crates/bw/src/auth/login.rs @@ -6,25 +6,23 @@ use bitwarden_core::{ TwoFactorRequest, }, }; -use bitwarden_vault::{SyncRequest, VaultClientExt}; use color_eyre::eyre::{Result, bail}; use inquire::{Password, Text}; use log::{debug, error, info}; +use crate::vault::{SyncRequest, sync}; + pub(crate) async fn login_password(client: Client, email: Option) -> Result<()> { let email = text_prompt_when_none("Email", email)?; let password = Password::new("Password").without_confirmation().prompt()?; - let kdf = client.auth().prelogin(email.clone()).await?; - let result = client .auth() .login_password(&PasswordLoginRequest { email: email.clone(), password: password.clone(), two_factor: None, - kdf: kdf.clone(), }) .await?; @@ -69,7 +67,6 @@ pub(crate) async fn login_password(client: Client, email: Option) -> Res email, password, two_factor, - kdf, }) .await?; @@ -78,12 +75,13 @@ pub(crate) async fn login_password(client: Client, email: Option) -> Res debug!("{result:?}"); } - let res = client - .vault() - .sync(&SyncRequest { + let res = sync( + &client, + &SyncRequest { exclude_subdomains: Some(true), - }) - .await?; + }, + ) + .await?; info!("{res:#?}"); Ok(()) diff --git a/crates/bw/src/vault/mod.rs b/crates/bw/src/vault/mod.rs index 0a3106c90..4f273ef5d 100644 --- a/crates/bw/src/vault/mod.rs +++ b/crates/bw/src/vault/mod.rs @@ -2,6 +2,9 @@ use clap::Subcommand; use crate::render::{CommandOutput, CommandResult}; +mod sync; +pub(crate) use sync::{SyncRequest, sync}; + #[derive(Subcommand, Clone)] pub enum ItemCommands { Get { id: String }, diff --git a/crates/bw/src/vault/sync.rs b/crates/bw/src/vault/sync.rs new file mode 100644 index 000000000..36f53d11b --- /dev/null +++ b/crates/bw/src/vault/sync.rs @@ -0,0 +1,415 @@ +use bitwarden_api_api::models::{ + DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel, +}; +use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; +use bitwarden_core::{ + Client, MissingFieldError, NotAuthenticatedError, OrganizationId, UserId, + client::encryption_settings::EncryptionSettingsError, + key_management::{MasterPasswordError, UserDecryptionData}, + require, +}; +use bitwarden_vault::{Cipher, Folder, GlobalDomains, VaultParseError}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SyncError { + #[error(transparent)] + Api(#[from] bitwarden_core::ApiError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + CollectionParse(#[from] CollectionsParseError), + #[error(transparent)] + EncryptionSettings(#[from] EncryptionSettingsError), + #[error(transparent)] + MasterPassword(#[from] MasterPasswordError), + #[error(transparent)] + NotAuthenticatedError(#[from] NotAuthenticatedError), +} + +#[allow(missing_docs)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SyncRequest { + /// Exclude the subdomains from the response, defaults to false + pub exclude_subdomains: Option, +} + +pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result { + let config = client.internal.get_api_configurations().await; + let sync = config + .api_client + .sync_api() + .get(input.exclude_subdomains) + .await + .map_err(|e| SyncError::Api(e.into()))?; + + let master_password_unlock = sync + .user_decryption + .as_deref() + .map(UserDecryptionData::try_from) + .transpose()? + .and_then(|user_decryption| user_decryption.master_password_unlock); + if let Some(master_password_unlock) = master_password_unlock { + client + .internal + .set_user_master_password_unlock(master_password_unlock)?; + } + + let org_keys: Vec<_> = require!(sync.profile.as_ref()) + .organizations + .as_deref() + .unwrap_or_default() + .iter() + .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok()))) + .map(|(id, key)| (OrganizationId::new(id), key)) + .collect(); + + client.internal.initialize_org_crypto(org_keys)?; + + SyncResponse::process_response(sync) +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProfileResponse { + pub id: UserId, + pub name: String, + pub email: String, + + //key: String, + //private_key: String, + pub organizations: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct ProfileOrganizationResponse { + pub id: OrganizationId, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct DomainResponse { + pub equivalent_domains: Vec>, + pub global_equivalent_domains: Vec, +} + +#[allow(missing_docs)] +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct SyncResponse { + /// Data about the user, including their encryption keys and the organizations they are a part + /// of + pub profile: ProfileResponse, + pub folders: Vec, + pub collections: Vec, + /// List of ciphers accessible by the user + pub ciphers: Vec, + pub domains: Option, + //pub policies: Vec, + //pub sends: Vec, +} + +impl SyncResponse { + pub(crate) fn process_response(response: SyncResponseModel) -> Result { + let profile = require!(response.profile); + let ciphers = require!(response.ciphers); + + fn try_into_iter(iter: In) -> Result + where + In: IntoIterator, + InItem: TryInto, + Out: FromIterator, + { + iter.into_iter().map(|i| i.try_into()).collect() + } + + Ok(SyncResponse { + profile: ProfileResponse::process_response(*profile)?, + folders: try_into_iter(require!(response.folders))?, + collections: try_into_iter(require!(response.collections))?, + ciphers: try_into_iter(ciphers)?, + domains: response.domains.map(|d| (*d).try_into()).transpose()?, + //policies: try_into_iter(require!(response.policies))?, + //sends: try_into_iter(require!(response.sends))?, + }) + } +} + +impl ProfileOrganizationResponse { + fn process_response( + response: ProfileOrganizationResponseModel, + ) -> Result { + Ok(ProfileOrganizationResponse { + id: OrganizationId::new(require!(response.id)), + }) + } +} + +impl ProfileResponse { + fn process_response( + response: ProfileResponseModel, + ) -> Result { + Ok(ProfileResponse { + id: UserId::new(require!(response.id)), + name: require!(response.name), + email: require!(response.email), + //key: response.key, + //private_key: response.private_key, + organizations: response + .organizations + .unwrap_or_default() + .into_iter() + .map(ProfileOrganizationResponse::process_response) + .collect::>()?, + }) + } +} + +impl TryFrom for DomainResponse { + type Error = SyncError; + + fn try_from(value: DomainsResponseModel) -> Result { + Ok(Self { + equivalent_domains: value.equivalent_domains.unwrap_or_default(), + global_equivalent_domains: value + .global_equivalent_domains + .unwrap_or_default() + .into_iter() + .map(|s| s.try_into()) + .collect::, _>>()?, + }) + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use bitwarden_api_api::models::{ + KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel, + UserDecryptionResponseModel, + }; + use bitwarden_core::{ + ClientSettings, DeviceType, + key_management::{ + SymmetricKeyId, + crypto::{InitOrgCryptoRequest, InitUserCryptoMethod, InitUserCryptoRequest}, + }, + }; + use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey}; + use bitwarden_test::start_api_mock; + use wiremock::{Mock, MockServer, Request, ResponseTemplate, matchers}; + + use super::*; + + const TEST_USER_NAME: &str = "Test User"; + const TEST_USER_EMAIL: &str = "test@bitwarden.com"; + const TEST_USER_PASSWORD: &str = "asdfasdfasdf"; + const TEST_USER_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8"; + const TEST_ACCOUNT_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_ACCOUNT_PRIVATE_KEY: &str = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4="; + const TEST_ACCOUNT_ORGANIZATION_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8"; + const TEST_ACCOUNT_ORGANIZATION_KEY: &str = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q=="; + + fn create_profile_response(user_id: UserId) -> ProfileResponseModel { + ProfileResponseModel { + id: Some(user_id.into()), + name: Some(TEST_USER_NAME.to_string()), + email: Some(TEST_USER_EMAIL.to_string()), + organizations: Some(vec![]), + ..ProfileResponseModel::new() + } + } + + fn create_sync_response(user_id: UserId) -> SyncResponseModel { + SyncResponseModel { + profile: Some(Box::new(create_profile_response(user_id))), + folders: Some(vec![]), + collections: Some(vec![]), + ciphers: Some(vec![]), + ..SyncResponseModel::new() + } + } + + async fn setup_sync_client( + response: SyncResponseModel, + user_crypto_request: InitUserCryptoRequest, + org_crypto_request: Option, + ) -> (MockServer, Client) { + let (server, api_config) = start_api_mock(vec![ + Mock::given(matchers::path("/sync")) + .respond_with(move |_: &Request| { + ResponseTemplate::new(200).set_body_json(response.to_owned()) + }) + .expect(1), + ]) + .await; + + let client = Client::new(Some(ClientSettings { + identity_url: api_config.base_path.clone(), + api_url: api_config.base_path, + user_agent: api_config.user_agent.unwrap(), + device_type: DeviceType::SDK, + })); + + client + .crypto() + .initialize_user_crypto(user_crypto_request) + .await + .unwrap(); + + if let Some(org_crypto_request) = org_crypto_request { + client + .crypto() + .initialize_org_crypto(org_crypto_request) + .await + .unwrap(); + } + + (server, client) + } + + fn make_user_crypto_request() -> InitUserCryptoRequest { + InitUserCryptoRequest { + user_id: Some(TEST_USER_ID.parse().unwrap()), + kdf_params: Kdf::default(), + email: TEST_USER_EMAIL.to_string(), + private_key: TEST_ACCOUNT_PRIVATE_KEY.parse().unwrap(), + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::Password { + password: TEST_USER_PASSWORD.to_string(), + user_key: TEST_ACCOUNT_USER_KEY.parse().unwrap(), + }, + } + } + + #[tokio::test] + async fn test_sync_user_empty_vault_no_organizations() { + let user_id: UserId = TEST_USER_ID.parse().unwrap(); + let organization_id: OrganizationId = TEST_ACCOUNT_ORGANIZATION_ID + .parse() + .expect("Invalid organization ID"); + let user_crypto_request = make_user_crypto_request(); + let (_server, client) = + setup_sync_client(create_sync_response(user_id), user_crypto_request, None).await; + + let sync_request = SyncRequest { + exclude_subdomains: Some(false), + }; + + let sync_response = sync(&client, &sync_request).await; + assert!(sync_response.is_ok()); + + let sync_response = sync_response.unwrap(); + assert_eq!(sync_response.profile.id, user_id); + assert_eq!(sync_response.profile.name, TEST_USER_NAME); + assert_eq!(sync_response.profile.email, TEST_USER_EMAIL); + assert!(sync_response.profile.organizations.is_empty()); + assert!(sync_response.ciphers.is_empty()); + assert!(sync_response.folders.is_empty()); + assert!(sync_response.collections.is_empty()); + assert!(sync_response.domains.is_none()); + assert!( + !client + .internal + .get_key_store() + .context() + .has_symmetric_key(SymmetricKeyId::Organization(organization_id)) + ); + } + + #[tokio::test] + async fn test_sync_user_with_organization() { + let user_id = UserId::new(uuid::uuid!(TEST_USER_ID)); + let organization_id: OrganizationId = TEST_ACCOUNT_ORGANIZATION_ID + .parse() + .expect("Invalid organization ID"); + let organization_key: UnsignedSharedKey = TEST_ACCOUNT_ORGANIZATION_KEY + .parse() + .expect("Invalid organization key"); + let user_crypto_request = make_user_crypto_request(); + let response = SyncResponseModel { + profile: Some(Box::new(ProfileResponseModel { + organizations: Some(vec![ProfileOrganizationResponseModel { + id: Some(organization_id.into()), + key: Some(organization_key.to_string()), + ..ProfileOrganizationResponseModel::new() + }]), + ..create_profile_response(user_id) + })), + ..create_sync_response(user_id) + }; + let (_server, client) = setup_sync_client(response, user_crypto_request, None).await; + + let sync_request = SyncRequest { + exclude_subdomains: Some(false), + }; + + let sync_response = sync(&client, &sync_request).await; + assert!(sync_response.is_ok()); + + let sync_response = sync_response.unwrap(); + assert_eq!(sync_response.profile.id, user_id); + assert_eq!(sync_response.profile.name, TEST_USER_NAME); + assert_eq!(sync_response.profile.email, TEST_USER_EMAIL); + assert_eq!(sync_response.profile.organizations.len(), 1); + let organization = sync_response.profile.organizations.first().unwrap(); + assert_eq!(organization.id, organization_id); + assert!(sync_response.ciphers.is_empty()); + assert!(sync_response.folders.is_empty()); + assert!(sync_response.collections.is_empty()); + assert!(sync_response.domains.is_none()); + assert!( + client + .internal + .get_key_store() + .context() + .has_symmetric_key(SymmetricKeyId::Organization(organization_id)) + ); + } + + #[tokio::test] + async fn test_sync_user_with_decryption_options_master_password_unlock() { + let user_id = UserId::new(uuid::uuid!(TEST_USER_ID)); + let user_key: EncString = TEST_ACCOUNT_USER_KEY.parse().expect("Invalid user key"); + let user_crypto_request = make_user_crypto_request(); + let response = SyncResponseModel { + user_decryption: Some(Box::new(UserDecryptionResponseModel { + master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 4, + memory: Some(65), + parallelism: Some(5), + }), + salt: Some(TEST_USER_EMAIL.to_string()), + master_key_encrypted_user_key: Some(user_key.to_string()), + })), + })), + ..create_sync_response(user_id) + }; + let (_server, client) = setup_sync_client(response, user_crypto_request, None).await; + + let sync_request = SyncRequest { + exclude_subdomains: Some(false), + }; + + let sync_response = sync(&client, &sync_request).await; + assert!(sync_response.is_ok()); + + assert_eq!( + client.internal.get_kdf().unwrap(), + Kdf::Argon2id { + iterations: NonZeroU32::new(4).unwrap(), + memory: NonZeroU32::new(65).unwrap(), + parallelism: NonZeroU32::new(5).unwrap(), + } + ); + } +}