From 44773f7a21af0e0ce4d17bd96b179b9934b96d8c Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Mon, 4 Aug 2025 23:40:59 +0100 Subject: [PATCH 01/29] Adds MasterPasswordUnlock into identity's response user decryption options --- .../api/response/identity_success_response.rs | 5 ++ .../bitwarden-core/src/auth/login/api_key.rs | 26 ++++++- .../src/auth/login/auth_request.rs | 55 ++++++++++----- crates/bitwarden-core/src/auth/login/mod.rs | 2 + .../bitwarden-core/src/auth/login/password.rs | 27 +++++++- .../src/key_management/master_password.rs | 69 +++++++++++++++++++ .../bitwarden-core/src/key_management/mod.rs | 3 + crates/bitwarden-crypto/src/keys/kdf.rs | 2 +- 8 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 crates/bitwarden-core/src/key_management/master_password.rs 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 94ebe9445..f31a353fa 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 @@ -3,6 +3,7 @@ use std::{collections::HashMap, num::NonZeroU32}; use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; +use bitwarden_api_api::models::UserDecryptionResponseModel; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct IdentityTokenSuccessResponse { @@ -35,6 +36,9 @@ pub struct IdentityTokenSuccessResponse { #[serde(rename = "keyConnectorUrl", alias = "KeyConnectorUrl")] key_connector_url: Option, + #[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")] + pub user_decryption_options: Option, + /// Stores unknown api response fields extra: Option>, } @@ -61,6 +65,7 @@ mod test { force_password_reset: Default::default(), api_use_key_connector: Default::default(), key_connector_url: Default::default(), + user_decryption_options: Default::default(), extra: Default::default(), } } diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 408347485..c6d1f59c1 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -9,6 +9,7 @@ use crate::{ JwtToken, }, client::{internal::UserKeyState, LoginMethod, UserLoginMethod}, + key_management::master_password::MasterPasswordUnlockData, require, Client, }; @@ -29,7 +30,28 @@ pub(crate) async fn login_api_key( .email .ok_or(LoginError::JwtTokenMissingEmail)?; - let kdf = client.auth().prelogin(email.clone()).await?; + // Users who have master password will use the master_password_unlock data + let (kdf, user_key) = match r + .user_decryption_options + .as_ref() + .and_then(|opts| opts.master_password_unlock.as_ref()) + { + Some(master_password_unlock) => { + let master_password_unlock_data = MasterPasswordUnlockData::process_response( + master_password_unlock.as_ref().clone(), + )?; + ( + master_password_unlock_data.kdf, + master_password_unlock_data.master_key_wrapped_user_key, + ) + } + None => { + // Fall back to prelogin KDF and r.key coming from identity response + let kdf = client.auth().prelogin(email.clone()).await?; + let user_key: EncString = require!(r.key.as_deref()).parse()?; + (kdf, user_key) + } + }; client.internal.set_tokens( r.access_token.clone(), @@ -47,8 +69,6 @@ pub(crate) async fn login_api_key( 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( diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 52d20b6a7..a74b910db 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -1,11 +1,5 @@ -use bitwarden_api_api::{ - apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, - models::{AuthRequestCreateRequestModel, AuthRequestType}, -}; -use bitwarden_crypto::Kdf; -use uuid::Uuid; - use super::LoginError; +use crate::key_management::master_password::MasterPasswordUnlockData; use crate::{ auth::{ api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, @@ -15,6 +9,12 @@ use crate::{ key_management::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, require, ApiError, Client, }; +use bitwarden_api_api::{ + apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, + models::{AuthRequestCreateRequestModel, AuthRequestType}, +}; +use bitwarden_crypto::Kdf; +use uuid::Uuid; #[allow(missing_docs)] pub struct NewAuthRequestResponse { @@ -88,7 +88,36 @@ pub(crate) async fn complete_auth_request( .await?; if let IdentityTokenResponse::Authenticated(r) = response { - let kdf = Kdf::default(); + let (kdf, method) = match (res.master_password_hash, r.user_decryption_options) { + (Some(_), Some(options)) => match options.master_password_unlock { + Some(master_password) => { + let master_password_unlock_data = MasterPasswordUnlockData::process_response( + master_password.as_ref().clone(), + )?; + let kdf = master_password_unlock_data.kdf.clone(); + let method = AuthRequestMethod::MasterKey { + protected_master_key: require!(res.key).parse()?, + auth_request_key: master_password_unlock_data.master_key_wrapped_user_key, + }; + (kdf, method) + } + None => { + // TODO backward compatibility, should be removed in the future and return error + let method = AuthRequestMethod::MasterKey { + protected_master_key: require!(res.key).parse()?, + auth_request_key: require!(r.key).parse()?, + }; + (Kdf::default(), method) + } + }, + (None, _) => { + let method = AuthRequestMethod::UserKey { + protected_user_key: require!(res.key).parse()?, + }; + (Kdf::default(), method) + } + (_, _) => return Err(LoginError::InvalidResponse), + }; client.internal.set_tokens( r.access_token.clone(), @@ -103,16 +132,6 @@ pub(crate) async fn complete_auth_request( kdf: kdf.clone(), })); - let method = match res.master_password_hash { - Some(_) => AuthRequestMethod::MasterKey { - protected_master_key: require!(res.key).parse()?, - auth_request_key: require!(r.key).parse()?, - }, - None => AuthRequestMethod::UserKey { - protected_user_key: require!(res.key).parse()?, - }, - }; - client .crypto() .initialize_user_crypto(InitUserCryptoRequest { diff --git a/crates/bitwarden-core/src/auth/login/mod.rs b/crates/bitwarden-core/src/auth/login/mod.rs index e186ac043..d05154429 100644 --- a/crates/bitwarden-core/src/auth/login/mod.rs +++ b/crates/bitwarden-core/src/auth/login/mod.rs @@ -45,6 +45,8 @@ pub enum LoginError { #[error(transparent)] MissingField(#[from] crate::MissingFieldError), + #[error(transparent)] + MasterPassword(#[from] crate::key_management::master_password::MasterPasswordError), #[error(transparent)] JwtTokenParse(#[from] super::JwtTokenParseError), diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 28e33ff7e..bc3721026 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -12,6 +12,7 @@ use crate::auth::{ use crate::{ auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest}, client::LoginMethod, + key_management::master_password::MasterPasswordUnlockData, Client, }; @@ -36,6 +37,29 @@ pub(crate) async fn login_password( let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { + // Users who have master password will use the master_password_unlock data + let (kdf, user_key) = match r + .user_decryption_options + .as_ref() + .and_then(|opts| opts.master_password_unlock.as_ref()) + { + Some(master_password_unlock) => { + let master_password_unlock_data = MasterPasswordUnlockData::process_response( + master_password_unlock.as_ref().clone(), + )?; + + ( + master_password_unlock_data.kdf, + master_password_unlock_data.master_key_wrapped_user_key, + ) + } + None => { + // TODO backward compatibility, should be removed in the future and return error + let user_key: EncString = require!(r.key.as_deref()).parse()?; + (input.kdf.clone(), user_key) + } + }; + client.internal.set_tokens( r.access_token.clone(), r.refresh_token.clone(), @@ -46,10 +70,9 @@ pub(crate) async fn login_password( .set_login_method(LoginMethod::User(UserLoginMethod::Username { client_id: "web".to_owned(), email: input.email.to_owned(), - kdf: input.kdf.to_owned(), + kdf: kdf.clone(), })); - 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( diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs new file mode 100644 index 000000000..4fa8bdb7b --- /dev/null +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -0,0 +1,69 @@ +#![allow(missing_docs)] + +use crate::{require, MissingFieldError}; +use bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel; +use bitwarden_api_api::models::KdfType; +use bitwarden_crypto::{CryptoError, EncString, Kdf}; +use bitwarden_error::bitwarden_error; +use serde::{Deserialize, Serialize}; +use std::num::NonZeroU32; +use std::str::FromStr; + +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum MasterPasswordError { + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct MasterPasswordUnlockData { + pub kdf: Kdf, + pub master_key_wrapped_user_key: EncString, + pub salt: String, +} + +impl MasterPasswordUnlockData { + pub(crate) fn process_response( + response: MasterPasswordUnlockResponseModel, + ) -> Result { + let kdf = match response.kdf.kdf_type { + KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { + iterations: NonZeroU32::new(response.kdf.iterations as u32).unwrap(), + }, + KdfType::Argon2id => Kdf::Argon2id { + iterations: NonZeroU32::new(response.kdf.iterations as u32).unwrap(), + memory: NonZeroU32::new(require!(response.kdf.memory) as u32).unwrap(), + parallelism: NonZeroU32::new(require!(response.kdf.parallelism) as u32).unwrap(), + }, + }; + + let master_key_encrypted_user_key = require!(response.master_key_encrypted_user_key); + let master_key_wrapped_user_key = + EncString::from_str(master_key_encrypted_user_key.as_str()) + .map_err(|e: CryptoError| MasterPasswordError::from(e))?; + + let salt = require!(response.salt); + + Ok(MasterPasswordUnlockData { + kdf, + master_key_wrapped_user_key, + salt, + }) + } +} + +#[allow(missing_docs)] +#[cfg(test)] +mod test { + // TODO +} diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 10be377a8..eab266215 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -20,6 +20,9 @@ pub use crypto_client::CryptoClient; #[cfg(feature = "internal")] mod security_state; +#[cfg(feature = "internal")] +pub mod master_password; + #[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; 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))] From 07128788b462a2a147594a7edfa827168f93c55f Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 00:10:32 +0100 Subject: [PATCH 02/29] Adds MasterPasswordUnlock KDF change handling in sync --- crates/bitwarden-core/src/client/internal.rs | 41 +++++++++++++++++++ .../src/key_management/master_password.rs | 2 +- crates/bitwarden-vault/src/sync.rs | 20 ++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 847ef16d5..6d755495a 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -193,6 +193,47 @@ impl InternalClient { } } + #[allow(missing_docs)] + #[cfg(feature = "internal")] + pub fn set_kdf(&self, kdf: Kdf) -> Result<(), NotAuthenticatedError> { + match self + .login_method + .read() + .expect("RwLock is not poisoned") + .as_deref() + { + Some(LoginMethod::User(e)) => { + match e { + UserLoginMethod::Username { + client_id, email, .. + } => { + self.set_login_method(LoginMethod::User(UserLoginMethod::Username { + client_id: client_id.clone(), + email: email.clone(), + kdf: kdf.clone(), + })); + } + UserLoginMethod::ApiKey { + client_id, + client_secret, + email, + .. + } => { + self.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { + client_id: client_id.clone(), + client_secret: client_secret.clone(), + email: email.clone(), + kdf: kdf.clone(), + })); + } + } + + Ok(()) + } + _ => Err(NotAuthenticatedError), + } + } + #[allow(missing_docs)] pub async fn get_api_configurations(&self) -> Arc { // At the moment we ignore the error result from the token renewal, if it fails, diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 4fa8bdb7b..af252a93b 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -33,7 +33,7 @@ pub struct MasterPasswordUnlockData { } impl MasterPasswordUnlockData { - pub(crate) fn process_response( + pub fn process_response( response: MasterPasswordUnlockResponseModel, ) -> Result { let kdf = match response.kdf.kdf_type { diff --git a/crates/bitwarden-vault/src/sync.rs b/crates/bitwarden-vault/src/sync.rs index 63bdb8ffe..a73e400be 100644 --- a/crates/bitwarden-vault/src/sync.rs +++ b/crates/bitwarden-vault/src/sync.rs @@ -3,7 +3,9 @@ use bitwarden_api_api::models::{ }; use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; use bitwarden_core::{ - client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError, + client::encryption_settings::EncryptionSettingsError, + key_management::master_password::MasterPasswordUnlockData, + require, Client, MissingFieldError, NotAuthenticatedError, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -23,6 +25,10 @@ pub enum SyncError { CollectionParse(#[from] CollectionsParseError), #[error(transparent)] EncryptionSettings(#[from] EncryptionSettingsError), + #[error(transparent)] + MasterPassword(#[from] bitwarden_core::key_management::master_password::MasterPasswordError), + #[error(transparent)] + NotAuthenticated(#[from] NotAuthenticatedError), } #[allow(missing_docs)] @@ -49,6 +55,18 @@ pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result Date: Tue, 5 Aug 2025 11:11:33 +0100 Subject: [PATCH 03/29] simplification --- .../bitwarden-core/src/auth/login/api_key.rs | 32 +++++++++++-------- .../src/auth/login/auth_request.rs | 8 ----- .../bitwarden-core/src/auth/login/password.rs | 32 ++++++++----------- 3 files changed, 33 insertions(+), 39 deletions(-) diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index c6d1f59c1..8699dfb41 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -2,13 +2,14 @@ use bitwarden_crypto::{EncString, MasterKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}; use crate::{ auth::{ api::{request::ApiTokenRequest, response::IdentityTokenResponse}, login::{response::two_factor::TwoFactorProviders, LoginError, PasswordLoginResponse}, JwtToken, }, - client::{internal::UserKeyState, LoginMethod, UserLoginMethod}, + client::{LoginMethod, UserLoginMethod}, key_management::master_password::MasterPasswordUnlockData, require, Client, }; @@ -59,7 +60,23 @@ pub(crate) async fn login_api_key( r.expires_in, ); - let master_key = MasterKey::derive(&input.password, &email, &kdf)?; + let private_key: EncString = require!(r.private_key.as_deref()).parse()?; + + client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + user_id: None, + kdf_params: kdf.clone(), + email: email.clone(), + private_key, + signing_key: None, + security_state: None, + method: InitUserCryptoMethod::Password { + password: input.password.to_owned(), + user_key, + }, + }) + .await?; client .internal @@ -69,17 +86,6 @@ pub(crate) async fn login_api_key( email, kdf, })); - 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, - }, - )?; } 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 a74b910db..2485ea781 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -5,7 +5,6 @@ use crate::{ api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, auth_request::new_auth_request, }, - client::{LoginMethod, UserLoginMethod}, key_management::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, require, ApiError, Client, }; @@ -124,13 +123,6 @@ pub(crate) async fn complete_auth_request( 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(), - })); client .crypto() diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index bc3721026..3f57b67e9 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -8,10 +8,10 @@ use serde::{Deserialize, Serialize}; use crate::auth::{ api::response::IdentityTokenResponse, login::response::two_factor::TwoFactorProviders, }; +use crate::key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}; #[cfg(feature = "internal")] use crate::{ auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest}, - client::LoginMethod, key_management::master_password::MasterPasswordUnlockData, Client, }; @@ -23,10 +23,7 @@ pub(crate) async fn login_password( ) -> Result { use bitwarden_crypto::{EncString, HashPurpose, MasterKey}; - use crate::{ - client::{internal::UserKeyState, UserLoginMethod}, - require, - }; + use crate::require; info!("password logging in"); @@ -65,25 +62,24 @@ 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: kdf.clone(), - })); let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - client.internal.initialize_user_crypto_master_key( - master_key, - user_key, - UserKeyState { + client + .crypto() + .initialize_user_crypto(InitUserCryptoRequest { + user_id: None, + kdf_params: kdf.clone(), + email: input.email.to_owned(), private_key, signing_key: None, security_state: None, - }, - )?; + method: InitUserCryptoMethod::Password { + password: input.password.to_owned(), + user_key, + }, + }) + .await?; } Ok(PasswordLoginResponse::process_response(response)) From 28a8423f6ed2b3a2a6fbee11e2532e27a809f2cc Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 11:21:52 +0100 Subject: [PATCH 04/29] clippy fix --- .../bitwarden-core/src/auth/login/api_key.rs | 2 +- .../src/key_management/master_password.rs | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 8699dfb41..8f371bfcf 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::{EncString, MasterKey}; +use bitwarden_crypto::EncString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index af252a93b..a577ac632 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -38,12 +38,16 @@ impl MasterPasswordUnlockData { ) -> Result { let kdf = match response.kdf.kdf_type { KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { - iterations: NonZeroU32::new(response.kdf.iterations as u32).unwrap(), + iterations: NonZeroU32::new(response.kdf.iterations as u32) + .ok_or(MissingFieldError("kdf.iterations"))?, }, KdfType::Argon2id => Kdf::Argon2id { - iterations: NonZeroU32::new(response.kdf.iterations as u32).unwrap(), - memory: NonZeroU32::new(require!(response.kdf.memory) as u32).unwrap(), - parallelism: NonZeroU32::new(require!(response.kdf.parallelism) as u32).unwrap(), + iterations: NonZeroU32::new(response.kdf.iterations as u32) + .ok_or(MissingFieldError("kdf.iterations"))?, + memory: NonZeroU32::new(require!(response.kdf.memory) as u32) + .ok_or(MissingFieldError("kdf.memory"))?, + parallelism: NonZeroU32::new(require!(response.kdf.parallelism) as u32) + .ok_or(MissingFieldError("kdf.parallelism"))?, }, }; @@ -61,9 +65,3 @@ impl MasterPasswordUnlockData { }) } } - -#[allow(missing_docs)] -#[cfg(test)] -mod test { - // TODO -} From b089ea0776edac4db68f49c6e27f04d0d918c560 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 11:59:16 +0100 Subject: [PATCH 05/29] formatting --- .../api/response/identity_success_response.rs | 2 +- .../bitwarden-core/src/auth/login/api_key.rs | 6 ++++-- .../src/auth/login/auth_request.rs | 19 +++++++++++-------- .../bitwarden-core/src/auth/login/password.rs | 8 ++++---- .../src/key_management/master_password.rs | 12 +++++++----- .../bitwarden-core/src/key_management/mod.rs | 4 ++-- crates/bitwarden-vault/src/sync.rs | 8 ++++---- 7 files changed, 33 insertions(+), 26 deletions(-) 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 f31a353fa..08378ad7c 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 @@ -1,9 +1,9 @@ use std::{collections::HashMap, num::NonZeroU32}; +use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; -use bitwarden_api_api::models::UserDecryptionResponseModel; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct IdentityTokenSuccessResponse { diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index 8f371bfcf..d370d160f 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -2,7 +2,6 @@ use bitwarden_crypto::EncString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}; use crate::{ auth::{ api::{request::ApiTokenRequest, response::IdentityTokenResponse}, @@ -10,7 +9,10 @@ use crate::{ JwtToken, }, client::{LoginMethod, UserLoginMethod}, - key_management::master_password::MasterPasswordUnlockData, + key_management::{ + crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, + master_password::MasterPasswordUnlockData, + }, require, Client, }; diff --git a/crates/bitwarden-core/src/auth/login/auth_request.rs b/crates/bitwarden-core/src/auth/login/auth_request.rs index 2485ea781..0e32081a6 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -1,19 +1,22 @@ +use bitwarden_api_api::{ + apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, + models::{AuthRequestCreateRequestModel, AuthRequestType}, +}; +use bitwarden_crypto::Kdf; +use uuid::Uuid; + use super::LoginError; -use crate::key_management::master_password::MasterPasswordUnlockData; use crate::{ auth::{ api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, auth_request::new_auth_request, }, - key_management::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + key_management::{ + crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, + master_password::MasterPasswordUnlockData, + }, require, ApiError, Client, }; -use bitwarden_api_api::{ - apis::auth_requests_api::{auth_requests_id_response_get, auth_requests_post}, - models::{AuthRequestCreateRequestModel, AuthRequestType}, -}; -use bitwarden_crypto::Kdf; -use uuid::Uuid; #[allow(missing_docs)] pub struct NewAuthRequestResponse { diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index 3f57b67e9..dc76deb43 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -5,16 +5,16 @@ use log::info; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::auth::{ - api::response::IdentityTokenResponse, login::response::two_factor::TwoFactorProviders, -}; -use crate::key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}; #[cfg(feature = "internal")] use crate::{ auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest}, key_management::master_password::MasterPasswordUnlockData, Client, }; +use crate::{ + auth::{api::response::IdentityTokenResponse, login::response::two_factor::TwoFactorProviders}, + key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, +}; #[cfg(feature = "internal")] pub(crate) async fn login_password( diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index a577ac632..83086c256 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,13 +1,15 @@ #![allow(missing_docs)] -use crate::{require, MissingFieldError}; -use bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel; -use bitwarden_api_api::models::KdfType; +use std::{num::NonZeroU32, str::FromStr}; + +use bitwarden_api_api::models::{ + master_password_unlock_response_model::MasterPasswordUnlockResponseModel, KdfType, +}; use bitwarden_crypto::{CryptoError, EncString, Kdf}; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; -use std::num::NonZeroU32; -use std::str::FromStr; + +use crate::{require, MissingFieldError}; #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index eab266215..177ad93ec 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,10 +18,10 @@ mod crypto_client; #[cfg(feature = "internal")] pub use crypto_client::CryptoClient; -#[cfg(feature = "internal")] -mod security_state; #[cfg(feature = "internal")] pub mod master_password; +#[cfg(feature = "internal")] +mod security_state; #[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; diff --git a/crates/bitwarden-vault/src/sync.rs b/crates/bitwarden-vault/src/sync.rs index a73e400be..ad78ef0d6 100644 --- a/crates/bitwarden-vault/src/sync.rs +++ b/crates/bitwarden-vault/src/sync.rs @@ -3,9 +3,9 @@ use bitwarden_api_api::models::{ }; use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; use bitwarden_core::{ - client::encryption_settings::EncryptionSettingsError, - key_management::master_password::MasterPasswordUnlockData, - require, Client, MissingFieldError, NotAuthenticatedError, + client::encryption_settings::EncryptionSettingsError, + key_management::master_password::MasterPasswordUnlockData, require, Client, MissingFieldError, + NotAuthenticatedError, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -61,7 +61,7 @@ pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result Date: Tue, 5 Aug 2025 12:04:40 +0100 Subject: [PATCH 06/29] no handling, just response parsing --- .../bitwarden-core/src/auth/login/api_key.rs | 62 +++++------------- .../src/auth/login/auth_request.rs | 54 ++++++---------- crates/bitwarden-core/src/auth/login/mod.rs | 2 - .../bitwarden-core/src/auth/login/password.rs | 63 +++++++------------ crates/bitwarden-core/src/client/internal.rs | 41 ------------ crates/bitwarden-crypto/src/keys/kdf.rs | 2 +- crates/bitwarden-vault/src/sync.rs | 20 +----- 7 files changed, 61 insertions(+), 183 deletions(-) diff --git a/crates/bitwarden-core/src/auth/login/api_key.rs b/crates/bitwarden-core/src/auth/login/api_key.rs index d370d160f..408347485 100644 --- a/crates/bitwarden-core/src/auth/login/api_key.rs +++ b/crates/bitwarden-core/src/auth/login/api_key.rs @@ -1,4 +1,4 @@ -use bitwarden_crypto::EncString; +use bitwarden_crypto::{EncString, MasterKey}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,11 +8,7 @@ use crate::{ login::{response::two_factor::TwoFactorProviders, LoginError, PasswordLoginResponse}, JwtToken, }, - client::{LoginMethod, UserLoginMethod}, - key_management::{ - crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, - master_password::MasterPasswordUnlockData, - }, + client::{internal::UserKeyState, LoginMethod, UserLoginMethod}, require, Client, }; @@ -33,28 +29,7 @@ pub(crate) async fn login_api_key( .email .ok_or(LoginError::JwtTokenMissingEmail)?; - // Users who have master password will use the master_password_unlock data - let (kdf, user_key) = match r - .user_decryption_options - .as_ref() - .and_then(|opts| opts.master_password_unlock.as_ref()) - { - Some(master_password_unlock) => { - let master_password_unlock_data = MasterPasswordUnlockData::process_response( - master_password_unlock.as_ref().clone(), - )?; - ( - master_password_unlock_data.kdf, - master_password_unlock_data.master_key_wrapped_user_key, - ) - } - None => { - // Fall back to prelogin KDF and r.key coming from identity response - let kdf = client.auth().prelogin(email.clone()).await?; - let user_key: EncString = require!(r.key.as_deref()).parse()?; - (kdf, user_key) - } - }; + let kdf = client.auth().prelogin(email.clone()).await?; client.internal.set_tokens( r.access_token.clone(), @@ -62,23 +37,7 @@ pub(crate) async fn login_api_key( r.expires_in, ); - let private_key: EncString = require!(r.private_key.as_deref()).parse()?; - - client - .crypto() - .initialize_user_crypto(InitUserCryptoRequest { - user_id: None, - kdf_params: kdf.clone(), - email: email.clone(), - private_key, - signing_key: None, - security_state: None, - method: InitUserCryptoMethod::Password { - password: input.password.to_owned(), - user_key, - }, - }) - .await?; + let master_key = MasterKey::derive(&input.password, &email, &kdf)?; client .internal @@ -88,6 +47,19 @@ pub(crate) async fn login_api_key( 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, + }, + )?; } 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 0e32081a6..52d20b6a7 100644 --- a/crates/bitwarden-core/src/auth/login/auth_request.rs +++ b/crates/bitwarden-core/src/auth/login/auth_request.rs @@ -11,10 +11,8 @@ use crate::{ api::{request::AuthRequestTokenRequest, response::IdentityTokenResponse}, auth_request::new_auth_request, }, - key_management::{ - crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, - master_password::MasterPasswordUnlockData, - }, + client::{LoginMethod, UserLoginMethod}, + key_management::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest}, require, ApiError, Client, }; @@ -90,42 +88,30 @@ pub(crate) async fn complete_auth_request( .await?; if let IdentityTokenResponse::Authenticated(r) = response { - let (kdf, method) = match (res.master_password_hash, r.user_decryption_options) { - (Some(_), Some(options)) => match options.master_password_unlock { - Some(master_password) => { - let master_password_unlock_data = MasterPasswordUnlockData::process_response( - master_password.as_ref().clone(), - )?; - let kdf = master_password_unlock_data.kdf.clone(); - let method = AuthRequestMethod::MasterKey { - protected_master_key: require!(res.key).parse()?, - auth_request_key: master_password_unlock_data.master_key_wrapped_user_key, - }; - (kdf, method) - } - None => { - // TODO backward compatibility, should be removed in the future and return error - let method = AuthRequestMethod::MasterKey { - protected_master_key: require!(res.key).parse()?, - auth_request_key: require!(r.key).parse()?, - }; - (Kdf::default(), method) - } - }, - (None, _) => { - let method = AuthRequestMethod::UserKey { - protected_user_key: require!(res.key).parse()?, - }; - (Kdf::default(), method) - } - (_, _) => return Err(LoginError::InvalidResponse), - }; + 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 { + protected_master_key: require!(res.key).parse()?, + auth_request_key: require!(r.key).parse()?, + }, + None => AuthRequestMethod::UserKey { + protected_user_key: require!(res.key).parse()?, + }, + }; client .crypto() diff --git a/crates/bitwarden-core/src/auth/login/mod.rs b/crates/bitwarden-core/src/auth/login/mod.rs index d05154429..e186ac043 100644 --- a/crates/bitwarden-core/src/auth/login/mod.rs +++ b/crates/bitwarden-core/src/auth/login/mod.rs @@ -45,8 +45,6 @@ pub enum LoginError { #[error(transparent)] MissingField(#[from] crate::MissingFieldError), - #[error(transparent)] - MasterPassword(#[from] crate::key_management::master_password::MasterPasswordError), #[error(transparent)] JwtTokenParse(#[from] super::JwtTokenParseError), diff --git a/crates/bitwarden-core/src/auth/login/password.rs b/crates/bitwarden-core/src/auth/login/password.rs index dc76deb43..28e33ff7e 100644 --- a/crates/bitwarden-core/src/auth/login/password.rs +++ b/crates/bitwarden-core/src/auth/login/password.rs @@ -5,16 +5,15 @@ use log::info; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::auth::{ + api::response::IdentityTokenResponse, login::response::two_factor::TwoFactorProviders, +}; #[cfg(feature = "internal")] use crate::{ auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest}, - key_management::master_password::MasterPasswordUnlockData, + client::LoginMethod, Client, }; -use crate::{ - auth::{api::response::IdentityTokenResponse, login::response::two_factor::TwoFactorProviders}, - key_management::crypto::{InitUserCryptoMethod, InitUserCryptoRequest}, -}; #[cfg(feature = "internal")] pub(crate) async fn login_password( @@ -23,7 +22,10 @@ pub(crate) async fn login_password( ) -> Result { use bitwarden_crypto::{EncString, HashPurpose, MasterKey}; - use crate::require; + use crate::{ + client::{internal::UserKeyState, UserLoginMethod}, + require, + }; info!("password logging in"); @@ -34,52 +36,31 @@ pub(crate) async fn login_password( let response = request_identity_tokens(client, input, &password_hash).await?; if let IdentityTokenResponse::Authenticated(r) = &response { - // Users who have master password will use the master_password_unlock data - let (kdf, user_key) = match r - .user_decryption_options - .as_ref() - .and_then(|opts| opts.master_password_unlock.as_ref()) - { - Some(master_password_unlock) => { - let master_password_unlock_data = MasterPasswordUnlockData::process_response( - master_password_unlock.as_ref().clone(), - )?; - - ( - master_password_unlock_data.kdf, - master_password_unlock_data.master_key_wrapped_user_key, - ) - } - None => { - // TODO backward compatibility, should be removed in the future and return error - let user_key: EncString = require!(r.key.as_deref()).parse()?; - (input.kdf.clone(), user_key) - } - }; - 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: 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 - .crypto() - .initialize_user_crypto(InitUserCryptoRequest { - user_id: None, - kdf_params: kdf.clone(), - email: input.email.to_owned(), + client.internal.initialize_user_crypto_master_key( + master_key, + user_key, + UserKeyState { private_key, signing_key: None, security_state: None, - method: InitUserCryptoMethod::Password { - password: input.password.to_owned(), - user_key, - }, - }) - .await?; + }, + )?; } Ok(PasswordLoginResponse::process_response(response)) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 6d755495a..847ef16d5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -193,47 +193,6 @@ impl InternalClient { } } - #[allow(missing_docs)] - #[cfg(feature = "internal")] - pub fn set_kdf(&self, kdf: Kdf) -> Result<(), NotAuthenticatedError> { - match self - .login_method - .read() - .expect("RwLock is not poisoned") - .as_deref() - { - Some(LoginMethod::User(e)) => { - match e { - UserLoginMethod::Username { - client_id, email, .. - } => { - self.set_login_method(LoginMethod::User(UserLoginMethod::Username { - client_id: client_id.clone(), - email: email.clone(), - kdf: kdf.clone(), - })); - } - UserLoginMethod::ApiKey { - client_id, - client_secret, - email, - .. - } => { - self.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey { - client_id: client_id.clone(), - client_secret: client_secret.clone(), - email: email.clone(), - kdf: kdf.clone(), - })); - } - } - - Ok(()) - } - _ => Err(NotAuthenticatedError), - } - } - #[allow(missing_docs)] pub async fn get_api_configurations(&self) -> Arc { // At the moment we ignore the error result from the token renewal, if it fails, diff --git a/crates/bitwarden-crypto/src/keys/kdf.rs b/crates/bitwarden-crypto/src/keys/kdf.rs index 0476e9608..429b3d66c 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, PartialEq)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)] #[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/src/sync.rs b/crates/bitwarden-vault/src/sync.rs index ad78ef0d6..63bdb8ffe 100644 --- a/crates/bitwarden-vault/src/sync.rs +++ b/crates/bitwarden-vault/src/sync.rs @@ -3,9 +3,7 @@ use bitwarden_api_api::models::{ }; use bitwarden_collections::{collection::Collection, error::CollectionsParseError}; use bitwarden_core::{ - client::encryption_settings::EncryptionSettingsError, - key_management::master_password::MasterPasswordUnlockData, require, Client, MissingFieldError, - NotAuthenticatedError, + client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -25,10 +23,6 @@ pub enum SyncError { CollectionParse(#[from] CollectionsParseError), #[error(transparent)] EncryptionSettings(#[from] EncryptionSettingsError), - #[error(transparent)] - MasterPassword(#[from] bitwarden_core::key_management::master_password::MasterPasswordError), - #[error(transparent)] - NotAuthenticated(#[from] NotAuthenticatedError), } #[allow(missing_docs)] @@ -55,18 +49,6 @@ pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result Date: Tue, 5 Aug 2025 12:51:28 +0100 Subject: [PATCH 07/29] test coverage --- .../src/key_management/master_password.rs | 292 +++++++++++++++++- 1 file changed, 288 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 83086c256..1ceeb24af 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -41,15 +41,15 @@ impl MasterPasswordUnlockData { let kdf = match response.kdf.kdf_type { KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { iterations: NonZeroU32::new(response.kdf.iterations as u32) - .ok_or(MissingFieldError("kdf.iterations"))?, + .ok_or(MissingFieldError(stringify!(response.kdf.iterations)))?, }, KdfType::Argon2id => Kdf::Argon2id { iterations: NonZeroU32::new(response.kdf.iterations as u32) - .ok_or(MissingFieldError("kdf.iterations"))?, + .ok_or(MissingFieldError(stringify!(response.kdf.iterations)))?, memory: NonZeroU32::new(require!(response.kdf.memory) as u32) - .ok_or(MissingFieldError("kdf.memory"))?, + .ok_or(MissingFieldError(stringify!(response.kdf.memory)))?, parallelism: NonZeroU32::new(require!(response.kdf.parallelism) as u32) - .ok_or(MissingFieldError("kdf.parallelism"))?, + .ok_or(MissingFieldError(stringify!(response.kdf.parallelism)))?, }, }; @@ -67,3 +67,287 @@ impl MasterPasswordUnlockData { }) } } + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockKdfResponseModel}; + + use super::*; + + const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI="; + const TEST_SALT: &str = "test@example.com"; + + fn create_pbkdf2_response( + iterations: i32, + encrypted_user_key: Option, + salt: Option, + ) -> MasterPasswordUnlockResponseModel { + MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: encrypted_user_key, + salt, + } + } + + fn create_argon2id_response( + iterations: i32, + memory: Option, + parallelism: Option, + encrypted_user_key: Option, + salt: Option, + ) -> MasterPasswordUnlockResponseModel { + MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations, + memory, + parallelism, + }), + master_key_encrypted_user_key: encrypted_user_key, + salt, + } + } + + #[test] + fn test_process_response_pbkdf2_success() { + let response = create_pbkdf2_response( + 600_000, + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response).unwrap(); + + match result.kdf { + Kdf::PBKDF2 { iterations } => { + assert_eq!(iterations.get(), 600_000); + } + _ => panic!("Expected PBKDF2 KDF"), + } + + assert_eq!(result.salt, TEST_SALT); + assert_eq!( + result.master_key_wrapped_user_key.to_string(), + TEST_USER_KEY + ); + } + + #[test] + fn test_process_response_argon2id_success() { + let response = create_argon2id_response( + 3, + Some(64), + Some(4), + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response).unwrap(); + + match result.kdf { + Kdf::Argon2id { + iterations, + memory, + parallelism, + } => { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } + _ => panic!("Expected Argon2id KDF"), + } + + assert_eq!(result.salt, TEST_SALT); + assert_eq!( + result.master_key_wrapped_user_key.to_string(), + TEST_USER_KEY + ); + } + + #[test] + fn test_process_response_invalid_user_key_crypto_error() { + let response = create_pbkdf2_response( + 600_000, + Some(TEST_INVALID_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!(result, Err(MasterPasswordError::Crypto(_)))); + } + + #[test] + fn test_process_response_missing_encrypted_user_key() { + let response = create_pbkdf2_response(600_000, None, Some(TEST_SALT.to_string())); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.master_key_encrypted_user_key" + ))) + )); + } + + #[test] + fn test_process_response_missing_salt() { + let response = create_pbkdf2_response(600_000, Some(TEST_USER_KEY.to_string()), None); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.salt" + ))) + )); + } + + #[test] + fn test_process_response_argon2id_missing_memory() { + let response = create_argon2id_response( + 3, + None, + Some(4), + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.memory" + ))) + )); + } + + #[test] + fn test_process_response_argon2id_missing_parallelism() { + let response = create_argon2id_response( + 3, + Some(64), + None, + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.parallelism" + ))) + )); + } + + #[test] + fn test_process_response_zero_iterations_pbkdf2() { + let response = create_pbkdf2_response( + 0, + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.iterations" + ))) + )); + } + + #[test] + fn test_process_response_zero_iterations_argon2id() { + let response = create_argon2id_response( + 0, + Some(0), + Some(0), + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + ); + + let result = MasterPasswordUnlockData::process_response(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.iterations" + ))) + )); + } + + #[test] + fn test_serde_serialization_pbkdf2() { + let data = MasterPasswordUnlockData { + kdf: Kdf::PBKDF2 { + iterations: 600_000.try_into().unwrap(), + }, + master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), + salt: TEST_SALT.to_string(), + }; + + let serialized = serde_json::to_string(&data).unwrap(); + let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); + + match (data.kdf, deserialized.kdf) { + (Kdf::PBKDF2 { iterations: i1 }, Kdf::PBKDF2 { iterations: i2 }) => { + assert_eq!(i1, i2); + } + _ => panic!("KDF types don't match"), + } + + assert_eq!( + data.master_key_wrapped_user_key.to_string(), + deserialized.master_key_wrapped_user_key.to_string() + ); + assert_eq!(data.salt, deserialized.salt); + } + + #[test] + fn test_serde_serialization_argon2id() { + let data = MasterPasswordUnlockData { + kdf: Kdf::Argon2id { + iterations: 3.try_into().unwrap(), + memory: 64.try_into().unwrap(), + parallelism: 4.try_into().unwrap(), + }, + master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), + salt: TEST_SALT.to_string(), + }; + + let serialized = serde_json::to_string(&data).unwrap(); + let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); + + match (data.kdf, deserialized.kdf) { + ( + Kdf::Argon2id { + iterations: i1, + memory: m1, + parallelism: p1, + }, + Kdf::Argon2id { + iterations: i2, + memory: m2, + parallelism: p2, + }, + ) => { + assert_eq!(i1, i2); + assert_eq!(m1, m2); + assert_eq!(p1, p2); + } + _ => panic!("KDF types don't match"), + } + + assert_eq!( + data.master_key_wrapped_user_key.to_string(), + deserialized.master_key_wrapped_user_key.to_string() + ); + assert_eq!(data.salt, deserialized.salt); + } +} From b3647f2ba59458b5433920d1b03000621e1f3b89 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 13:23:43 +0100 Subject: [PATCH 08/29] wasm --- Cargo.lock | 1 + crates/bitwarden-wasm-internal/Cargo.toml | 1 + .../src/custom_types.rs | 5 ++ crates/bitwarden-wasm-internal/src/lib.rs | 2 + .../src/master_password.rs | 61 +++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 crates/bitwarden-wasm-internal/src/master_password.rs diff --git a/Cargo.lock b/Cargo.lock index 04f97f266..585017e57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,6 +765,7 @@ version = "0.1.0" dependencies = [ "async-trait", "base64", + "bitwarden-api-api", "bitwarden-auth", "bitwarden-core", "bitwarden-crypto", diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index a85384f6e..2fc84daee 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -18,6 +18,7 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } base64 = ">=0.22.1, <0.23.0" +bitwarden-api-api = { workspace = true } bitwarden-auth = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } diff --git a/crates/bitwarden-wasm-internal/src/custom_types.rs b/crates/bitwarden-wasm-internal/src/custom_types.rs index f910d1173..e70056983 100644 --- a/crates/bitwarden-wasm-internal/src/custom_types.rs +++ b/crates/bitwarden-wasm-internal/src/custom_types.rs @@ -30,4 +30,9 @@ export type Utc = unknown; * An integer that is known not to equal zero. */ export type NonZeroU32 = number; + +/** + * An interger that is valid KdfType + */ +export type KdfType = number; "#; diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index 253ec8ffd..a60580a14 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -3,6 +3,7 @@ mod client; mod custom_types; mod init; +mod master_password; mod platform; mod pure_crypto; mod ssh; @@ -10,3 +11,4 @@ mod ssh; pub use bitwarden_ipc::wasm::*; pub use client::BitwardenClient; pub use init::init_sdk; +pub use master_password::*; diff --git a/crates/bitwarden-wasm-internal/src/master_password.rs b/crates/bitwarden-wasm-internal/src/master_password.rs new file mode 100644 index 000000000..068921850 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/master_password.rs @@ -0,0 +1,61 @@ +#![allow(missing_docs)] + +use bitwarden_api_api::models::KdfType; +use bitwarden_core::key_management::master_password::MasterPasswordUnlockData; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::prelude::*; + +// WASM-compatible wrapper for the auto-generated MasterPasswordUnlockKdfResponseModel +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct MasterPasswordUnlockKdfResponseModel { + #[serde(alias = "KdfType")] + pub kdf_type: KdfType, + #[serde(alias = "Iterations")] + pub iterations: i32, + #[serde(alias = "Memory")] + pub memory: Option, + #[serde(alias = "Parallelism")] + pub parallelism: Option, +} + +// WASM-compatible wrapper for the auto-generated MasterPasswordUnlockResponseModel +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct MasterPasswordUnlockResponseModel { + #[serde(alias = "Kdf")] + pub kdf: MasterPasswordUnlockKdfResponseModel, + #[serde(alias = "MasterKeyEncryptedUserKey")] + pub master_key_encrypted_user_key: Option, + #[serde(alias = "Salt")] + pub salt: Option, +} + +impl From for bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel { + fn from(wasm_model: MasterPasswordUnlockResponseModel) -> Self { + bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel { + kdf: Box::new(bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { + kdf_type: wasm_model.kdf.kdf_type, + iterations: wasm_model.kdf.iterations, + memory: wasm_model.kdf.memory, + parallelism: wasm_model.kdf.parallelism, + }), + master_key_encrypted_user_key: wasm_model.master_key_encrypted_user_key, + salt: wasm_model.salt, + } + } +} + +/// WASM-exposed function to process a MasterPasswordUnlockResponse +#[wasm_bindgen] +pub fn process_master_password_unlock_response( + response: MasterPasswordUnlockResponseModel, +) -> Result { + let api_response: bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel = response.into(); + + MasterPasswordUnlockData::process_response(api_response) + .map_err(|e| JsValue::from_str(&format!("MasterPasswordUnlockError: {}", e))) +} From 36d31360ced32d1be9c86c89c6b8b526e148e829 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 15:30:00 +0100 Subject: [PATCH 09/29] wasm unit test coverage --- .../src/master_password.rs | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/master_password.rs b/crates/bitwarden-wasm-internal/src/master_password.rs index 068921850..cecefebdf 100644 --- a/crates/bitwarden-wasm-internal/src/master_password.rs +++ b/crates/bitwarden-wasm-internal/src/master_password.rs @@ -1,7 +1,9 @@ #![allow(missing_docs)] use bitwarden_api_api::models::KdfType; -use bitwarden_core::key_management::master_password::MasterPasswordUnlockData; +use bitwarden_core::key_management::master_password::{ + MasterPasswordError, MasterPasswordUnlockData, +}; use serde::{Deserialize, Serialize}; use tsify::Tsify; use wasm_bindgen::prelude::*; @@ -53,9 +55,47 @@ impl From for bitwarden_api_api::models::mast #[wasm_bindgen] pub fn process_master_password_unlock_response( response: MasterPasswordUnlockResponseModel, -) -> Result { +) -> Result { let api_response: bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel = response.into(); MasterPasswordUnlockData::process_response(api_response) - .map_err(|e| JsValue::from_str(&format!("MasterPasswordUnlockError: {}", e))) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_process_master_password_unlock_response_success_argon2() { + let response = MasterPasswordUnlockResponseModel { + kdf: MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }, + master_key_encrypted_user_key: Some("2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM=".to_string()), + salt: Some("test@example.com".to_string()), + }; + + let result = process_master_password_unlock_response(response); + assert!(result.is_ok()); + } + + #[test] + fn test_process_master_password_unlock_response_failure_missing_salt() { + let response = MasterPasswordUnlockResponseModel { + kdf: MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }, + master_key_encrypted_user_key: Some("2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM=".to_string()), + salt: None, + }; + + let result = process_master_password_unlock_response(response); + assert!(result.is_err()); + } } From 7c8664dea1f4759411b3f321a257dd74ce4164d8 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 5 Aug 2025 15:45:18 +0100 Subject: [PATCH 10/29] wasm unit test coverage --- .../src/master_password.rs | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/master_password.rs b/crates/bitwarden-wasm-internal/src/master_password.rs index cecefebdf..1721a0fea 100644 --- a/crates/bitwarden-wasm-internal/src/master_password.rs +++ b/crates/bitwarden-wasm-internal/src/master_password.rs @@ -63,8 +63,43 @@ pub fn process_master_password_unlock_response( #[cfg(test)] mod tests { + use bitwarden_core::MissingFieldError; + use super::*; + const TEST_USER_KEY: &str = "2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM="; + const TEST_SALT: &str = "test@example.com"; + + #[test] + fn test_process_master_password_unlock_response_success_pbkdf2() { + let response = MasterPasswordUnlockResponseModel { + kdf: MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }, + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + let result = process_master_password_unlock_response(response); + assert!(result.is_ok()); + let data = result.unwrap(); + + // Verify the KDF data + match data.kdf { + bitwarden_crypto::Kdf::PBKDF2 { iterations } => { + assert_eq!(iterations.get(), 600_000); + } + _ => panic!("Expected PBKDF2 KDF"), + } + + // Verify salt and encrypted user key + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); + } + #[test] fn test_process_master_password_unlock_response_success_argon2() { let response = MasterPasswordUnlockResponseModel { @@ -74,12 +109,31 @@ mod tests { memory: Some(64), parallelism: Some(4), }, - master_key_encrypted_user_key: Some("2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM=".to_string()), - salt: Some("test@example.com".to_string()), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = process_master_password_unlock_response(response); assert!(result.is_ok()); + let data = result.unwrap(); + + // Verify the KDF data + match data.kdf { + bitwarden_crypto::Kdf::Argon2id { + iterations, + memory, + parallelism, + } => { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } + _ => panic!("Expected Argon2id KDF"), + } + + // Verify salt and encrypted user key + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); } #[test] @@ -91,11 +145,16 @@ mod tests { memory: Some(64), parallelism: Some(4), }, - master_key_encrypted_user_key: Some("2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM=".to_string()), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), salt: None, }; let result = process_master_password_unlock_response(response); - assert!(result.is_err()); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.salt" + ))) + )); } } From 9c7d50d2992aa2697fa9f8351ee2a9a48dc7f944 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 6 Aug 2025 00:18:27 +0100 Subject: [PATCH 11/29] Added UserDecryption data, response model with handling --- .../bitwarden-core/src/key_management/mod.rs | 2 + .../src/key_management/user_decryption.rs | 142 ++++++++++++++++++ .../{ => key_management}/master_password.rs | 0 .../src/key_management/mod.rs | 2 + .../src/key_management/user_decryption.rs | 124 +++++++++++++++ crates/bitwarden-wasm-internal/src/lib.rs | 3 +- 6 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 crates/bitwarden-core/src/key_management/user_decryption.rs rename crates/bitwarden-wasm-internal/src/{ => key_management}/master_password.rs (100%) create mode 100644 crates/bitwarden-wasm-internal/src/key_management/mod.rs create mode 100644 crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 177ad93ec..3fc1bb5bf 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -22,6 +22,8 @@ pub use crypto_client::CryptoClient; pub mod master_password; #[cfg(feature = "internal")] mod security_state; +#[cfg(feature = "internal")] +pub mod user_decryption; #[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs new file mode 100644 index 000000000..9a528333a --- /dev/null +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -0,0 +1,142 @@ +#![allow(missing_docs)] + +use bitwarden_api_api::models::UserDecryptionResponseModel; +use bitwarden_error::bitwarden_error; +use serde::{Deserialize, Serialize}; + +use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; + +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +pub enum UserDecryptionError { + #[error(transparent)] + MasterPasswordError(#[from] MasterPasswordError), +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +pub struct UserDecryptionData { + pub master_password_unlock: Option, +} + +impl UserDecryptionData { + pub fn process_response( + response: UserDecryptionResponseModel, + ) -> Result { + let master_password_unlock = match response.master_password_unlock { + Some(master_password_unlock) => Some(MasterPasswordUnlockData::process_response( + master_password_unlock.as_ref().clone(), + )?), + None => None, + }; + + Ok(UserDecryptionData { + master_password_unlock, + }) + } +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockResponseModel}; + use bitwarden_crypto::Kdf; + + use super::*; + + const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; + const TEST_SALT: &str = "test@example.com"; + + #[test] + fn test_process_response_master_password_unlock_some() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { + kdf: Box::new( + bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }, + ), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + })), + }; + + let result = UserDecryptionData::process_response(response); + assert!(result.is_ok()); + + let user_decryption_data = result.unwrap(); + + assert!(user_decryption_data.master_password_unlock.is_some()); + + let master_password_unlock = user_decryption_data.master_password_unlock.unwrap(); + + match master_password_unlock.kdf { + Kdf::Argon2id { + iterations, + memory, + parallelism, + } => { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } + _ => panic!("Expected Argon2id KDF"), + } + + assert_eq!(master_password_unlock.salt, TEST_SALT); + assert_eq!( + master_password_unlock + .master_key_wrapped_user_key + .to_string(), + TEST_USER_KEY + ); + } + + #[test] + fn test_process_response_missing_master_password_unlock() { + let response = UserDecryptionResponseModel { + master_password_unlock: None, + }; + + let result = UserDecryptionData::process_response(response); + assert!(result.is_ok()); + + let user_decryption_data = result.unwrap(); + + assert!(user_decryption_data.master_password_unlock.is_none()); + } + + #[test] + fn test_process_response_missing_master_password_unlock_salt() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { + kdf: Box::new( + bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }, + ), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: None, + })), + }; + + let result = UserDecryptionData::process_response(response); + assert!(matches!( + result, + Err(UserDecryptionError::MasterPasswordError( + MasterPasswordError::MissingField(_) + )) + )); + } +} diff --git a/crates/bitwarden-wasm-internal/src/master_password.rs b/crates/bitwarden-wasm-internal/src/key_management/master_password.rs similarity index 100% rename from crates/bitwarden-wasm-internal/src/master_password.rs rename to crates/bitwarden-wasm-internal/src/key_management/master_password.rs diff --git a/crates/bitwarden-wasm-internal/src/key_management/mod.rs b/crates/bitwarden-wasm-internal/src/key_management/mod.rs new file mode 100644 index 000000000..0466eab2c --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/key_management/mod.rs @@ -0,0 +1,2 @@ +mod master_password; +mod user_decryption; diff --git a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs new file mode 100644 index 000000000..ab4c8d1ff --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs @@ -0,0 +1,124 @@ +#![allow(missing_docs)] + +use bitwarden_core::key_management::user_decryption::{UserDecryptionData, UserDecryptionError}; +use serde::{Deserialize, Serialize}; +use tsify::Tsify; +use wasm_bindgen::prelude::*; + +use crate::key_management::master_password::MasterPasswordUnlockResponseModel; + +// WASM-compatible wrapper for the auto-generated UserDecryptionResponseModel +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct UserDecryptionResponseModel { + #[serde(alias = "MasterPasswordUnlock")] + pub master_password_unlock: Option, +} + +impl From for bitwarden_api_api::models::UserDecryptionResponseModel { + fn from(wasm_model: UserDecryptionResponseModel) -> Self { + bitwarden_api_api::models::UserDecryptionResponseModel { + master_password_unlock: wasm_model + .master_password_unlock + .map(|master_password_unlock| Box::new(master_password_unlock.into())), + } + } +} + +/// WASM-exposed function to process a UserDecryptionResponseModel +#[wasm_bindgen] +pub fn process_user_decryption_response( + response: UserDecryptionResponseModel, +) -> Result { + let api_response: bitwarden_api_api::models::UserDecryptionResponseModel = response.into(); + + UserDecryptionData::process_response(api_response) +} + +#[cfg(test)] +mod tests { + use bitwarden_api_api::models::KdfType; + use bitwarden_core::{key_management::master_password::MasterPasswordError, MissingFieldError}; + + use super::*; + use crate::key_management::master_password::MasterPasswordUnlockKdfResponseModel; + + const TEST_USER_KEY: &str = "2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM="; + const TEST_SALT: &str = "test@example.com"; + + #[test] + fn test_process_user_decryption_response_some() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(MasterPasswordUnlockResponseModel { + kdf: MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }, + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }), + }; + + let result = process_user_decryption_response(response); + assert!(result.is_ok()); + + let data = result.unwrap(); + assert!(data.master_password_unlock.is_some()); + + let master_password_unlock = data.master_password_unlock.unwrap(); + + match master_password_unlock.kdf { + bitwarden_crypto::Kdf::PBKDF2 { iterations } => { + assert_eq!(iterations.get(), 600_000); + } + _ => panic!("Expected PBKDF2 KDF"), + } + assert_eq!(master_password_unlock.salt, TEST_SALT); + assert_eq!( + master_password_unlock + .master_key_wrapped_user_key + .to_string(), + TEST_USER_KEY + ); + } + + #[test] + fn test_process_user_decryption_response_none() { + let response = UserDecryptionResponseModel { + master_password_unlock: None, + }; + + let result = process_user_decryption_response(response); + assert!(result.is_ok()); + + let data = result.unwrap(); + assert!(data.master_password_unlock.is_none()); + } + + #[test] + fn test_process_user_decryption_response_missing_salt() { + let response = UserDecryptionResponseModel { + master_password_unlock: Some(MasterPasswordUnlockResponseModel { + kdf: MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }, + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: None, + }), + }; + + let result = process_user_decryption_response(response); + assert!(matches!( + result, + Err(UserDecryptionError::MasterPasswordError( + MasterPasswordError::MissingField(MissingFieldError("response.salt")) + )) + )); + } +} diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index a60580a14..0e9951b4f 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -3,7 +3,7 @@ mod client; mod custom_types; mod init; -mod master_password; +mod key_management; mod platform; mod pure_crypto; mod ssh; @@ -11,4 +11,3 @@ mod ssh; pub use bitwarden_ipc::wasm::*; pub use client::BitwardenClient; pub use init::init_sdk; -pub use master_password::*; From 70ad9d3744ca316a9954e2d418fc91bd0afb5145 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 7 Aug 2025 13:05:26 +0100 Subject: [PATCH 12/29] autogenerated wasm responses, UserDecryption struct, use of TryFrom --- Cargo.lock | 4 + crates/bitwarden-api-api/Cargo.toml | 6 + .../bitwarden-api-api/src/models/kdf_type.rs | 7 + ...ster_password_unlock_kdf_response_model.rs | 7 + .../master_password_unlock_response_model.rs | 15 +- .../models/user_decryption_response_model.rs | 7 + crates/bitwarden-api-identity/Cargo.toml | 6 + .../src/models/kdf_type.rs | 7 + .../src/key_management/master_password.rs | 57 ++++--- .../src/key_management/user_decryption.rs | 20 +-- crates/bitwarden-wasm-internal/Cargo.toml | 2 +- .../src/key_management/master_password.rs | 160 ------------------ .../src/key_management/mod.rs | 3 +- .../src/key_management/user_decryption.rs | 132 ++------------- support/openapi-template/Cargo.mustache | 46 +++-- support/openapi-template/model.mustache | 46 +++++ 16 files changed, 198 insertions(+), 327 deletions(-) delete mode 100644 crates/bitwarden-wasm-internal/src/key_management/master_password.rs diff --git a/Cargo.lock b/Cargo.lock index 585017e57..ae5a82b26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,8 +326,10 @@ dependencies = [ "serde_json", "serde_repr", "serde_with", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] @@ -339,8 +341,10 @@ dependencies = [ "serde_json", "serde_repr", "serde_with", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 1c68a6cf1..a48b757d9 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -12,6 +12,10 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true +[features] +default = [] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support + [dependencies] reqwest = { workspace = true } serde = { workspace = true } @@ -24,3 +28,5 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ ] } url = ">=2.5, <3" uuid = { workspace = true } +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } +wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } diff --git a/crates/bitwarden-api-api/src/models/kdf_type.rs b/crates/bitwarden-api-api/src/models/kdf_type.rs index 882dc7226..bf2df2375 100644 --- a/crates/bitwarden-api-api/src/models/kdf_type.rs +++ b/crates/bitwarden-api-api/src/models/kdf_type.rs @@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; /// @@ -17,6 +19,11 @@ use crate::models; #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr, )] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs index 38e02d030..af98f1d00 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs @@ -9,10 +9,17 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct MasterPasswordUnlockKdfResponseModel { #[serde(rename = "kdfType")] pub kdf_type: models::KdfType, diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs index d6ec1c676..a161fa1dd 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs @@ -9,24 +9,31 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct MasterPasswordUnlockResponseModel { #[serde(rename = "kdf")] pub kdf: Box, #[serde(rename = "masterKeyEncryptedUserKey")] - pub master_key_encrypted_user_key: Option, + pub master_key_encrypted_user_key: String, #[serde(rename = "salt")] - pub salt: Option, + pub salt: String, } impl MasterPasswordUnlockResponseModel { pub fn new( kdf: models::MasterPasswordUnlockKdfResponseModel, - master_key_encrypted_user_key: Option, - salt: Option, + master_key_encrypted_user_key: String, + salt: String, ) -> MasterPasswordUnlockResponseModel { MasterPasswordUnlockResponseModel { kdf: Box::new(kdf), diff --git a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs index 94ed72a69..b53e730ed 100644 --- a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs +++ b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs @@ -9,10 +9,17 @@ */ use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct UserDecryptionResponseModel { #[serde( rename = "masterPasswordUnlock", diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 022225451..58fae643f 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -12,6 +12,10 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true +[features] +default = [] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support + [dependencies] reqwest = { workspace = true } serde = { workspace = true } @@ -24,3 +28,5 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ ] } url = ">=2.5, <3" uuid = { workspace = true } +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } +wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } diff --git a/crates/bitwarden-api-identity/src/models/kdf_type.rs b/crates/bitwarden-api-identity/src/models/kdf_type.rs index 0bc9d6282..a01284b94 100644 --- a/crates/bitwarden-api-identity/src/models/kdf_type.rs +++ b/crates/bitwarden-api-identity/src/models/kdf_type.rs @@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; use crate::models; /// @@ -17,6 +19,11 @@ use crate::models; #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr, )] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 1ceeb24af..d02c3933c 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,6 +1,6 @@ #![allow(missing_docs)] -use std::{num::NonZeroU32, str::FromStr}; +use std::num::NonZeroU32; use bitwarden_api_api::models::{ master_password_unlock_response_model::MasterPasswordUnlockResponseModel, KdfType, @@ -8,6 +8,8 @@ use bitwarden_api_api::models::{ use bitwarden_crypto::{CryptoError, EncString, Kdf}; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; use crate::{require, MissingFieldError}; @@ -34,40 +36,51 @@ pub struct MasterPasswordUnlockData { pub salt: String, } -impl MasterPasswordUnlockData { - pub fn process_response( - response: MasterPasswordUnlockResponseModel, - ) -> Result { +impl TryFrom for MasterPasswordUnlockData { + type Error = MasterPasswordError; + + fn try_from(response: MasterPasswordUnlockResponseModel) -> Result { let kdf = match response.kdf.kdf_type { KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { - iterations: NonZeroU32::new(response.kdf.iterations as u32) - .ok_or(MissingFieldError(stringify!(response.kdf.iterations)))?, + iterations: parse_nonzero_u32( + response.kdf.iterations, + stringify!(response.kdf.iterations), + )?, }, KdfType::Argon2id => Kdf::Argon2id { - iterations: NonZeroU32::new(response.kdf.iterations as u32) - .ok_or(MissingFieldError(stringify!(response.kdf.iterations)))?, - memory: NonZeroU32::new(require!(response.kdf.memory) as u32) - .ok_or(MissingFieldError(stringify!(response.kdf.memory)))?, - parallelism: NonZeroU32::new(require!(response.kdf.parallelism) as u32) - .ok_or(MissingFieldError(stringify!(response.kdf.parallelism)))?, + iterations: parse_nonzero_u32( + response.kdf.iterations, + stringify!(response.kdf.iterations), + )?, + memory: parse_nonzero_u32( + require!(response.kdf.memory), + stringify!(response.kdf.memory), + )?, + parallelism: parse_nonzero_u32( + require!(response.kdf.parallelism), + stringify!(response.kdf.parallelism), + )?, }, }; - let master_key_encrypted_user_key = require!(response.master_key_encrypted_user_key); - let master_key_wrapped_user_key = - EncString::from_str(master_key_encrypted_user_key.as_str()) - .map_err(|e: CryptoError| MasterPasswordError::from(e))?; - - let salt = require!(response.salt); - Ok(MasterPasswordUnlockData { kdf, - master_key_wrapped_user_key, - salt, + master_key_wrapped_user_key: response.master_key_encrypted_user_key.as_str().parse()?, + salt: response.salt, }) } } +fn parse_nonzero_u32( + value: impl TryInto, + field_name: &'static str, +) -> Result { + let num: u32 = value + .try_into() + .map_err(|_| MissingFieldError(field_name))?; + NonZeroU32::new(num).ok_or(MissingFieldError(field_name)) +} + #[cfg(test)] mod tests { use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockKdfResponseModel}; diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 9a528333a..067b982cb 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -3,6 +3,8 @@ use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; @@ -25,16 +27,14 @@ pub struct UserDecryptionData { pub master_password_unlock: Option, } -impl UserDecryptionData { - pub fn process_response( - response: UserDecryptionResponseModel, - ) -> Result { - let master_password_unlock = match response.master_password_unlock { - Some(master_password_unlock) => Some(MasterPasswordUnlockData::process_response( - master_password_unlock.as_ref().clone(), - )?), - None => None, - }; +impl TryFrom for UserDecryptionData { + type Error = UserDecryptionError; + + fn try_from(response: UserDecryptionResponseModel) -> Result { + let master_password_unlock = response + .master_password_unlock + .map(|response| MasterPasswordUnlockData::try_from(*response)) + .transpose()?; Ok(UserDecryptionData { master_password_unlock, diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 2fc84daee..87da34516 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } base64 = ">=0.22.1, <0.23.0" -bitwarden-api-api = { workspace = true } +bitwarden-api-api = { workspace = true, features = ["wasm"] } bitwarden-auth = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } diff --git a/crates/bitwarden-wasm-internal/src/key_management/master_password.rs b/crates/bitwarden-wasm-internal/src/key_management/master_password.rs deleted file mode 100644 index 1721a0fea..000000000 --- a/crates/bitwarden-wasm-internal/src/key_management/master_password.rs +++ /dev/null @@ -1,160 +0,0 @@ -#![allow(missing_docs)] - -use bitwarden_api_api::models::KdfType; -use bitwarden_core::key_management::master_password::{ - MasterPasswordError, MasterPasswordUnlockData, -}; -use serde::{Deserialize, Serialize}; -use tsify::Tsify; -use wasm_bindgen::prelude::*; - -// WASM-compatible wrapper for the auto-generated MasterPasswordUnlockKdfResponseModel -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct MasterPasswordUnlockKdfResponseModel { - #[serde(alias = "KdfType")] - pub kdf_type: KdfType, - #[serde(alias = "Iterations")] - pub iterations: i32, - #[serde(alias = "Memory")] - pub memory: Option, - #[serde(alias = "Parallelism")] - pub parallelism: Option, -} - -// WASM-compatible wrapper for the auto-generated MasterPasswordUnlockResponseModel -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct MasterPasswordUnlockResponseModel { - #[serde(alias = "Kdf")] - pub kdf: MasterPasswordUnlockKdfResponseModel, - #[serde(alias = "MasterKeyEncryptedUserKey")] - pub master_key_encrypted_user_key: Option, - #[serde(alias = "Salt")] - pub salt: Option, -} - -impl From for bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel { - fn from(wasm_model: MasterPasswordUnlockResponseModel) -> Self { - bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel { - kdf: Box::new(bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { - kdf_type: wasm_model.kdf.kdf_type, - iterations: wasm_model.kdf.iterations, - memory: wasm_model.kdf.memory, - parallelism: wasm_model.kdf.parallelism, - }), - master_key_encrypted_user_key: wasm_model.master_key_encrypted_user_key, - salt: wasm_model.salt, - } - } -} - -/// WASM-exposed function to process a MasterPasswordUnlockResponse -#[wasm_bindgen] -pub fn process_master_password_unlock_response( - response: MasterPasswordUnlockResponseModel, -) -> Result { - let api_response: bitwarden_api_api::models::master_password_unlock_response_model::MasterPasswordUnlockResponseModel = response.into(); - - MasterPasswordUnlockData::process_response(api_response) -} - -#[cfg(test)] -mod tests { - use bitwarden_core::MissingFieldError; - - use super::*; - - const TEST_USER_KEY: &str = "2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM="; - const TEST_SALT: &str = "test@example.com"; - - #[test] - fn test_process_master_password_unlock_response_success_pbkdf2() { - let response = MasterPasswordUnlockResponseModel { - kdf: MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }, - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }; - - let result = process_master_password_unlock_response(response); - assert!(result.is_ok()); - let data = result.unwrap(); - - // Verify the KDF data - match data.kdf { - bitwarden_crypto::Kdf::PBKDF2 { iterations } => { - assert_eq!(iterations.get(), 600_000); - } - _ => panic!("Expected PBKDF2 KDF"), - } - - // Verify salt and encrypted user key - assert_eq!(data.salt, TEST_SALT); - assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); - } - - #[test] - fn test_process_master_password_unlock_response_success_argon2() { - let response = MasterPasswordUnlockResponseModel { - kdf: MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::Argon2id, - iterations: 3, - memory: Some(64), - parallelism: Some(4), - }, - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }; - - let result = process_master_password_unlock_response(response); - assert!(result.is_ok()); - let data = result.unwrap(); - - // Verify the KDF data - match data.kdf { - bitwarden_crypto::Kdf::Argon2id { - iterations, - memory, - parallelism, - } => { - assert_eq!(iterations.get(), 3); - assert_eq!(memory.get(), 64); - assert_eq!(parallelism.get(), 4); - } - _ => panic!("Expected Argon2id KDF"), - } - - // Verify salt and encrypted user key - assert_eq!(data.salt, TEST_SALT); - assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); - } - - #[test] - fn test_process_master_password_unlock_response_failure_missing_salt() { - let response = MasterPasswordUnlockResponseModel { - kdf: MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::Argon2id, - iterations: 3, - memory: Some(64), - parallelism: Some(4), - }, - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: None, - }; - - let result = process_master_password_unlock_response(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.salt" - ))) - )); - } -} diff --git a/crates/bitwarden-wasm-internal/src/key_management/mod.rs b/crates/bitwarden-wasm-internal/src/key_management/mod.rs index 0466eab2c..4d68b17ba 100644 --- a/crates/bitwarden-wasm-internal/src/key_management/mod.rs +++ b/crates/bitwarden-wasm-internal/src/key_management/mod.rs @@ -1,2 +1 @@ -mod master_password; -mod user_decryption; +mod user_decryption; \ No newline at end of file diff --git a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs index ab4c8d1ff..8e32894a9 100644 --- a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs +++ b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs @@ -1,124 +1,24 @@ -#![allow(missing_docs)] - +use bitwarden_api_api::models::{MasterPasswordUnlockResponseModel, UserDecryptionResponseModel}; +use bitwarden_core::key_management::master_password::{ + MasterPasswordError, MasterPasswordUnlockData, +}; use bitwarden_core::key_management::user_decryption::{UserDecryptionData, UserDecryptionError}; -use serde::{Deserialize, Serialize}; -use tsify::Tsify; -use wasm_bindgen::prelude::*; - -use crate::key_management::master_password::MasterPasswordUnlockResponseModel; - -// WASM-compatible wrapper for the auto-generated UserDecryptionResponseModel -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Tsify)] -#[tsify(into_wasm_abi, from_wasm_abi)] -#[serde(rename_all = "camelCase")] -pub struct UserDecryptionResponseModel { - #[serde(alias = "MasterPasswordUnlock")] - pub master_password_unlock: Option, -} +use wasm_bindgen::prelude::wasm_bindgen; -impl From for bitwarden_api_api::models::UserDecryptionResponseModel { - fn from(wasm_model: UserDecryptionResponseModel) -> Self { - bitwarden_api_api::models::UserDecryptionResponseModel { - master_password_unlock: wasm_model - .master_password_unlock - .map(|master_password_unlock| Box::new(master_password_unlock.into())), - } - } -} - -/// WASM-exposed function to process a UserDecryptionResponseModel #[wasm_bindgen] -pub fn process_user_decryption_response( - response: UserDecryptionResponseModel, -) -> Result { - let api_response: bitwarden_api_api::models::UserDecryptionResponseModel = response.into(); - - UserDecryptionData::process_response(api_response) -} - -#[cfg(test)] -mod tests { - use bitwarden_api_api::models::KdfType; - use bitwarden_core::{key_management::master_password::MasterPasswordError, MissingFieldError}; - - use super::*; - use crate::key_management::master_password::MasterPasswordUnlockKdfResponseModel; +pub struct UserDecryption {} - const TEST_USER_KEY: &str = "2.Dh7AFLXR+LXcxUaO5cRjpg==|uXyhubjAoNH8lTdy/zgJDQ==|cHEMboj0MYsU5yDRQ1rLCgxcjNbKRc1PWKuv8bpU5pM="; - const TEST_SALT: &str = "test@example.com"; - - #[test] - fn test_process_user_decryption_response_some() { - let response = UserDecryptionResponseModel { - master_password_unlock: Some(MasterPasswordUnlockResponseModel { - kdf: MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }, - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }), - }; - - let result = process_user_decryption_response(response); - assert!(result.is_ok()); - - let data = result.unwrap(); - assert!(data.master_password_unlock.is_some()); - - let master_password_unlock = data.master_password_unlock.unwrap(); - - match master_password_unlock.kdf { - bitwarden_crypto::Kdf::PBKDF2 { iterations } => { - assert_eq!(iterations.get(), 600_000); - } - _ => panic!("Expected PBKDF2 KDF"), - } - assert_eq!(master_password_unlock.salt, TEST_SALT); - assert_eq!( - master_password_unlock - .master_key_wrapped_user_key - .to_string(), - TEST_USER_KEY - ); - } - - #[test] - fn test_process_user_decryption_response_none() { - let response = UserDecryptionResponseModel { - master_password_unlock: None, - }; - - let result = process_user_decryption_response(response); - assert!(result.is_ok()); - - let data = result.unwrap(); - assert!(data.master_password_unlock.is_none()); +#[wasm_bindgen] +impl UserDecryption { + pub fn get_user_decryption_data( + response: UserDecryptionResponseModel, + ) -> Result { + UserDecryptionData::try_from(response) } - #[test] - fn test_process_user_decryption_response_missing_salt() { - let response = UserDecryptionResponseModel { - master_password_unlock: Some(MasterPasswordUnlockResponseModel { - kdf: MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }, - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: None, - }), - }; - - let result = process_user_decryption_response(response); - assert!(matches!( - result, - Err(UserDecryptionError::MasterPasswordError( - MasterPasswordError::MissingField(MissingFieldError("response.salt")) - )) - )); + pub fn get_master_password_unlock_data( + response: MasterPasswordUnlockResponseModel, + ) -> Result { + MasterPasswordUnlockData::try_from(response) } } diff --git a/support/openapi-template/Cargo.mustache b/support/openapi-template/Cargo.mustache index cdaf12fc9..4a71adb53 100644 --- a/support/openapi-template/Cargo.mustache +++ b/support/openapi-template/Cargo.mustache @@ -14,16 +14,24 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true +[features] +default = [] +wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support + [dependencies] -serde = { version = "^1.0", features = ["derive"] } +serde = { workspace = true, features = ["derive"] } {{#serdeWith}} -serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } +serde_with = { version = ">=3.8, <4", default-features = false, features = [ + "base64", + "std", + "macros" +] } {{/serdeWith}} -serde_json = "^1.0" -serde_repr = "^0.1" -url = "^2.5" +serde_json = { workspace = true } +serde_repr = { workspace = true } +url = ">=2.5, <3" {{#hasUUIDs}} -uuid = { version = "^1.8", features = ["serde", "v4"] } +uuid = { workspace = true, features = ["serde", "v4", "js"] } {{/hasUUIDs}} {{#hyper}} {{#hyper0x}} @@ -46,27 +54,39 @@ secrecy = "0.8.0" {{/withAWSV4Signature}} {{#reqwest}} {{^supportAsync}} -reqwest = { version = "^0.12", default-features = false, features = ["json", "blocking", "multipart", "http2"] } +reqwest = { workspace = true, features = [ + "json", + "blocking", + "multipart", + "http2", +], default-features = false } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "blocking", "multipart"] } {{/supportMiddleware}} {{/supportAsync}} {{#supportAsync}} -reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } +reqwest = { workspace = true, features = [ + "json", + "multipart", + "http2", +], default-features = false } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} -async-trait = "^0.1" +async-trait = { workspace = true } # TODO: propose to Yoshidan to externalize this as non google related crate, so that it can easily be extended for other cloud providers. google-cloud-token = "^0.1" {{/supportTokenSource}} {{/supportAsync}} {{/reqwest}} {{#reqwestTrait}} -async-trait = "^0.1" -reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } -{{#supportMiddleware}} +async-trait = { workspace = true } +reqwest = { workspace = true, features = [ + "json", + "multipart", + "http2", +], default-features = false }{{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} @@ -90,3 +110,5 @@ mockall = ["dep:mockall"] bon = ["dep:bon"] {{/useBonBuilder}} {{/reqwestTrait}} +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } +wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } \ No newline at end of file diff --git a/support/openapi-template/model.mustache b/support/openapi-template/model.mustache index 8652cfe1c..f10433332 100644 --- a/support/openapi-template/model.mustache +++ b/support/openapi-template/model.mustache @@ -3,6 +3,10 @@ use crate::models; use serde::{Deserialize, Serialize}; {{#models}} {{#model}} +{{#vendorExtensions.x-sdk-wasm}} +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; +{{/vendorExtensions.x-sdk-wasm}} {{^isEnum}}{{#vendorExtensions.x-rust-has-byte-array}} use serde_with::serde_as; {{/vendorExtensions.x-rust-has-byte-array}}{{/isEnum}} @@ -20,6 +24,13 @@ use serde_repr::{Serialize_repr,Deserialize_repr}; /// {{{description}}} #[repr(i64)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -45,6 +56,13 @@ impl std::fmt::Display for {{{classname}}} { {{^isInteger}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -78,6 +96,13 @@ impl Default for {{{classname}}} { {{#discriminator}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "{{{propertyBaseName}}}")] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{^oneOf}} {{#mappedModels}} @@ -123,6 +148,13 @@ impl Default for {{classname}} { {{^discriminator}} {{#vendorExtensions.x-rust-has-byte-array}}#[serde_as] {{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub struct {{{classname}}} { {{#vars}} {{#description}} @@ -180,6 +212,13 @@ impl {{{classname}}} { {{/description}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{classname}} { {{#composedSchemas.oneOf}} {{#description}} @@ -202,6 +241,13 @@ impl Default for {{classname}} { {{#isEnum}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +{{#vendorExtensions.x-sdk-wasm}} +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] +{{/vendorExtensions.x-sdk-wasm}} pub enum {{{enumName}}} { {{#allowableValues}} {{#enumVars}} From 110f4dbc8fac80d498e15206d6e668b6b6c8b1f4 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 7 Aug 2025 13:29:36 +0100 Subject: [PATCH 13/29] failing unit test --- .../src/key_management/master_password.rs | 227 +++++++++--------- .../src/key_management/user_decryption.rs | 24 +- 2 files changed, 129 insertions(+), 122 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index d02c3933c..5d26fdaa8 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -91,79 +91,52 @@ mod tests { const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI="; const TEST_SALT: &str = "test@example.com"; - fn create_pbkdf2_response( - iterations: i32, - encrypted_user_key: Option, - salt: Option, - ) -> MasterPasswordUnlockResponseModel { - MasterPasswordUnlockResponseModel { + #[test] + fn test_try_from_master_password_unlock_response_model_pbkdf2_success() { + let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, - iterations, + iterations: 600_000, memory: None, parallelism: None, }), - master_key_encrypted_user_key: encrypted_user_key, - salt, - } - } - - fn create_argon2id_response( - iterations: i32, - memory: Option, - parallelism: Option, - encrypted_user_key: Option, - salt: Option, - ) -> MasterPasswordUnlockResponseModel { - MasterPasswordUnlockResponseModel { - kdf: Box::new(MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::Argon2id, - iterations, - memory, - parallelism, - }), - master_key_encrypted_user_key: encrypted_user_key, - salt, - } - } - - #[test] - fn test_process_response_pbkdf2_success() { - let response = create_pbkdf2_response( - 600_000, - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response).unwrap(); + let result = MasterPasswordUnlockData::try_from(response); + assert!(result.is_ok()); + let data = result.unwrap(); - match result.kdf { + match data.kdf { Kdf::PBKDF2 { iterations } => { assert_eq!(iterations.get(), 600_000); } _ => panic!("Expected PBKDF2 KDF"), } - assert_eq!(result.salt, TEST_SALT); - assert_eq!( - result.master_key_wrapped_user_key.to_string(), - TEST_USER_KEY - ); + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); } #[test] - fn test_process_response_argon2id_success() { - let response = create_argon2id_response( - 3, - Some(64), - Some(4), - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_argon2id_success() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response).unwrap(); + let result = MasterPasswordUnlockData::try_from(response); + assert!(result.is_ok()); + let data = result.unwrap(); - match result.kdf { + match data.kdf { Kdf::Argon2id { iterations, memory, @@ -176,81 +149,107 @@ mod tests { _ => panic!("Expected Argon2id KDF"), } - assert_eq!(result.salt, TEST_SALT); - assert_eq!( - result.master_key_wrapped_user_key.to_string(), - TEST_USER_KEY - ); + assert_eq!(data.salt, TEST_SALT); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); } #[test] - fn test_process_response_invalid_user_key_crypto_error() { - let response = create_pbkdf2_response( - 600_000, - Some(TEST_INVALID_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_invalid_user_key_crypto_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: TEST_INVALID_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!(result, Err(MasterPasswordError::Crypto(_)))); } #[test] - fn test_process_response_missing_encrypted_user_key() { - let response = create_pbkdf2_response(600_000, None, Some(TEST_SALT.to_string())); + fn test_try_from_master_password_unlock_response_model_argon2id_memory_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: None, + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( - "response.master_key_encrypted_user_key" + "response.kdf.memory" ))) )); } #[test] - fn test_process_response_missing_salt() { - let response = create_pbkdf2_response(600_000, Some(TEST_USER_KEY.to_string()), None); + fn test_try_from_master_password_unlock_response_model_argon2id_memory_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(0), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( - "response.salt" + "response.kdf.memory" ))) )); } #[test] - fn test_process_response_argon2id_missing_memory() { - let response = create_argon2id_response( - 3, - None, - Some(4), - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: None, + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.memory" + "response.kdf.parallelism" ))) )); } #[test] - fn test_process_response_argon2id_missing_parallelism() { - let response = create_argon2id_response( - 3, - Some(64), - None, - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: Some(64), + parallelism: Some(0), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( @@ -260,14 +259,19 @@ mod tests { } #[test] - fn test_process_response_zero_iterations_pbkdf2() { - let response = create_pbkdf2_response( - 0, - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_pbkdf2_iterations_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 0, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( @@ -277,16 +281,19 @@ mod tests { } #[test] - fn test_process_response_zero_iterations_argon2id() { - let response = create_argon2id_response( - 0, - Some(0), - Some(0), - Some(TEST_USER_KEY.to_string()), - Some(TEST_SALT.to_string()), - ); + fn test_try_from_master_password_unlock_response_model_argon2id_iterations_zero_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 0, + memory: Some(64), + parallelism: Some(4), + }), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), + }; - let result = MasterPasswordUnlockData::process_response(response); + let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( result, Err(MasterPasswordError::MissingField(MissingFieldError( diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 067b982cb..18b3d7e10 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -53,7 +53,7 @@ mod tests { const TEST_SALT: &str = "test@example.com"; #[test] - fn test_process_response_master_password_unlock_some() { + fn test_try_from_user_decryption_response_model_success() { let response = UserDecryptionResponseModel { master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { kdf: Box::new( @@ -64,12 +64,12 @@ mod tests { parallelism: Some(4), }, ), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), })), }; - let result = UserDecryptionData::process_response(response); + let result = UserDecryptionData::try_from(response); assert!(result.is_ok()); let user_decryption_data = result.unwrap(); @@ -101,12 +101,12 @@ mod tests { } #[test] - fn test_process_response_missing_master_password_unlock() { + fn test_try_from_user_decryption_response_model_master_password_unlock_none_success() { let response = UserDecryptionResponseModel { master_password_unlock: None, }; - let result = UserDecryptionData::process_response(response); + let result = UserDecryptionData::try_from(response); assert!(result.is_ok()); let user_decryption_data = result.unwrap(); @@ -115,23 +115,23 @@ mod tests { } #[test] - fn test_process_response_missing_master_password_unlock_salt() { + fn test_try_from_user_decryption_response_model_missing_field_error() { let response = UserDecryptionResponseModel { master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { kdf: Box::new( bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, iterations: 3, - memory: Some(64), - parallelism: Some(4), + memory: None, + parallelism: None, }, ), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: None, + master_key_encrypted_user_key: TEST_USER_KEY.to_string(), + salt: TEST_SALT.to_string(), })), }; - let result = UserDecryptionData::process_response(response); + let result = UserDecryptionData::try_from(response); assert!(matches!( result, Err(UserDecryptionError::MasterPasswordError( From ff86adfa76c1f5da85c717d3f1b939618268aef1 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 7 Aug 2025 13:31:38 +0100 Subject: [PATCH 14/29] lint --- crates/bitwarden-wasm-internal/src/key_management/mod.rs | 2 +- .../src/key_management/user_decryption.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/key_management/mod.rs b/crates/bitwarden-wasm-internal/src/key_management/mod.rs index 4d68b17ba..021fa70d6 100644 --- a/crates/bitwarden-wasm-internal/src/key_management/mod.rs +++ b/crates/bitwarden-wasm-internal/src/key_management/mod.rs @@ -1 +1 @@ -mod user_decryption; \ No newline at end of file +mod user_decryption; diff --git a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs index 8e32894a9..dd527d0c6 100644 --- a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs +++ b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs @@ -1,8 +1,8 @@ use bitwarden_api_api::models::{MasterPasswordUnlockResponseModel, UserDecryptionResponseModel}; -use bitwarden_core::key_management::master_password::{ - MasterPasswordError, MasterPasswordUnlockData, +use bitwarden_core::key_management::{ + master_password::{MasterPasswordError, MasterPasswordUnlockData}, + user_decryption::{UserDecryptionData, UserDecryptionError}, }; -use bitwarden_core::key_management::user_decryption::{UserDecryptionData, UserDecryptionError}; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen] From fa253a37773dd955576261fa0b18d6f394d270ab Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 7 Aug 2025 13:51:21 +0100 Subject: [PATCH 15/29] lint --- crates/bitwarden-api-api/Cargo.toml | 2 +- crates/bitwarden-api-identity/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index a48b757d9..8266d4348 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -26,7 +26,7 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ "std", "macros", ] } +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } url = ">=2.5, <3" uuid = { workspace = true } -tsify = { workspace = true, optional = true, features = ["js"], default-features = false } wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 58fae643f..86b61e75d 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -26,7 +26,7 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ "std", "macros", ] } +tsify = { workspace = true, optional = true, features = ["js"], default-features = false } url = ">=2.5, <3" uuid = { workspace = true } -tsify = { workspace = true, optional = true, features = ["js"], default-features = false } wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } From 4a95dc59ab78a0dedc8a6be8b3e9a6645efa4a30 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Thu, 7 Aug 2025 15:01:30 +0100 Subject: [PATCH 16/29] KdfType enum duplicate --- crates/bitwarden-api-api/src/models/kdf_type.rs | 7 ------- crates/bitwarden-api-identity/src/models/kdf_type.rs | 7 ------- 2 files changed, 14 deletions(-) diff --git a/crates/bitwarden-api-api/src/models/kdf_type.rs b/crates/bitwarden-api-api/src/models/kdf_type.rs index bf2df2375..882dc7226 100644 --- a/crates/bitwarden-api-api/src/models/kdf_type.rs +++ b/crates/bitwarden-api-api/src/models/kdf_type.rs @@ -10,8 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; use crate::models; /// @@ -19,11 +17,6 @@ use crate::models; #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr, )] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, diff --git a/crates/bitwarden-api-identity/src/models/kdf_type.rs b/crates/bitwarden-api-identity/src/models/kdf_type.rs index a01284b94..0bc9d6282 100644 --- a/crates/bitwarden-api-identity/src/models/kdf_type.rs +++ b/crates/bitwarden-api-identity/src/models/kdf_type.rs @@ -10,8 +10,6 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; use crate::models; /// @@ -19,11 +17,6 @@ use crate::models; #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr, )] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub enum KdfType { PBKDF2_SHA256 = 0, Argon2id = 1, From cd37e31f78021cc2e02daed27b697c6849b56d9b Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 8 Aug 2025 09:57:40 +0100 Subject: [PATCH 17/29] revert identity crate wasm, since there it's not used right now --- Cargo.lock | 2 -- crates/bitwarden-api-identity/Cargo.toml | 6 ------ 2 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae5a82b26..abfca253f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -341,10 +341,8 @@ dependencies = [ "serde_json", "serde_repr", "serde_with", - "tsify", "url", "uuid", - "wasm-bindgen", ] [[package]] diff --git a/crates/bitwarden-api-identity/Cargo.toml b/crates/bitwarden-api-identity/Cargo.toml index 86b61e75d..022225451 100644 --- a/crates/bitwarden-api-identity/Cargo.toml +++ b/crates/bitwarden-api-identity/Cargo.toml @@ -12,10 +12,6 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true -[features] -default = [] -wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support - [dependencies] reqwest = { workspace = true } serde = { workspace = true } @@ -26,7 +22,5 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ "std", "macros", ] } -tsify = { workspace = true, optional = true, features = ["js"], default-features = false } url = ">=2.5, <3" uuid = { workspace = true } -wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } From 3bf880f858feee8b102365c05f061114de42e0a4 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 8 Aug 2025 10:39:54 +0100 Subject: [PATCH 18/29] docs --- .../bitwarden-core/src/key_management/master_password.rs | 8 +++++++- .../bitwarden-core/src/key_management/user_decryption.rs | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 5d26fdaa8..f6767330b 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,4 +1,6 @@ -#![allow(missing_docs)] +//! Mobile specific master password operations +//! +//! This module contains the data structures and error handling for master password unlock operations. use std::num::NonZeroU32; @@ -13,6 +15,8 @@ use wasm_bindgen::prelude::*; use crate::{require, MissingFieldError}; +/// Error for master password related operations. +#[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum MasterPasswordError { @@ -22,6 +26,8 @@ pub enum MasterPasswordError { MissingField(#[from] MissingFieldError), } +/// Represents the data required to unlock with the master password. +#[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 18b3d7e10..c752cf193 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -1,4 +1,6 @@ -#![allow(missing_docs)] +//! Mobile specific user decryption operations +//! +//! This module contains the data structures and error handling for user decryption operations, use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_error::bitwarden_error; @@ -8,6 +10,8 @@ use wasm_bindgen::prelude::*; use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; +/// Error for master user decryption related operations. +#[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum UserDecryptionError { @@ -15,6 +19,9 @@ pub enum UserDecryptionError { MasterPasswordError(#[from] MasterPasswordError), } +/// Represents data required to decrypt user's vault. +/// Currently, this is only used for master password unlock. +#[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] From 93a3d0c7bb26898fc85eab9c22aab3540b844c21 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 8 Aug 2025 10:41:56 +0100 Subject: [PATCH 19/29] formatting --- crates/bitwarden-core/src/key_management/master_password.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index f6767330b..19c570570 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,6 +1,7 @@ //! Mobile specific master password operations //! -//! This module contains the data structures and error handling for master password unlock operations. +//! This module contains the data structures and error handling for master password unlock +//! operations. use std::num::NonZeroU32; From 3e6a46a79d9d3651445156c57feb2d57455623f4 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 13 Aug 2025 18:00:20 +0100 Subject: [PATCH 20/29] revert user decryption response parsing in wasm --- Cargo.lock | 1 - ...ster_password_unlock_kdf_response_model.rs | 7 --- .../master_password_unlock_response_model.rs | 15 ++---- .../models/user_decryption_response_model.rs | 7 --- .../src/key_management/master_password.rs | 10 ---- .../src/key_management/user_decryption.rs | 12 +---- crates/bitwarden-wasm-internal/Cargo.toml | 1 - .../src/custom_types.rs | 5 -- .../src/key_management/mod.rs | 1 - .../src/key_management/user_decryption.rs | 24 ---------- crates/bitwarden-wasm-internal/src/lib.rs | 1 - support/openapi-template/Cargo.mustache | 46 +++++-------------- support/openapi-template/model.mustache | 46 ------------------- 13 files changed, 17 insertions(+), 159 deletions(-) delete mode 100644 crates/bitwarden-wasm-internal/src/key_management/mod.rs delete mode 100644 crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs diff --git a/Cargo.lock b/Cargo.lock index 932ab302a..4cf7352e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -768,7 +768,6 @@ version = "0.1.0" dependencies = [ "async-trait", "base64", - "bitwarden-api-api", "bitwarden-auth", "bitwarden-core", "bitwarden-crypto", diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs index af98f1d00..38e02d030 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_kdf_response_model.rs @@ -9,17 +9,10 @@ */ use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub struct MasterPasswordUnlockKdfResponseModel { #[serde(rename = "kdfType")] pub kdf_type: models::KdfType, diff --git a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs index a161fa1dd..d6ec1c676 100644 --- a/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs +++ b/crates/bitwarden-api-api/src/models/master_password_unlock_response_model.rs @@ -9,31 +9,24 @@ */ use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub struct MasterPasswordUnlockResponseModel { #[serde(rename = "kdf")] pub kdf: Box, #[serde(rename = "masterKeyEncryptedUserKey")] - pub master_key_encrypted_user_key: String, + pub master_key_encrypted_user_key: Option, #[serde(rename = "salt")] - pub salt: String, + pub salt: Option, } impl MasterPasswordUnlockResponseModel { pub fn new( kdf: models::MasterPasswordUnlockKdfResponseModel, - master_key_encrypted_user_key: String, - salt: String, + master_key_encrypted_user_key: Option, + salt: Option, ) -> MasterPasswordUnlockResponseModel { MasterPasswordUnlockResponseModel { kdf: Box::new(kdf), diff --git a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs index b53e730ed..94ed72a69 100644 --- a/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs +++ b/crates/bitwarden-api-api/src/models/user_decryption_response_model.rs @@ -9,17 +9,10 @@ */ use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; use crate::models; #[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub struct UserDecryptionResponseModel { #[serde( rename = "masterPasswordUnlock", diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 19c570570..85b874391 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,5 +1,3 @@ -//! Mobile specific master password operations -//! //! This module contains the data structures and error handling for master password unlock //! operations. @@ -11,8 +9,6 @@ use bitwarden_api_api::models::{ use bitwarden_crypto::{CryptoError, EncString, Kdf}; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::*; use crate::{require, MissingFieldError}; @@ -31,12 +27,6 @@ pub enum MasterPasswordError { #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub struct MasterPasswordUnlockData { pub kdf: Kdf, pub master_key_wrapped_user_key: EncString, diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index c752cf193..7a606ab21 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -1,12 +1,8 @@ -//! Mobile specific user decryption operations -//! -//! This module contains the data structures and error handling for user decryption operations, +//! This module contains the data structures and error handling for user decryption operations. use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::*; use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; @@ -24,12 +20,6 @@ pub enum UserDecryptionError { #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] pub struct UserDecryptionData { pub master_password_unlock: Option, } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 87da34516..a85384f6e 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -18,7 +18,6 @@ crate-type = ["cdylib"] [dependencies] async-trait = { workspace = true } base64 = ">=0.22.1, <0.23.0" -bitwarden-api-api = { workspace = true, features = ["wasm"] } bitwarden-auth = { workspace = true, features = ["wasm"] } bitwarden-core = { workspace = true, features = ["wasm", "internal"] } bitwarden-crypto = { workspace = true, features = ["wasm"] } diff --git a/crates/bitwarden-wasm-internal/src/custom_types.rs b/crates/bitwarden-wasm-internal/src/custom_types.rs index e70056983..f910d1173 100644 --- a/crates/bitwarden-wasm-internal/src/custom_types.rs +++ b/crates/bitwarden-wasm-internal/src/custom_types.rs @@ -30,9 +30,4 @@ export type Utc = unknown; * An integer that is known not to equal zero. */ export type NonZeroU32 = number; - -/** - * An interger that is valid KdfType - */ -export type KdfType = number; "#; diff --git a/crates/bitwarden-wasm-internal/src/key_management/mod.rs b/crates/bitwarden-wasm-internal/src/key_management/mod.rs deleted file mode 100644 index 021fa70d6..000000000 --- a/crates/bitwarden-wasm-internal/src/key_management/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod user_decryption; diff --git a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs b/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs deleted file mode 100644 index dd527d0c6..000000000 --- a/crates/bitwarden-wasm-internal/src/key_management/user_decryption.rs +++ /dev/null @@ -1,24 +0,0 @@ -use bitwarden_api_api::models::{MasterPasswordUnlockResponseModel, UserDecryptionResponseModel}; -use bitwarden_core::key_management::{ - master_password::{MasterPasswordError, MasterPasswordUnlockData}, - user_decryption::{UserDecryptionData, UserDecryptionError}, -}; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen] -pub struct UserDecryption {} - -#[wasm_bindgen] -impl UserDecryption { - pub fn get_user_decryption_data( - response: UserDecryptionResponseModel, - ) -> Result { - UserDecryptionData::try_from(response) - } - - pub fn get_master_password_unlock_data( - response: MasterPasswordUnlockResponseModel, - ) -> Result { - MasterPasswordUnlockData::try_from(response) - } -} diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index 0e9951b4f..253ec8ffd 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -3,7 +3,6 @@ mod client; mod custom_types; mod init; -mod key_management; mod platform; mod pure_crypto; mod ssh; diff --git a/support/openapi-template/Cargo.mustache b/support/openapi-template/Cargo.mustache index 4a71adb53..cdaf12fc9 100644 --- a/support/openapi-template/Cargo.mustache +++ b/support/openapi-template/Cargo.mustache @@ -14,24 +14,16 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true -[features] -default = [] -wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support - [dependencies] -serde = { workspace = true, features = ["derive"] } +serde = { version = "^1.0", features = ["derive"] } {{#serdeWith}} -serde_with = { version = ">=3.8, <4", default-features = false, features = [ - "base64", - "std", - "macros" -] } +serde_with = { version = "^3.8", default-features = false, features = ["base64", "std", "macros"] } {{/serdeWith}} -serde_json = { workspace = true } -serde_repr = { workspace = true } -url = ">=2.5, <3" +serde_json = "^1.0" +serde_repr = "^0.1" +url = "^2.5" {{#hasUUIDs}} -uuid = { workspace = true, features = ["serde", "v4", "js"] } +uuid = { version = "^1.8", features = ["serde", "v4"] } {{/hasUUIDs}} {{#hyper}} {{#hyper0x}} @@ -54,39 +46,27 @@ secrecy = "0.8.0" {{/withAWSV4Signature}} {{#reqwest}} {{^supportAsync}} -reqwest = { workspace = true, features = [ - "json", - "blocking", - "multipart", - "http2", -], default-features = false } +reqwest = { version = "^0.12", default-features = false, features = ["json", "blocking", "multipart", "http2"] } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "blocking", "multipart"] } {{/supportMiddleware}} {{/supportAsync}} {{#supportAsync}} -reqwest = { workspace = true, features = [ - "json", - "multipart", - "http2", -], default-features = false } +reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } {{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} -async-trait = { workspace = true } +async-trait = "^0.1" # TODO: propose to Yoshidan to externalize this as non google related crate, so that it can easily be extended for other cloud providers. google-cloud-token = "^0.1" {{/supportTokenSource}} {{/supportAsync}} {{/reqwest}} {{#reqwestTrait}} -async-trait = { workspace = true } -reqwest = { workspace = true, features = [ - "json", - "multipart", - "http2", -], default-features = false }{{#supportMiddleware}} +async-trait = "^0.1" +reqwest = { version = "^0.12", default-features = false, features = ["json", "multipart", "http2"] } +{{#supportMiddleware}} reqwest-middleware = { version = "^0.4", features = ["json", "multipart"] } {{/supportMiddleware}} {{#supportTokenSource}} @@ -110,5 +90,3 @@ mockall = ["dep:mockall"] bon = ["dep:bon"] {{/useBonBuilder}} {{/reqwestTrait}} -tsify = { workspace = true, optional = true, features = ["js"], default-features = false } -wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } \ No newline at end of file diff --git a/support/openapi-template/model.mustache b/support/openapi-template/model.mustache index f10433332..8652cfe1c 100644 --- a/support/openapi-template/model.mustache +++ b/support/openapi-template/model.mustache @@ -3,10 +3,6 @@ use crate::models; use serde::{Deserialize, Serialize}; {{#models}} {{#model}} -{{#vendorExtensions.x-sdk-wasm}} -#[cfg(feature = "wasm")] -use wasm_bindgen::prelude::wasm_bindgen; -{{/vendorExtensions.x-sdk-wasm}} {{^isEnum}}{{#vendorExtensions.x-rust-has-byte-array}} use serde_with::serde_as; {{/vendorExtensions.x-rust-has-byte-array}}{{/isEnum}} @@ -24,13 +20,6 @@ use serde_repr::{Serialize_repr,Deserialize_repr}; /// {{{description}}} #[repr(i64)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize_repr, Deserialize_repr)] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -56,13 +45,6 @@ impl std::fmt::Display for {{{classname}}} { {{^isInteger}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{#allowableValues}} {{#enumVars}} @@ -96,13 +78,6 @@ impl Default for {{{classname}}} { {{#discriminator}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "{{{propertyBaseName}}}")] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub enum {{{classname}}} { {{^oneOf}} {{#mappedModels}} @@ -148,13 +123,6 @@ impl Default for {{classname}} { {{^discriminator}} {{#vendorExtensions.x-rust-has-byte-array}}#[serde_as] {{/vendorExtensions.x-rust-has-byte-array}}{{#oneOf.isEmpty}}#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub struct {{{classname}}} { {{#vars}} {{#description}} @@ -212,13 +180,6 @@ impl {{{classname}}} { {{/description}} #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(untagged)] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub enum {{classname}} { {{#composedSchemas.oneOf}} {{#description}} @@ -241,13 +202,6 @@ impl Default for {{classname}} { {{#isEnum}} /// {{{description}}} #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] -{{#vendorExtensions.x-sdk-wasm}} -#[cfg_attr( - feature = "wasm", - derive(tsify::Tsify), - tsify(into_wasm_abi, from_wasm_abi) -)] -{{/vendorExtensions.x-sdk-wasm}} pub enum {{{enumName}}} { {{#allowableValues}} {{#enumVars}} From 7c34725f8178211e051df8011d2e6870ca19c80c Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 13 Aug 2025 18:11:32 +0100 Subject: [PATCH 21/29] fixed unit test --- .../src/key_management/master_password.rs | 87 ++++++++++++++----- .../src/key_management/user_decryption.rs | 8 +- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 85b874391..73729b2ec 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -60,10 +60,13 @@ impl TryFrom for MasterPasswordUnlockData { }, }; + let master_key_encrypted_user_key = require!(response.master_key_encrypted_user_key); + let salt = require!(response.salt); + Ok(MasterPasswordUnlockData { kdf, - master_key_wrapped_user_key: response.master_key_encrypted_user_key.as_str().parse()?, - salt: response.salt, + master_key_wrapped_user_key: master_key_encrypted_user_key.as_str().parse()?, + salt, }) } } @@ -97,8 +100,8 @@ mod tests { memory: None, parallelism: None, }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -125,8 +128,8 @@ mod tests { memory: Some(64), parallelism: Some(4), }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -159,14 +162,58 @@ mod tests { memory: None, parallelism: None, }), - master_key_encrypted_user_key: TEST_INVALID_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_INVALID_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); assert!(matches!(result, Err(MasterPasswordError::Crypto(_)))); } + #[test] + fn test_try_from_master_password_unlock_response_model_user_key_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: None, + salt: Some(TEST_SALT.to_string()), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.master_key_encrypted_user_key" + ))) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_salt_none_error() { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: None, + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.salt" + ))) + )); + } + #[test] fn test_try_from_master_password_unlock_response_model_argon2id_memory_none_error() { let response = MasterPasswordUnlockResponseModel { @@ -176,8 +223,8 @@ mod tests { memory: None, parallelism: Some(4), }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -198,8 +245,8 @@ mod tests { memory: Some(0), parallelism: Some(4), }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -220,8 +267,8 @@ mod tests { memory: Some(64), parallelism: None, }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -242,8 +289,8 @@ mod tests { memory: Some(64), parallelism: Some(0), }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -264,8 +311,8 @@ mod tests { memory: None, parallelism: None, }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); @@ -286,8 +333,8 @@ mod tests { memory: Some(64), parallelism: Some(4), }), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), }; let result = MasterPasswordUnlockData::try_from(response); diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 7a606ab21..81c2d7acf 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -61,8 +61,8 @@ mod tests { parallelism: Some(4), }, ), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), })), }; @@ -123,8 +123,8 @@ mod tests { parallelism: None, }, ), - master_key_encrypted_user_key: TEST_USER_KEY.to_string(), - salt: TEST_SALT.to_string(), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), })), }; From 042678c3c17ea99db57f8ad96f7975036d087cfd Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 13 Aug 2025 18:32:47 +0100 Subject: [PATCH 22/29] revert wasm dependencies --- Cargo.lock | 2 -- crates/bitwarden-api-api/Cargo.toml | 6 ------ 2 files changed, 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cf7352e2..14943a298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,10 +326,8 @@ dependencies = [ "serde_json", "serde_repr", "serde_with", - "tsify", "url", "uuid", - "wasm-bindgen", ] [[package]] diff --git a/crates/bitwarden-api-api/Cargo.toml b/crates/bitwarden-api-api/Cargo.toml index 8266d4348..1c68a6cf1 100644 --- a/crates/bitwarden-api-api/Cargo.toml +++ b/crates/bitwarden-api-api/Cargo.toml @@ -12,10 +12,6 @@ repository.workspace = true license-file.workspace = true keywords.workspace = true -[features] -default = [] -wasm = ["dep:tsify", "dep:wasm-bindgen"] # WASM support - [dependencies] reqwest = { workspace = true } serde = { workspace = true } @@ -26,7 +22,5 @@ serde_with = { version = ">=3.8, <4", default-features = false, features = [ "std", "macros", ] } -tsify = { workspace = true, optional = true, features = ["js"], default-features = false } url = ">=2.5, <3" uuid = { workspace = true } -wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] } From 9676069424d62c6257e546f7fbf6d6f7445579dc Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Wed, 13 Aug 2025 20:50:44 +0100 Subject: [PATCH 23/29] bring back wasm and uniffi bindings to MasterPasswordUnlockData, error and docs improvements --- .../src/key_management/master_password.rs | 136 +++++++++--------- .../src/key_management/user_decryption.rs | 12 +- 2 files changed, 70 insertions(+), 78 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 73729b2ec..716257986 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -6,30 +6,44 @@ use std::num::NonZeroU32; use bitwarden_api_api::models::{ master_password_unlock_response_model::MasterPasswordUnlockResponseModel, KdfType, }; -use bitwarden_crypto::{CryptoError, EncString, Kdf}; +use bitwarden_crypto::{EncString, Kdf}; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; use crate::{require, MissingFieldError}; /// Error for master password related operations. -#[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum MasterPasswordError { - #[error(transparent)] - Crypto(#[from] CryptoError), + /// The wrapped encryption key could not be parsed because the encstring is malformed + #[error("Wrapped encryption key is malformed")] + EncryptionKeyMalformed, + /// The KDF data could not be parsed, because it is missing values or has an invalid value + #[error("KDF is malformed")] + KdfMalformed, + /// The wrapped encryption key or salt fields are missing or KDF data is malformed #[error(transparent)] MissingField(#[from] MissingFieldError), } /// Represents the data required to unlock with the master password. -#[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr( + feature = "wasm", + derive(tsify::Tsify), + tsify(into_wasm_abi, from_wasm_abi) +)] pub struct MasterPasswordUnlockData { + /// The key derivation function used to derive the master key pub kdf: Kdf, + /// The master key wrapped user key pub master_key_wrapped_user_key: EncString, + /// The salt used in the KDF, typically the user's email pub salt: String, } @@ -39,23 +53,21 @@ impl TryFrom for MasterPasswordUnlockData { fn try_from(response: MasterPasswordUnlockResponseModel) -> Result { let kdf = match response.kdf.kdf_type { KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { - iterations: parse_nonzero_u32( - response.kdf.iterations, - stringify!(response.kdf.iterations), - )?, + iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?, }, KdfType::Argon2id => Kdf::Argon2id { - iterations: parse_nonzero_u32( - response.kdf.iterations, - stringify!(response.kdf.iterations), - )?, - memory: parse_nonzero_u32( - require!(response.kdf.memory), - stringify!(response.kdf.memory), + iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?, + memory: kdf_parse_nonzero_u32( + response + .kdf + .memory + .ok_or(MasterPasswordError::KdfMalformed)?, )?, - parallelism: parse_nonzero_u32( - require!(response.kdf.parallelism), - stringify!(response.kdf.parallelism), + parallelism: kdf_parse_nonzero_u32( + response + .kdf + .parallelism + .ok_or(MasterPasswordError::KdfMalformed)?, )?, }, }; @@ -65,20 +77,20 @@ impl TryFrom for MasterPasswordUnlockData { Ok(MasterPasswordUnlockData { kdf, - master_key_wrapped_user_key: master_key_encrypted_user_key.as_str().parse()?, + master_key_wrapped_user_key: master_key_encrypted_user_key + .as_str() + .parse() + .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?, salt, }) } } -fn parse_nonzero_u32( - value: impl TryInto, - field_name: &'static str, -) -> Result { +fn kdf_parse_nonzero_u32(value: impl TryInto) -> Result { let num: u32 = value .try_into() - .map_err(|_| MissingFieldError(field_name))?; - NonZeroU32::new(num).ok_or(MissingFieldError(field_name)) + .map_err(|_| MasterPasswordError::KdfMalformed)?; + NonZeroU32::new(num).ok_or(MasterPasswordError::KdfMalformed) } #[cfg(test)] @@ -154,7 +166,8 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_invalid_user_key_crypto_error() { + fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_key_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, @@ -167,11 +180,14 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!(result, Err(MasterPasswordError::Crypto(_)))); + assert!(matches!( + result, + Err(MasterPasswordError::EncryptionKeyMalformed) + )); } #[test] - fn test_try_from_master_password_unlock_response_model_user_key_none_error() { + fn test_try_from_master_password_unlock_response_model_user_key_none_missing_field_error() { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, @@ -193,7 +209,7 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_salt_none_error() { + fn test_try_from_master_password_unlock_response_model_salt_none_missing_field_error() { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, @@ -215,7 +231,8 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_memory_none_error() { + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_none_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, @@ -228,16 +245,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.memory" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_memory_zero_error() { + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_zero_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, @@ -250,16 +263,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.memory" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_none_error() { + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, @@ -272,16 +281,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.parallelism" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_parallelism_zero_error() { + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_zero_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, @@ -294,16 +299,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.parallelism" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] - fn test_try_from_master_password_unlock_response_model_pbkdf2_iterations_zero_error() { + fn test_try_from_master_password_unlock_response_model_pbkdf2_kdf_iterations_zero_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, @@ -316,16 +317,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.iterations" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_iterations_zero_error() { + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_iterations_zero_kdf_malformed_error( + ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::Argon2id, @@ -338,12 +335,7 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!( - result, - Err(MasterPasswordError::MissingField(MissingFieldError( - "response.kdf.iterations" - ))) - )); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } #[test] diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 81c2d7acf..6673a9cb3 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -7,20 +7,20 @@ use serde::{Deserialize, Serialize}; use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; /// Error for master user decryption related operations. -#[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, thiserror::Error)] pub enum UserDecryptionError { + /// Error related to master password unlock. #[error(transparent)] MasterPasswordError(#[from] MasterPasswordError), } /// Represents data required to decrypt user's vault. /// Currently, this is only used for master password unlock. -#[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct UserDecryptionData { + /// Optional master password unlock data. pub master_password_unlock: Option, } @@ -112,19 +112,19 @@ mod tests { } #[test] - fn test_try_from_user_decryption_response_model_missing_field_error() { + fn test_try_from_user_decryption_response_model_salt_none_missing_field_error() { let response = UserDecryptionResponseModel { master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { kdf: Box::new( bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::Argon2id, - iterations: 3, + kdf_type: KdfType::PBKDF2_SHA256, + iterations: 600_000, memory: None, parallelism: None, }, ), master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), + salt: None, })), }; From 7973f12acc05ab9bc133262a984428f6e85f5fdf Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 15 Aug 2025 13:14:04 +0100 Subject: [PATCH 24/29] identity separate user decryption options response model --- .../auth/api/response/identity_success_response.rs | 5 +++-- crates/bitwarden-core/src/auth/api/response/mod.rs | 1 + .../api/response/user_decryption_options_response.rs | 11 +++++++++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs 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 08378ad7c..2d4acfde1 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 @@ -1,10 +1,11 @@ use std::{collections::HashMap, num::NonZeroU32}; -use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::auth::api::response::user_decryption_options_response::UserDecryptionOptionsResponseModel; + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct IdentityTokenSuccessResponse { pub access_token: String, @@ -37,7 +38,7 @@ pub struct IdentityTokenSuccessResponse { key_connector_url: Option, #[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")] - pub user_decryption_options: Option, + pub user_decryption_options: Option, /// Stores unknown api response fields extra: Option>, diff --git a/crates/bitwarden-core/src/auth/api/response/mod.rs b/crates/bitwarden-core/src/auth/api/response/mod.rs index 482d1f9e7..2264398d6 100644 --- a/crates/bitwarden-core/src/auth/api/response/mod.rs +++ b/crates/bitwarden-core/src/auth/api/response/mod.rs @@ -6,6 +6,7 @@ mod identity_token_response; mod identity_two_factor_response; pub(crate) mod two_factor_provider_data; mod two_factor_providers; +mod user_decryption_options_response; pub(crate) use identity_payload_response::*; pub(crate) use identity_refresh_response::*; 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 new file mode 100644 index 000000000..86255e276 --- /dev/null +++ b/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs @@ -0,0 +1,11 @@ +use bitwarden_api_api::models::MasterPasswordUnlockResponseModel; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct UserDecryptionOptionsResponseModel { + #[serde( + rename = "masterPasswordUnlock", + skip_serializing_if = "Option::is_none" + )] + pub master_password_unlock: Option, +} From 5a4f3140c69467ba4297664657f420a99091be3c Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 15 Aug 2025 13:34:48 +0100 Subject: [PATCH 25/29] review suggestions --- .../src/key_management/master_password.rs | 201 +++++------------- .../bitwarden-core/src/key_management/mod.rs | 9 +- .../src/key_management/user_decryption.rs | 101 --------- 3 files changed, 60 insertions(+), 251 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index 716257986..c68f92833 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -1,6 +1,3 @@ -//! This module contains the data structures and error handling for master password unlock -//! operations. - use std::num::NonZeroU32; use bitwarden_api_api::models::{ @@ -78,7 +75,6 @@ impl TryFrom for MasterPasswordUnlockData { Ok(MasterPasswordUnlockData { kdf, master_key_wrapped_user_key: master_key_encrypted_user_key - .as_str() .parse() .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?, salt, @@ -87,10 +83,11 @@ impl TryFrom for MasterPasswordUnlockData { } fn kdf_parse_nonzero_u32(value: impl TryInto) -> Result { - let num: u32 = value + value .try_into() - .map_err(|_| MasterPasswordError::KdfMalformed)?; - NonZeroU32::new(num).ok_or(MasterPasswordError::KdfMalformed) + .ok() + .and_then(NonZeroU32::new) + .ok_or(MasterPasswordError::KdfMalformed) } #[cfg(test)] @@ -103,28 +100,37 @@ mod tests { const TEST_INVALID_USER_KEY: &str = "-1.8UClLa8IPE1iZT7chy5wzQ==|6PVfHnVk5S3XqEtQemnM5yb4JodxmPkkWzmDRdfyHtjORmvxqlLX40tBJZ+CKxQWmS8tpEB5w39rbgHg/gqs0haGdZG4cPbywsgGzxZ7uNI="; const TEST_SALT: &str = "test@example.com"; - #[test] - fn test_try_from_master_password_unlock_response_model_pbkdf2_success() { - let response = MasterPasswordUnlockResponseModel { + fn create_pbkdf2_response( + master_key_encrypted_user_key: Option, + salt: Option, + iterations: i32, + ) -> MasterPasswordUnlockResponseModel { + MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, + iterations, memory: None, parallelism: None, }), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }; + master_key_encrypted_user_key, + salt, + } + } - let result = MasterPasswordUnlockData::try_from(response); - assert!(result.is_ok()); - let data = result.unwrap(); - - match data.kdf { - Kdf::PBKDF2 { iterations } => { - assert_eq!(iterations.get(), 600_000); - } - _ => panic!("Expected PBKDF2 KDF"), + #[test] + fn test_try_from_master_password_unlock_response_model_pbkdf2_success() { + let response = create_pbkdf2_response( + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + 600_000, + ); + + let data = MasterPasswordUnlockData::try_from(response).unwrap(); + + if let Kdf::PBKDF2 { iterations } = data.kdf { + assert_eq!(iterations.get(), 600_000); + } else { + panic!("Expected PBKDF2 KDF") } assert_eq!(data.salt, TEST_SALT); @@ -144,21 +150,19 @@ mod tests { salt: Some(TEST_SALT.to_string()), }; - let result = MasterPasswordUnlockData::try_from(response); - assert!(result.is_ok()); - let data = result.unwrap(); - - match data.kdf { - Kdf::Argon2id { - iterations, - memory, - parallelism, - } => { - assert_eq!(iterations.get(), 3); - assert_eq!(memory.get(), 64); - assert_eq!(parallelism.get(), 4); - } - _ => panic!("Expected Argon2id KDF"), + let data = MasterPasswordUnlockData::try_from(response).unwrap(); + + if let Kdf::Argon2id { + iterations, + memory, + parallelism, + } = data.kdf + { + assert_eq!(iterations.get(), 3); + assert_eq!(memory.get(), 64); + assert_eq!(parallelism.get(), 4); + } else { + panic!("Expected Argon2id KDF") } assert_eq!(data.salt, TEST_SALT); @@ -168,16 +172,11 @@ mod tests { #[test] fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_key_malformed_error( ) { - let response = MasterPasswordUnlockResponseModel { - kdf: Box::new(MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }), - master_key_encrypted_user_key: Some(TEST_INVALID_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }; + let response = create_pbkdf2_response( + Some(TEST_INVALID_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + 600_000, + ); let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( @@ -188,16 +187,7 @@ mod tests { #[test] fn test_try_from_master_password_unlock_response_model_user_key_none_missing_field_error() { - let response = MasterPasswordUnlockResponseModel { - kdf: Box::new(MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }), - master_key_encrypted_user_key: None, - salt: Some(TEST_SALT.to_string()), - }; + let response = create_pbkdf2_response(None, Some(TEST_SALT.to_string()), 600_000); let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( @@ -210,16 +200,7 @@ mod tests { #[test] fn test_try_from_master_password_unlock_response_model_salt_none_missing_field_error() { - let response = MasterPasswordUnlockResponseModel { - kdf: Box::new(MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: None, - }; + let response = create_pbkdf2_response(Some(TEST_USER_KEY.to_string()), None, 600_000); let result = MasterPasswordUnlockData::try_from(response); assert!(matches!( @@ -305,16 +286,11 @@ mod tests { #[test] fn test_try_from_master_password_unlock_response_model_pbkdf2_kdf_iterations_zero_kdf_malformed_error( ) { - let response = MasterPasswordUnlockResponseModel { - kdf: Box::new(MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 0, - memory: None, - parallelism: None, - }), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - }; + let response = create_pbkdf2_response( + Some(TEST_USER_KEY.to_string()), + Some(TEST_SALT.to_string()), + 0, + ); let result = MasterPasswordUnlockData::try_from(response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); @@ -337,73 +313,4 @@ mod tests { let result = MasterPasswordUnlockData::try_from(response); assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); } - - #[test] - fn test_serde_serialization_pbkdf2() { - let data = MasterPasswordUnlockData { - kdf: Kdf::PBKDF2 { - iterations: 600_000.try_into().unwrap(), - }, - master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), - salt: TEST_SALT.to_string(), - }; - - let serialized = serde_json::to_string(&data).unwrap(); - let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); - - match (data.kdf, deserialized.kdf) { - (Kdf::PBKDF2 { iterations: i1 }, Kdf::PBKDF2 { iterations: i2 }) => { - assert_eq!(i1, i2); - } - _ => panic!("KDF types don't match"), - } - - assert_eq!( - data.master_key_wrapped_user_key.to_string(), - deserialized.master_key_wrapped_user_key.to_string() - ); - assert_eq!(data.salt, deserialized.salt); - } - - #[test] - fn test_serde_serialization_argon2id() { - let data = MasterPasswordUnlockData { - kdf: Kdf::Argon2id { - iterations: 3.try_into().unwrap(), - memory: 64.try_into().unwrap(), - parallelism: 4.try_into().unwrap(), - }, - master_key_wrapped_user_key: TEST_USER_KEY.parse().unwrap(), - salt: TEST_SALT.to_string(), - }; - - let serialized = serde_json::to_string(&data).unwrap(); - let deserialized: MasterPasswordUnlockData = serde_json::from_str(&serialized).unwrap(); - - match (data.kdf, deserialized.kdf) { - ( - Kdf::Argon2id { - iterations: i1, - memory: m1, - parallelism: p1, - }, - Kdf::Argon2id { - iterations: i2, - memory: m2, - parallelism: p2, - }, - ) => { - assert_eq!(i1, i2); - assert_eq!(m1, m2); - assert_eq!(p1, p2); - } - _ => panic!("KDF types don't match"), - } - - assert_eq!( - data.master_key_wrapped_user_key.to_string(), - deserialized.master_key_wrapped_user_key.to_string() - ); - assert_eq!(data.salt, deserialized.salt); - } } diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 3fc1bb5bf..aa33bab3e 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -19,14 +19,17 @@ mod crypto_client; pub use crypto_client::CryptoClient; #[cfg(feature = "internal")] -pub mod master_password; +mod master_password; +#[cfg(feature = "internal")] +pub use master_password::*; #[cfg(feature = "internal")] mod security_state; #[cfg(feature = "internal")] -pub mod user_decryption; - +mod user_decryption; #[cfg(feature = "internal")] pub use security_state::{SecurityState, SignedSecurityState}; +#[cfg(feature = "internal")] +pub use user_decryption::*; key_ids! { #[symmetric] diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 6673a9cb3..be6b6259c 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -1,5 +1,3 @@ -//! This module contains the data structures and error handling for user decryption operations. - use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; @@ -38,102 +36,3 @@ impl TryFrom for UserDecryptionData { }) } } - -#[cfg(test)] -mod tests { - use bitwarden_api_api::models::{KdfType, MasterPasswordUnlockResponseModel}; - use bitwarden_crypto::Kdf; - - use super::*; - - const TEST_USER_KEY: &str = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE="; - const TEST_SALT: &str = "test@example.com"; - - #[test] - fn test_try_from_user_decryption_response_model_success() { - let response = UserDecryptionResponseModel { - master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { - kdf: Box::new( - bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::Argon2id, - iterations: 3, - memory: Some(64), - parallelism: Some(4), - }, - ), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: Some(TEST_SALT.to_string()), - })), - }; - - let result = UserDecryptionData::try_from(response); - assert!(result.is_ok()); - - let user_decryption_data = result.unwrap(); - - assert!(user_decryption_data.master_password_unlock.is_some()); - - let master_password_unlock = user_decryption_data.master_password_unlock.unwrap(); - - match master_password_unlock.kdf { - Kdf::Argon2id { - iterations, - memory, - parallelism, - } => { - assert_eq!(iterations.get(), 3); - assert_eq!(memory.get(), 64); - assert_eq!(parallelism.get(), 4); - } - _ => panic!("Expected Argon2id KDF"), - } - - assert_eq!(master_password_unlock.salt, TEST_SALT); - assert_eq!( - master_password_unlock - .master_key_wrapped_user_key - .to_string(), - TEST_USER_KEY - ); - } - - #[test] - fn test_try_from_user_decryption_response_model_master_password_unlock_none_success() { - let response = UserDecryptionResponseModel { - master_password_unlock: None, - }; - - let result = UserDecryptionData::try_from(response); - assert!(result.is_ok()); - - let user_decryption_data = result.unwrap(); - - assert!(user_decryption_data.master_password_unlock.is_none()); - } - - #[test] - fn test_try_from_user_decryption_response_model_salt_none_missing_field_error() { - let response = UserDecryptionResponseModel { - master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel { - kdf: Box::new( - bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel { - kdf_type: KdfType::PBKDF2_SHA256, - iterations: 600_000, - memory: None, - parallelism: None, - }, - ), - master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), - salt: None, - })), - }; - - let result = UserDecryptionData::try_from(response); - assert!(matches!( - result, - Err(UserDecryptionError::MasterPasswordError( - MasterPasswordError::MissingField(_) - )) - )); - } -} From 7e425ca5fc6cb2ec2ed6787f905ea5a4aeaaf616 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 15 Aug 2025 14:06:24 +0100 Subject: [PATCH 26/29] identity name prefix for UserDecryptionOptions --- .../src/auth/api/response/identity_success_response.rs | 4 ++-- ...sponse.rs => identity_user_decryption_options_response.rs} | 2 +- crates/bitwarden-core/src/auth/api/response/mod.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename crates/bitwarden-core/src/auth/api/response/{user_decryption_options_response.rs => identity_user_decryption_options_response.rs} (85%) 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 2d4acfde1..9eb8de69e 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 @@ -4,7 +4,7 @@ use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::auth::api::response::user_decryption_options_response::UserDecryptionOptionsResponseModel; +use crate::auth::api::response::identity_user_decryption_options_response::IdentityUserDecryptionOptionsResponseModel; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct IdentityTokenSuccessResponse { @@ -38,7 +38,7 @@ pub struct IdentityTokenSuccessResponse { key_connector_url: Option, #[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")] - pub user_decryption_options: Option, + pub 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/identity_user_decryption_options_response.rs similarity index 85% rename from crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs rename to crates/bitwarden-core/src/auth/api/response/identity_user_decryption_options_response.rs index 86255e276..a74b0c13a 100644 --- a/crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs +++ b/crates/bitwarden-core/src/auth/api/response/identity_user_decryption_options_response.rs @@ -2,7 +2,7 @@ use bitwarden_api_api::models::MasterPasswordUnlockResponseModel; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct UserDecryptionOptionsResponseModel { +pub struct IdentityUserDecryptionOptionsResponseModel { #[serde( rename = "masterPasswordUnlock", skip_serializing_if = "Option::is_none" diff --git a/crates/bitwarden-core/src/auth/api/response/mod.rs b/crates/bitwarden-core/src/auth/api/response/mod.rs index 2264398d6..ca26e0b14 100644 --- a/crates/bitwarden-core/src/auth/api/response/mod.rs +++ b/crates/bitwarden-core/src/auth/api/response/mod.rs @@ -4,9 +4,9 @@ mod identity_success_response; mod identity_token_fail_response; mod identity_token_response; mod identity_two_factor_response; +pub(crate) mod identity_user_decryption_options_response; pub(crate) mod two_factor_provider_data; mod two_factor_providers; -mod user_decryption_options_response; pub(crate) use identity_payload_response::*; pub(crate) use identity_refresh_response::*; From c9aaa8cbe1b4cb11bd83cb772b71fb81af81117e Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 15 Aug 2025 14:06:52 +0100 Subject: [PATCH 27/29] IdentityUserDecryptionOptionsResponseModel mapping to UserDecryptionData --- .../src/key_management/user_decryption.rs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index be6b6259c..408a8fdde 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -2,7 +2,10 @@ use bitwarden_api_api::models::UserDecryptionResponseModel; use bitwarden_error::bitwarden_error; use serde::{Deserialize, Serialize}; -use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}; +use crate::{ + auth::api::response::identity_user_decryption_options_response::IdentityUserDecryptionOptionsResponseModel, + key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData}, +}; /// Error for master user decryption related operations. #[bitwarden_error(flat)] @@ -36,3 +39,18 @@ impl TryFrom for UserDecryptionData { }) } } + +impl TryFrom for UserDecryptionData { + type Error = UserDecryptionError; + + fn try_from(response: IdentityUserDecryptionOptionsResponseModel) -> Result { + let master_password_unlock = response + .master_password_unlock + .map(|response| MasterPasswordUnlockData::try_from(response)) + .transpose()?; + + Ok(UserDecryptionData { + master_password_unlock, + }) + } +} From 25917734b39365f3bdbcc85584cf5f0a60432a07 Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Fri, 15 Aug 2025 14:09:36 +0100 Subject: [PATCH 28/29] clippy --- crates/bitwarden-core/src/key_management/user_decryption.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-core/src/key_management/user_decryption.rs b/crates/bitwarden-core/src/key_management/user_decryption.rs index 408a8fdde..d31dea4cf 100644 --- a/crates/bitwarden-core/src/key_management/user_decryption.rs +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -46,7 +46,7 @@ impl TryFrom for UserDecryptionData fn try_from(response: IdentityUserDecryptionOptionsResponseModel) -> Result { let master_password_unlock = response .master_password_unlock - .map(|response| MasterPasswordUnlockData::try_from(response)) + .map(MasterPasswordUnlockData::try_from) .transpose()?; Ok(UserDecryptionData { From 77c2d6802186cfc63dbf92d7b9405b7eecd6fcce Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Tue, 19 Aug 2025 17:10:47 +0100 Subject: [PATCH 29/29] missing kdf fields treated as missing field error --- .../src/key_management/master_password.rs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/crates/bitwarden-core/src/key_management/master_password.rs b/crates/bitwarden-core/src/key_management/master_password.rs index c68f92833..b6a1cf1a0 100644 --- a/crates/bitwarden-core/src/key_management/master_password.rs +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -18,10 +18,10 @@ pub enum MasterPasswordError { /// The wrapped encryption key could not be parsed because the encstring is malformed #[error("Wrapped encryption key is malformed")] EncryptionKeyMalformed, - /// The KDF data could not be parsed, because it is missing values or has an invalid value + /// The KDF data could not be parsed, because it has an invalid value #[error("KDF is malformed")] KdfMalformed, - /// The wrapped encryption key or salt fields are missing or KDF data is malformed + /// The wrapped encryption key or salt fields are missing or KDF data is incomplete #[error(transparent)] MissingField(#[from] MissingFieldError), } @@ -54,18 +54,8 @@ impl TryFrom for MasterPasswordUnlockData { }, KdfType::Argon2id => Kdf::Argon2id { iterations: kdf_parse_nonzero_u32(response.kdf.iterations)?, - memory: kdf_parse_nonzero_u32( - response - .kdf - .memory - .ok_or(MasterPasswordError::KdfMalformed)?, - )?, - parallelism: kdf_parse_nonzero_u32( - response - .kdf - .parallelism - .ok_or(MasterPasswordError::KdfMalformed)?, - )?, + memory: kdf_parse_nonzero_u32(require!(response.kdf.memory))?, + parallelism: kdf_parse_nonzero_u32(require!(response.kdf.parallelism))?, }, }; @@ -170,7 +160,7 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_key_malformed_error( + fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_kdf_malformed_error( ) { let response = create_pbkdf2_response( Some(TEST_INVALID_USER_KEY.to_string()), @@ -212,7 +202,7 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_none_kdf_malformed_error( + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_memory_none_missing_field_error( ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { @@ -226,7 +216,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.memory" + ))) + )); } #[test] @@ -248,7 +243,7 @@ mod tests { } #[test] - fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_kdf_malformed_error( + fn test_try_from_master_password_unlock_response_model_argon2id_kdf_parallelism_none_missing_field_error( ) { let response = MasterPasswordUnlockResponseModel { kdf: Box::new(MasterPasswordUnlockKdfResponseModel { @@ -262,7 +257,12 @@ mod tests { }; let result = MasterPasswordUnlockData::try_from(response); - assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + assert!(matches!( + result, + Err(MasterPasswordError::MissingField(MissingFieldError( + "response.kdf.parallelism" + ))) + )); } #[test]