-
Notifications
You must be signed in to change notification settings - Fork 14
[PM-24051] In identity and sync response & decryption options, add MasterPasswordUnlockDataResponse in 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
base: main
Are you sure you want to change the base?
Changes from 11 commits
44773f7
0712878
8ca20a8
28a8423
b089ea0
0d35393
a08179d
b3647f2
36d3136
7c8664d
9c7d50d
70ad9d3
110f4db
ff86adf
fa253a3
4a95dc5
cd37e31
3bf880f
93a3d0c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,353 @@ | ||||
#![allow(missing_docs)] | ||||
|
||||
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 crate::{require, MissingFieldError}; | ||||
|
||||
#[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, | ||||
quexten marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
pub salt: String, | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have an opaque string type Ideally we would expose this from rust as we do for EncString
This one is optional, if we don't do it here it is tech debt we should record. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what i understand, in cases This is different here, since we don't have equivalent in our Rust code.
Or change the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my main concern here is that we want the tagged type, so that the typesafety stretches to TS. A bit more verbose than I'd like, but this does the job, and is the same approach I use for the new PinProtectedKeyEnvelope (PasswordProtectedkeyEnvelope):
But again, we can do that later if we don't want to do in in this PR, I don't want to scope creep it. |
||||
} | ||||
|
||||
impl MasterPasswordUnlockData { | ||||
pub fn process_response( | ||||
mzieniukbw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
response: MasterPasswordUnlockResponseModel, | ||||
) -> Result<MasterPasswordUnlockData, MasterPasswordError> { | ||||
let kdf = match response.kdf.kdf_type { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you do apply the suggestion from above, then this would require mapping to the different error types |
||||
KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 { | ||||
iterations: NonZeroU32::new(response.kdf.iterations as u32) | ||||
.ok_or(MissingFieldError(stringify!(response.kdf.iterations)))?, | ||||
}, | ||||
KdfType::Argon2id => Kdf::Argon2id { | ||||
iterations: NonZeroU32::new(response.kdf.iterations as u32) | ||||
mzieniukbw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
.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)))?, | ||||
}, | ||||
}; | ||||
|
||||
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, | ||||
}) | ||||
mzieniukbw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
} | ||||
} | ||||
|
||||
#[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( | ||||
iterations: i32, | ||||
encrypted_user_key: Option<String>, | ||||
salt: Option<String>, | ||||
) -> 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<i32>, | ||||
parallelism: Option<i32>, | ||||
encrypted_user_key: Option<String>, | ||||
salt: Option<String>, | ||||
) -> 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); | ||||
} | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this and add comments. We require comments on everything that is publicly exposed.