Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
44773f7
Adds MasterPasswordUnlock into identity's response user decryption opโ€ฆ
mzieniukbw Aug 4, 2025
0712878
Adds MasterPasswordUnlock KDF change handling in sync
mzieniukbw Aug 4, 2025
8ca20a8
simplification
mzieniukbw Aug 5, 2025
28a8423
clippy fix
mzieniukbw Aug 5, 2025
b089ea0
formatting
mzieniukbw Aug 5, 2025
0d35393
no handling, just response parsing
mzieniukbw Aug 5, 2025
a08179d
test coverage
mzieniukbw Aug 5, 2025
b3647f2
wasm
mzieniukbw Aug 5, 2025
36d3136
wasm unit test coverage
mzieniukbw Aug 5, 2025
7c8664d
wasm unit test coverage
mzieniukbw Aug 5, 2025
9c7d50d
Added UserDecryption data, response model with handling
mzieniukbw Aug 5, 2025
70ad9d3
autogenerated wasm responses, UserDecryption struct, use of TryFrom
mzieniukbw Aug 7, 2025
110f4db
failing unit test
mzieniukbw Aug 7, 2025
ff86adf
lint
mzieniukbw Aug 7, 2025
fa253a3
lint
mzieniukbw Aug 7, 2025
4a95dc5
KdfType enum duplicate
mzieniukbw Aug 7, 2025
cd37e31
revert identity crate wasm, since there it's not used right now
mzieniukbw Aug 8, 2025
3bf880f
docs
mzieniukbw Aug 8, 2025
93a3d0c
formatting
mzieniukbw Aug 8, 2025
f0c4f2a
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Aug 13, 2025
3e6a46a
revert user decryption response parsing in wasm
mzieniukbw Aug 13, 2025
7c34725
fixed unit test
mzieniukbw Aug 13, 2025
042678c
revert wasm dependencies
mzieniukbw Aug 13, 2025
9676069
bring back wasm and uniffi bindings to MasterPasswordUnlockData, erroโ€ฆ
mzieniukbw Aug 13, 2025
7973f12
identity separate user decryption options response model
mzieniukbw Aug 15, 2025
5a4f314
review suggestions
mzieniukbw Aug 15, 2025
7e425ca
identity name prefix for UserDecryptionOptions
mzieniukbw Aug 15, 2025
c9aaa8c
IdentityUserDecryptionOptionsResponseModel mapping to UserDecryptionData
mzieniukbw Aug 15, 2025
2591773
clippy
mzieniukbw Aug 15, 2025
3212002
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Aug 19, 2025
9b293c7
introduce initialize_user_crypto_master_password_unlock
mzieniukbw Aug 19, 2025
5b3ecf8
build fix
mzieniukbw Aug 19, 2025
77c2d68
missing kdf fields treated as missing field error
mzieniukbw Aug 19, 2025
a487fff
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Aug 19, 2025
00811a9
Merge branch 'km/pm-24051-add-master-password-unlock-decryption-optioโ€ฆ
mzieniukbw Aug 19, 2025
da700c0
login method not being overridden by initialize_user_crypto
mzieniukbw Aug 20, 2025
ecfde52
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Aug 20, 2025
debf701
Merge branch 'km/pm-24051-add-master-password-unlock-decryption-optioโ€ฆ
mzieniukbw Aug 20, 2025
ac64be7
update kdf on sync
mzieniukbw Aug 20, 2025
92d710d
function order
mzieniukbw Aug 20, 2025
8c3b599
sync unit tests
mzieniukbw Aug 21, 2025
70dee0c
reducing the public scope
mzieniukbw Aug 25, 2025
ad732ce
UserDecryptionOptions rename
mzieniukbw Aug 25, 2025
5e6f1dd
UserDecryptionOptions documentation
mzieniukbw Aug 25, 2025
d11f653
simplify error with MasterPasswordError
mzieniukbw Aug 25, 2025
0e104bb
remove wasm and uniffi
mzieniukbw Aug 25, 2025
a9f46db
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Aug 25, 2025
3cef902
api crate private error
mzieniukbw Aug 25, 2025
ae10e35
public crate user decryption options
mzieniukbw Aug 25, 2025
1f32993
Merge branch 'km/pm-24051-add-master-password-unlock-decryption-optioโ€ฆ
mzieniukbw Aug 25, 2025
b5218cd
fix build
mzieniukbw Aug 25, 2025
6861006
change from unused_imports to dead_code
mzieniukbw Aug 27, 2025
b22537f
Merge branch 'km/pm-24051-add-master-password-unlock-decryption-optioโ€ฆ
mzieniukbw Aug 27, 2025
edd0cbf
use UserDecryptionData instead of MasterPasswordUnlockData
mzieniukbw Aug 27, 2025
af667b0
require! stringify without reference
mzieniukbw Aug 27, 2025
7d538ca
update InternalClient using MasterPasswordUnlockData
mzieniukbw Aug 27, 2025
e5f16dc
rename to update_user_master_password_unlock
mzieniukbw Aug 27, 2025
3cf5cc4
user friendly require! error message
mzieniukbw Aug 27, 2025
13ecd8e
increased test coverage
mzieniukbw Aug 29, 2025
1b8ccee
fix clippy
mzieniukbw Aug 29, 2025
6b98aaf
allow dead code on struct
mzieniukbw Sep 1, 2025
4a00571
Merge branch 'main' into km/pm-24051-add-master-password-unlock-decryโ€ฆ
mzieniukbw Sep 1, 2025
308066b
Merge branch 'km/pm-24051-add-master-password-unlock-decryption-optioโ€ฆ
mzieniukbw Sep 1, 2025
260078b
Merge branch 'main' into km/pm-24051-setting-master-password-unlock-iโ€ฆ
mzieniukbw Sep 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub(crate) struct IdentityTokenSuccessResponse {
key_connector_url: Option<String>,

#[serde(rename = "userDecryptionOptions", alias = "UserDecryptionOptions")]
user_decryption_options: Option<UserDecryptionOptionsResponseModel>,
pub(crate) user_decryption_options: Option<UserDecryptionOptionsResponseModel>,

/// Stores unknown api response fields
extra: Option<HashMap<String, Value>>,
Expand Down
9 changes: 5 additions & 4 deletions crates/bitwarden-core/src/auth/login/access_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ pub(crate) async fn login_access_token(
r.refresh_token.clone(),
r.expires_in,
);

client
.internal
.initialize_crypto_single_org_key(organization_id, encryption_key);
Comment on lines +89 to +91
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.


client
.internal
.set_login_method(LoginMethod::ServiceAccount(
Expand All @@ -94,10 +99,6 @@ pub(crate) async fn login_access_token(
state_file: input.state_file.clone(),
},
));

client
.internal
.initialize_crypto_single_org_key(organization_id, encryption_key);
}

