-
Notifications
You must be signed in to change notification settings - Fork 17
[PM-24051] MasterPasswordUnlockData model with response mapping and adds it to identity success response model #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mzieniukbw
merged 44 commits into
main
from
km/pm-24051-add-master-password-unlock-decryption-options-to-identity-sync-response
Sep 3, 2025
Merged
Changes from 42 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
44773f7
Adds MasterPasswordUnlock into identity's response user decryption opโฆ
mzieniukbw 0712878
Adds MasterPasswordUnlock KDF change handling in sync
mzieniukbw 8ca20a8
simplification
mzieniukbw 28a8423
clippy fix
mzieniukbw b089ea0
formatting
mzieniukbw 0d35393
no handling, just response parsing
mzieniukbw a08179d
test coverage
mzieniukbw b3647f2
wasm
mzieniukbw 36d3136
wasm unit test coverage
mzieniukbw 7c8664d
wasm unit test coverage
mzieniukbw 9c7d50d
Added UserDecryption data, response model with handling
mzieniukbw 70ad9d3
autogenerated wasm responses, UserDecryption struct, use of TryFrom
mzieniukbw 110f4db
failing unit test
mzieniukbw ff86adf
lint
mzieniukbw fa253a3
lint
mzieniukbw 4a95dc5
KdfType enum duplicate
mzieniukbw cd37e31
revert identity crate wasm, since there it's not used right now
mzieniukbw 3bf880f
docs
mzieniukbw 93a3d0c
formatting
mzieniukbw f0c4f2a
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw 3e6a46a
revert user decryption response parsing in wasm
mzieniukbw 7c34725
fixed unit test
mzieniukbw 042678c
revert wasm dependencies
mzieniukbw 9676069
bring back wasm and uniffi bindings to MasterPasswordUnlockData, erroโฆ
mzieniukbw 7973f12
identity separate user decryption options response model
mzieniukbw 5a4f314
review suggestions
mzieniukbw 7e425ca
identity name prefix for UserDecryptionOptions
mzieniukbw c9aaa8c
IdentityUserDecryptionOptionsResponseModel mapping to UserDecryptionData
mzieniukbw 2591773
clippy
mzieniukbw 3212002
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw 77c2d68
missing kdf fields treated as missing field error
mzieniukbw a487fff
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw ecfde52
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw 70dee0c
reducing the public scope
mzieniukbw ad732ce
UserDecryptionOptions rename
mzieniukbw 5e6f1dd
UserDecryptionOptions documentation
mzieniukbw d11f653
simplify error with MasterPasswordError
mzieniukbw 0e104bb
remove wasm and uniffi
mzieniukbw a9f46db
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw 3cef902
api crate private error
mzieniukbw ae10e35
public crate user decryption options
mzieniukbw 6861006
change from unused_imports to dead_code
mzieniukbw 6b98aaf
allow dead code on struct
mzieniukbw 4a00571
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโฆ
mzieniukbw File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
crates/bitwarden-core/src/auth/api/response/user_decryption_options_response.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
use bitwarden_api_api::models::MasterPasswordUnlockResponseModel; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
/// Provides user decryption options used to unlock user's vault. | ||
/// Currently, only master password unlock is supported. | ||
#[derive(Serialize, Deserialize, Debug, PartialEq)] | ||
pub(crate) struct UserDecryptionOptionsResponseModel { | ||
/// Contains information needed to unlock user's vault with master password. | ||
/// None when user have no master password. | ||
#[serde( | ||
rename = "masterPasswordUnlock", | ||
skip_serializing_if = "Option::is_none" | ||
)] | ||
pub(crate) master_password_unlock: Option<MasterPasswordUnlockResponseModel>, | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
308 changes: 308 additions & 0 deletions
308
crates/bitwarden-core/src/key_management/master_password.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,308 @@ | ||
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}; | ||
|
||
use crate::{require, MissingFieldError}; | ||
|
||
/// Error for master password related operations. | ||
#[bitwarden_error(flat)] | ||
#[derive(Debug, thiserror::Error)] | ||
pub(crate) 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 has an invalid value | ||
#[error("KDF is malformed")] | ||
KdfMalformed, | ||
/// The wrapped encryption key or salt fields are missing or KDF data is incomplete | ||
#[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)] | ||
pub(crate) struct MasterPasswordUnlockData { | ||
/// The key derivation function used to derive the master key | ||
kdf: Kdf, | ||
/// The master key wrapped user key | ||
master_key_wrapped_user_key: EncString, | ||
/// The salt used in the KDF, typically the user's email | ||
salt: String, | ||
} | ||
|
||
impl TryFrom<MasterPasswordUnlockResponseModel> for MasterPasswordUnlockData { | ||
type Error = MasterPasswordError; | ||
|
||
fn try_from(response: MasterPasswordUnlockResponseModel) -> Result<Self, Self::Error> { | ||
let kdf = match response.kdf.kdf_type { | ||
mzieniukbw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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(require!(response.kdf.memory))?, | ||
parallelism: kdf_parse_nonzero_u32(require!(response.kdf.parallelism))?, | ||
}, | ||
}; | ||
|
||
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<u32>) -> Result<NonZeroU32, MasterPasswordError> { | ||
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 = "[email protected]"; | ||
|
||
fn create_pbkdf2_response( | ||
master_key_encrypted_user_key: Option<String>, | ||
salt: Option<String>, | ||
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_kdf_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_missing_field_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::MissingField(MissingFieldError( | ||
"response.kdf.memory" | ||
))) | ||
)); | ||
} | ||
|
||
#[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_none_missing_field_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::MissingField(MissingFieldError( | ||
"response.kdf.parallelism" | ||
))) | ||
)); | ||
} | ||
|
||
#[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))); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.