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..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,6 +4,8 @@ use bitwarden_api_identity::models::KdfType; use serde::{Deserialize, Serialize}; use serde_json::Value; +use crate::auth::api::response::identity_user_decryption_options_response::IdentityUserDecryptionOptionsResponseModel; + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct IdentityTokenSuccessResponse { pub access_token: String, @@ -35,6 +37,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 +66,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/api/response/identity_user_decryption_options_response.rs b/crates/bitwarden-core/src/auth/api/response/identity_user_decryption_options_response.rs new file mode 100644 index 000000000..a74b0c13a --- /dev/null +++ b/crates/bitwarden-core/src/auth/api/response/identity_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 IdentityUserDecryptionOptionsResponseModel { + #[serde( + rename = "masterPasswordUnlock", + skip_serializing_if = "Option::is_none" + )] + pub master_password_unlock: 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..ca26e0b14 100644 --- a/crates/bitwarden-core/src/auth/api/response/mod.rs +++ b/crates/bitwarden-core/src/auth/api/response/mod.rs @@ -4,6 +4,7 @@ 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; 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..c68f92833 --- /dev/null +++ b/crates/bitwarden-core/src/key_management/master_password.rs @@ -0,0 +1,316 @@ +use std::num::NonZeroU32; + +use bitwarden_api_api::models::{ + master_password_unlock_response_model::MasterPasswordUnlockResponseModel, KdfType, +}; +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. +#[bitwarden_error(flat)] +#[derive(Debug, thiserror::Error)] +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 + #[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. +#[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, +} + +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: kdf_parse_nonzero_u32(response.kdf.iterations)?, + }, + 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)?, + )?, + }, + }; + + 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: master_key_encrypted_user_key + .parse() + .map_err(|_| MasterPasswordError::EncryptionKeyMalformed)?, + salt, + }) + } +} + +fn kdf_parse_nonzero_u32(value: impl TryInto) -> Result { + value + .try_into() + .ok() + .and_then(NonZeroU32::new) + .ok_or(MasterPasswordError::KdfMalformed) +} + +#[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( + master_key_encrypted_user_key: Option, + salt: Option, + iterations: i32, + ) -> MasterPasswordUnlockResponseModel { + MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::PBKDF2_SHA256, + iterations, + memory: None, + parallelism: None, + }), + master_key_encrypted_user_key, + salt, + } + } + + #[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); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); + } + + #[test] + 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: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + 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); + assert_eq!(data.master_key_wrapped_user_key.to_string(), TEST_USER_KEY); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_invalid_user_key_encryption_key_malformed_error( + ) { + 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!( + result, + Err(MasterPasswordError::EncryptionKeyMalformed) + )); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_user_key_none_missing_field_error() { + let response = create_pbkdf2_response(None, Some(TEST_SALT.to_string()), 600_000); + + let result = MasterPasswordUnlockData::try_from(response); + 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_missing_field_error() { + let response = create_pbkdf2_response(Some(TEST_USER_KEY.to_string()), None, 600_000); + + 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_kdf_memory_none_kdf_malformed_error( + ) { + let response = MasterPasswordUnlockResponseModel { + kdf: Box::new(MasterPasswordUnlockKdfResponseModel { + kdf_type: KdfType::Argon2id, + iterations: 3, + memory: None, + parallelism: Some(4), + }), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + } + + #[test] + 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, + iterations: 3, + memory: Some(0), + parallelism: Some(4), + }), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + } + + #[test] + 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, + iterations: 3, + memory: Some(64), + parallelism: None, + }), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + } + + #[test] + 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, + iterations: 3, + memory: Some(64), + parallelism: Some(0), + }), + master_key_encrypted_user_key: Some(TEST_USER_KEY.to_string()), + salt: Some(TEST_SALT.to_string()), + }; + + let result = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + } + + #[test] + fn test_try_from_master_password_unlock_response_model_pbkdf2_kdf_iterations_zero_kdf_malformed_error( + ) { + 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))); + } + + #[test] + 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, + iterations: 0, + 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 = MasterPasswordUnlockData::try_from(response); + assert!(matches!(result, Err(MasterPasswordError::KdfMalformed))); + } +} diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 10be377a8..aa33bab3e 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -18,10 +18,18 @@ mod crypto_client; #[cfg(feature = "internal")] pub use crypto_client::CryptoClient; +#[cfg(feature = "internal")] +mod master_password; +#[cfg(feature = "internal")] +pub use master_password::*; #[cfg(feature = "internal")] mod security_state; #[cfg(feature = "internal")] +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 new file mode 100644 index 000000000..d31dea4cf --- /dev/null +++ b/crates/bitwarden-core/src/key_management/user_decryption.rs @@ -0,0 +1,56 @@ +use bitwarden_api_api::models::UserDecryptionResponseModel; +use bitwarden_error::bitwarden_error; +use serde::{Deserialize, Serialize}; + +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)] +#[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. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct UserDecryptionData { + /// Optional master password unlock data. + pub master_password_unlock: Option, +} + +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, + }) + } +} + +impl TryFrom for UserDecryptionData { + type Error = UserDecryptionError; + + fn try_from(response: IdentityUserDecryptionOptionsResponseModel) -> Result { + let master_password_unlock = response + .master_password_unlock + .map(MasterPasswordUnlockData::try_from) + .transpose()?; + + Ok(UserDecryptionData { + master_password_unlock, + }) + } +}