AccessTokenLoginResponse::process_response(response)
Expand Down
57 changes: 39 additions & 18 deletions crates/bitwarden-core/src/auth/login/api_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
JwtToken,
},
client::{internal::UserKeyState, LoginMethod, UserLoginMethod},
key_management::UserDecryptionData,
require, Client,
};

Expand All @@ -32,34 +33,54 @@ pub(crate) async fn login_api_key(
let kdf = client.auth().prelogin(email.clone()).await?;

client.internal.set_tokens(
r.access_token.clone(),
r.refresh_token.clone(),
r.access_token.to_owned(),
r.refresh_token.to_owned(),
Comment on lines +36 to +37
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.

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

let private_key: EncString = require!(private_key).parse()?;

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.

security_state: None,
};

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

.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 {
Comment on lines +50 to +64
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,
                )?;
            }
        }

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

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::ApiKey {
client_id: input.client_id.to_owned(),
client_secret: input.client_secret.to_owned(),
client_id: input.client_id.clone(),
client_secret: input.client_secret.clone(),
email,
kdf,
}));

let user_key: EncString = require!(r.key.as_deref()).parse()?;
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;

client.internal.initialize_user_crypto_master_key(
master_key,
user_key,
UserKeyState {
private_key,
signing_key: None,
security_state: None,
},
)?;
}

