Skip to content

Conversation

mzieniukbw
Copy link
Contributor

@mzieniukbw mzieniukbw commented Aug 19, 2025

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-24051

📔 Objective

  • Initialise client with MasterPasswordUnlock via initialize_user_crypto_master_password_unlock in InternalClient, primary used used in auth login methods.
    • Moved set_login_method to be the after the initialize_user_crypto function in execution order, since the latter overrides the set_login_method. Move everywhere else for consistency.
  • Update user's KDF settings with MasterPasswordUnlock via update_user_master_password_unlock in InternalClient, triggered in sync.

Continuation of #376

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
    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 confirmed
    issue 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

@mzieniukbw mzieniukbw marked this pull request as ready for review August 27, 2025 12:29
@mzieniukbw mzieniukbw requested review from a team as code owners August 27, 2025 12:29
@mzieniukbw mzieniukbw requested review from coroiu, Thomas-Avery and shane-melton and removed request for a team August 27, 2025 12:29
…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
Base automatically changed from km/pm-24051-add-master-password-unlock-decryption-options-to-identity-sync-response to main September 3, 2025 14:32
…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
Copy link

sonarqubecloud bot commented Sep 3, 2025

@mzieniukbw mzieniukbw requested a review from quexten September 3, 2025 14:44
Copy link
Contributor

@Thomas-Avery Thomas-Avery left a 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.

Comment on lines +89 to +91
client
.internal
.initialize_crypto_single_org_key(organization_id, encryption_key);
Copy link
Contributor

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.

Comment on lines +36 to +37
r.access_token.to_owned(),
r.refresh_token.to_owned(),
Copy link
Contributor

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>.

Copy link
Contributor

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.

Comment on lines +50 to +64
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 {
Copy link
Contributor

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,
                )?;
            }
        }

Comment on lines +124 to +130
client
.internal
.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "web".to_owned(),
email: auth_req.email.to_owned(),
kdf: Kdf::default(),
}));
Copy link
Contributor

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
Copy link
Contributor

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)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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)?,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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);
Copy link
Contributor

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?

Copy link
Contributor

@quexten quexten left a 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();
Copy link
Contributor

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();
Copy link
Contributor

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();
Copy link
Contributor

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()?;
Copy link
Contributor

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
Copy link
Contributor

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,
                )?;
}

Comment on lines +36 to +37
r.access_token.to_owned(),
r.refresh_token.to_owned(),
Copy link
Contributor

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
Copy link
Contributor

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,
Copy link
Contributor

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,
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants