-
Notifications
You must be signed in to change notification settings - Fork 17
[PM-24051] Init crypto and update kdf with MasterPasswordUnlock #399
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?
[PM-24051] Init crypto and update kdf with MasterPasswordUnlock #399
Conversation
…ption-options-to-identity-sync-response
…r and docs improvements
…ption-options-to-identity-sync-response
…ns-to-identity-sync-response' into km/pm-24051-setting-master-password-unlock-into-state # Conflicts: # crates/bitwarden-vault/src/sync.rs
…ns-to-identity-sync-response' into km/pm-24051-setting-master-password-unlock-into-state
…ption-options-to-identity-sync-response
…ns-to-identity-sync-response' into km/pm-24051-setting-master-password-unlock-into-state # Conflicts: # crates/bitwarden-core/src/auth/mod.rs # crates/bitwarden-core/src/client/internal.rs # crates/bitwarden-core/src/key_management/crypto.rs
…nto-state # Conflicts: # crates/bitwarden-core/src/auth/api/response/identity_success_response.rs # crates/bitwarden-core/src/key_management/master_password.rs # crates/bitwarden-core/src/key_management/mod.rs # crates/bitwarden-core/src/key_management/user_decryption.rs
|
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.
Mostly just questions for my clarity and a few optional minor changes.
client | ||
.internal | ||
.initialize_crypto_single_org_key(organization_id, encryption_key); |
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.
💭 question:
initialize_crypto_single_org_key
didn't have the side effect of setting the login method? Is this change just to have consistency with the others? I'm a little hesitant because it's SM.
r.access_token.to_owned(), | ||
r.refresh_token.to_owned(), |
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.
💭 not a rust expert
It is weird to me that we are keeping .clone()
in crates/bitwarden-core/src/auth/login/access_token.rs
crates/bitwarden-core/src/auth/login/auth_request.rs
and crates/bitwarden-core/src/auth/login/password.rs
and only updating to .to_owned()
here.
From the rust docs https://doc.rust-lang.org/std/borrow/trait.ToOwned.html
A generalization of Clone to borrowed data.
Some types make it possible to go from borrowed to owned, usually by implementing the Clone trait. But Clone works only for going from &T to T. The ToOwned trait generalizes Clone to construct owned data from any borrow of a given type.
It sounds like we should prefer clone when going from T
to T
? In this case we want to go from String
-> String
and Option<String>
-> Option<String>
.
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.
Agree on this. In this case we are not even going from borrowed data to owned data, so semantically it is confusing saying "to owned" (implying we are converting to an owned object (by cloning usually).
We're going from String to String (T to T), so we should clone.
if let Some(master_password_unlock) = r | ||
.user_decryption_options | ||
.as_ref() | ||
.map(UserDecryptionData::try_from) | ||
.transpose()? | ||
.and_then(|user_decryption| user_decryption.master_password_unlock) | ||
{ | ||
client | ||
.internal | ||
.initialize_user_crypto_master_password_unlock( | ||
input.password.clone(), | ||
master_password_unlock, | ||
user_key_state, | ||
)?; | ||
} else { |
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.
⛏️
Since we care about both branches of logic match
might be more readable here thanif let Some(
let master_password_unlock = r
.user_decryption_options
.as_ref()
.map(UserDecryptionData::try_from)
.transpose()?
.and_then(|user_decryption| user_decryption.master_password_unlock);
match master_password_unlock {
Some(master_password_unlock) => {
client
.internal
.initialize_user_crypto_master_password_unlock(
input.password.clone(),
master_password_unlock,
user_key_state,
)?;
}
None => {
let user_key = r.key.as_deref();
let user_key: EncString = require!(user_key).parse()?;
let master_key = MasterKey::derive(&input.password, &email, &kdf)?;
client.internal.initialize_user_crypto_master_key(
master_key,
user_key,
user_key_state,
)?;
}
}
client | ||
.internal | ||
.set_login_method(LoginMethod::User(UserLoginMethod::Username { | ||
client_id: "web".to_owned(), | ||
email: auth_req.email.to_owned(), | ||
kdf: Kdf::default(), | ||
})); |
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.
❓ Looking to clarify.
So this will be a change in behavior? Previously this would get overridden by the initialize_user_crypto
setting the final state to:
client
.internal
.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "".to_string(),
email: req.email,
kdf: req.kdf_params,
}));
With the difference being client_id
will be getting set to web
now?
Reading through the old flow previously kdf was getting set to default and emails match up.
Odd for initing crypto to have that side effect but I don't understand all the flows.
security_state: None, | ||
}; | ||
|
||
if let Some(master_password_unlock) = r |
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.
⛏️ Same comment about the match for readability.
LoginMethod::User(UserLoginMethod::Username { kdf, .. }) => kdf, | ||
LoginMethod::User(UserLoginMethod::ApiKey { kdf, .. }) => kdf, | ||
#[cfg(feature = "secrets")] | ||
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError)?, |
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.
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError)?, | |
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError), |
I don't think returning the error needs the unwrap.
kdf: new_kdf, | ||
})), | ||
#[cfg(feature = "secrets")] | ||
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError)?, |
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.
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError)?, | |
LoginMethod::ServiceAccount(_) => return Err(NotAuthenticatedError), |
login_method.as_ref() | ||
{ | ||
assert_eq!(*email, test_account.user.email); | ||
assert_eq!(*kdf, test_account.user.kdf_params); |
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.
Should we add an assert for the client_id
to be the empty string?
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.
Looks mostly good, I have some stylistic / readability improvements noted.
@@ -41,26 +42,48 @@ pub(crate) async fn login_password( | |||
r.refresh_token.clone(), | |||
r.expires_in, | |||
); | |||
|
|||
let private_key = r.private_key.as_deref(); |
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.
Looks like this two-line pattern to convert from Option<&str> to EncString repeats a fair bit in this PR. Could we just implement TryFrom<Option<&str>> for EncString, and then remove all of thees temporary variables? It would make this a bit shorter and more readable.
user_key_state, | ||
)?; | ||
} else { | ||
let user_key = r.key.as_deref(); |
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.
Ref the comment above about TryFrom<Option<&str>>
r.expires_in, | ||
); | ||
|
||
let master_key = MasterKey::derive(&input.password, &email, &kdf)?; | ||
let private_key = r.private_key.as_deref(); |
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.
Ref the other comment about TryFrom<Option<&str>>
)?; | ||
} else { | ||
let user_key = r.key.as_deref(); | ||
let user_key: EncString = require!(user_key).parse()?; |
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.
Ref the comment about TryFrom<Option<&str>>
}; | ||
|
||
if let Some(master_password_unlock) = r | ||
.user_decryption_options |
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.
Suggestion (non-blocking): We have this long chain quite a bit. It may be nice to just implement "master_password_unlock() -> Option" on the decryptionData.
Then, you can rewrite as (untested):
let decryption_data: UserDecryptionData = r.user_decryption_options.
if let Some(master_password_unlock_data) = decryption_data.master_password_unlock_data() {
client
.internal
.initialize_user_crypto_master_password_unlock(
input.password.clone(),
master_password_unlock,
user_key_state,
)?;
} else {
client.internal.initialize_user_crypto_master_key(
MasterKey::derive(&input.password, &email, &kdf)?,
r.key.try_into()?,
user_key_state,
)?;
}
r.access_token.to_owned(), | ||
r.refresh_token.to_owned(), |
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.
Agree on this. In this case we are not even going from borrowed data to owned data, so semantically it is confusing saying "to owned" (implying we are converting to an owned object (by cloning usually).
We're going from String to String (T to T), so we should clone.
@@ -39,6 +44,18 @@ pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result<SyncRes | |||
.await | |||
.map_err(|e| SyncError::Api(e.into()))?; | |||
|
|||
if let Some(master_password_unlock) = sync | |||
.user_decryption |
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.
Ref the comment about implementing the masterpassword function on the decyrption data
/// The key derivation function used to derive the master key | ||
kdf: Kdf, | ||
pub kdf: Kdf, |
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.
Looks like this can be pub(crate)
unless I'm missing something?
|
||
let user_key_state = UserKeyState { | ||
private_key, | ||
signing_key: None, |
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.
(Not in this PR) looks like we don't have support for parsing the new keys from the token response here yet. We should make a follow-up ticket somewhere to fix this.
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-24051
📔 Objective
MasterPasswordUnlock
viainitialize_user_crypto_master_password_unlock
inInternalClient
, primary used used in auth login methods.set_login_method
to be the after theinitialize_user_crypto
function in execution order, since the latter overrides theset_login_method
. Move everywhere else for consistency.MasterPasswordUnlock
viaupdate_user_master_password_unlock
inInternalClient
, triggered in sync.Continuation of #376
⏰ Reminders before review
team
🦮 Reviewer guidelines
:+1:
) or similar for great changes:memo:
) or ℹ️ (:information_source:
) for notes or general info:question:
) for questions:thinking:
) or 💭 (:thought_balloon:
) for more open inquiry that's not quite a confirmedissue and could potentially benefit from discussion
:art:
) for suggestions / improvements:x:
) or:warning:
) for more significant problems or concerns needing attention:seedling:
) or ♻️ (:recycle:
) for future improvements or indications of technical debt:pick:
) for minor or nitpick changes