Ok(ApiKeyLoginResponse::process_response(response))
Expand Down
21 changes: 10 additions & 11 deletions crates/bitwarden-core/src/auth/login/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,11 @@ pub(crate) async fn complete_auth_request(
.await?;

if let IdentityTokenResponse::Authenticated(r) = response {
let kdf = Kdf::default();

client.internal.set_tokens(
r.access_token.clone(),
r.refresh_token.clone(),
r.expires_in,
);
client
.internal
.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "web".to_owned(),
email: auth_req.email.to_owned(),
kdf: kdf.clone(),
}));

let method = match res.master_password_hash {
Copy link
Contributor

@quexten quexten Aug 20, 2025

Choose a reason for hiding this comment

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

We should probably just drop this code. Support for this auth method on the approving side has been removed 3 months ago: bitwarden/clients#14236

(not this PR)

Some(_) => AuthRequestMethod::MasterKey {
Expand All @@ -118,8 +109,8 @@ pub(crate) async fn complete_auth_request(
.crypto()
.initialize_user_crypto(InitUserCryptoRequest {
user_id: None,
kdf_params: kdf,
email: auth_req.email,
kdf_params: Kdf::default(),
email: auth_req.email.to_owned(),
private_key: require!(r.private_key).parse()?,
signing_key: None,
security_state: None,
Expand All @@ -130,6 +121,14 @@ pub(crate) async fn complete_auth_request(
})
.await?;

client
.internal
.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "web".to_owned(),
email: auth_req.email.to_owned(),
kdf: Kdf::default(),
}));
Comment on lines +124 to +130
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.


Ok(())
} else {
Err(LoginError::AuthenticationFailed)
Expand Down
4 changes: 4 additions & 0 deletions crates/bitwarden-core/src/auth/login/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,8 @@ pub enum LoginError {

#[error("Failed to authenticate")]
AuthenticationFailed,

#[cfg(feature = "internal")]
#[error(transparent)]
MasterPassword(#[from] crate::key_management::MasterPasswordError),
}
53 changes: 38 additions & 15 deletions crates/bitwarden-core/src/auth/login/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::auth::{
use crate::{
auth::{api::request::PasswordTokenRequest, login::LoginError, login::TwoFactorRequest},
client::LoginMethod,
key_management::UserDecryptionData,
Client,
};

Expand Down Expand Up @@ -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.

let private_key: EncString = require!(private_key).parse()?;

let user_key_state = UserKeyState {
private_key,
signing_key: None,
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.

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

let user_key: EncString = require!(user_key).parse()?;

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: input.email.to_owned(),
kdf: input.kdf.to_owned(),
email: input.email.clone(),
kdf: input.kdf.clone(),
}));

let user_key: EncString = require!(r.key.as_deref()).parse()?;
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;

client.internal.initialize_user_crypto_master_key(
master_key,
user_key,
UserKeyState {
private_key,
signing_key: None,
security_state: None,
},
)?;
}

Ok(PasswordLoginResponse::process_response(response))
Expand Down
19 changes: 10 additions & 9 deletions crates/bitwarden-core/src/auth/tde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,6 @@ pub(super) fn make_register_tde_keys(
None
};

client
.internal
.set_login_method(crate::client::LoginMethod::User(
crate::client::UserLoginMethod::Username {
client_id: "".to_owned(),
email,
kdf: Kdf::default(),
},
));
client.internal.initialize_user_crypto_decrypted_key(
user_key.0,
UserKeyState {
Expand All @@ -52,6 +43,16 @@ pub(super) fn make_register_tde_keys(
},
)?;

client
.internal
.set_login_method(crate::client::LoginMethod::User(
crate::client::UserLoginMethod::Username {
client_id: "".to_owned(),
email,
kdf: Kdf::default(),
},
));

Ok(RegisterTdeKeyResponse {
private_key: key_pair.private,
public_key: key_pair.public,
Expand Down
Loading
Loading