Skip to content

[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

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

mzieniukbw
Copy link
Contributor

@mzieniukbw mzieniukbw commented Aug 5, 2025

🎟️ Tracking

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

📔 Objective

Adds MasterPasswordUnlock and UserDecryption support with response parsing into the identity token response.
Includes Uniffi and WASM support.
Tested response parsing in client's (via WASM).

⏰ 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

Copy link
Contributor

github-actions bot commented Aug 5, 2025

Logo
Checkmarx One – Scan Summary & Details1a65f52b-1474-417e-abb6-86d5d05ab37f

Great job! No new security vulnerabilities introduced in this pull request

Copy link

codecov bot commented Aug 5, 2025

Codecov Report

❌ Patch coverage is 95.07246% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.36%. Comparing base (ae0cae2) to head (93a3d0c).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...asm-internal/src/key_management/user_decryption.rs 0.00% 10 Missing ⚠️
...twarden-core/src/key_management/master_password.rs 98.02% 5 Missing ⚠️
...twarden-core/src/key_management/user_decryption.rs 97.53% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #376      +/-   ##
==========================================
+ Coverage   73.95%   74.36%   +0.41%     
==========================================
  Files         252      256       +4     
  Lines       21574    22126     +552     
==========================================
+ Hits        15955    16454     +499     
- Misses       5619     5672      +53     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@mzieniukbw mzieniukbw requested review from a team and quexten and removed request for a team August 5, 2025 11:08
@mzieniukbw mzieniukbw marked this pull request as ready for review August 5, 2025 14:51
@mzieniukbw mzieniukbw requested a review from a team as a code owner August 5, 2025 14:51
@mzieniukbw mzieniukbw requested a review from justindbaur August 5, 2025 14:51
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.

Some initial comments, some of these are required, some are idiomatic (but probably required since the quality bar for SDK seems to be set much higher).

I'd like a review from someone with more rust experience such as @Hinton or @dani-garcia too. I believe specifically the request parsing is something that has not existed in the SDK so far, and there may be a better way to do this that we're unaware of.

pub struct MasterPasswordUnlockData {
pub kdf: Kdf,
pub master_key_wrapped_user_key: EncString,
pub salt: String,
Copy link
Contributor

@quexten quexten Aug 6, 2025

Choose a reason for hiding this comment

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

We have an opaque string type MasterPasswordSalt in clients: https://github.com/bitwarden/clients/blob/61cd0c4f51d9fe390c93f61f2f55076de33b57cf/libs/common/src/key-management/master-password/types/master-password.types.ts#L15.

Ideally we would expose this from rust as we do for EncString

export type EncString = Tagged<string, "EncString">;
and replace the TS version with the SDK version so that we have type safety across the FFI boundary.

This one is optional, if we don't do it here it is tech debt we should record.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From what i understand, in cases EncString, we have an equivalent type in Rust SDK, that is different during Serialization: EncString becomed String type, hence why we export it in TS as string.

This is different here, since we don't have equivalent in our Rust code.
If we want one, then i think either of the two will be ok:

#[cfg_attr(feature = "wasm", tsify::declare(tsify::Tsify))]
pub type MasterPasswordSalt = String;

Or change the salt field to have different type in TS.

    #[cfg_attr(
        feature = "wasm",
        tsify(type = "MasterPasswordSalt")
    )]
    pub salt: String,

Copy link
Contributor

@quexten quexten Aug 8, 2025

Choose a reason for hiding this comment

The 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):

#[derive(Serialize, Deserialize)]
#[serde(transparent)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct MasterPasswordSalt(
    #[cfg_attr(
        feature = "wasm",
        tsify(type = r#"Tagged<string, "MasterPasswordSalt">"#)
    )]
   String,
);

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.

@@ -0,0 +1,353 @@
#![allow(missing_docs)]
Copy link
Contributor

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.

@mzieniukbw mzieniukbw requested a review from quexten August 8, 2025 09:42
Copy link

sonarqubecloud bot commented Aug 8, 2025

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.

Looking pretty nice now! Some more smaller items that improve usability by teams unfamiliar with the code (docs / error names)

Comment on lines +20 to +28
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum MasterPasswordError {
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
MissingField(#[from] MissingFieldError),
}
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
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum MasterPasswordError {
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
MissingField(#[from] MissingFieldError),
}
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum MasterPasswordError {
/// The wrapped encryption key could not be parsed
#[error(transparent)]
Crypto(#[from] CryptoError),
/// The KDF data is malformed
#[error(transparent)]
MissingField(#[from] MissingFieldError),
}

I would actually suggest making these errors more descriptive. If I understand the rest of the file correctly, missingfield happens exactly when the KDF is broken, and Crypto happens exactly when the encstring parsing fails, so you could just skip wrapping the errors and make it more clear to the caller what actually failed:

Suggested change
#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum MasterPasswordError {
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
MissingField(#[from] MissingFieldError),
}
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
pub enum MasterPasswordError {
/// The wrapped encryption key could not be parsed because the encstring is malformed
EncryptionKeyMalformed,
/// The KDF data could not be parsed, because it is missing values or has an invalid value
KdfMalformed
}

type Error = MasterPasswordError;

fn try_from(response: MasterPasswordUnlockResponseModel) -> Result<Self, Self::Error> {
let kdf = match response.kdf.kdf_type {
Copy link
Contributor

Choose a reason for hiding this comment

The 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

use crate::key_management::master_password::{MasterPasswordError, MasterPasswordUnlockData};

/// Error for master user decryption related operations.
#[allow(missing_docs)]
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
#[allow(missing_docs)]


/// Represents data required to decrypt user's vault.
/// Currently, this is only used for master password unlock.
#[allow(missing_docs)]
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
#[allow(missing_docs)]

};
use wasm_bindgen::prelude::wasm_bindgen;

#[wasm_bindgen]
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
#[wasm_bindgen]
/// This is a stop-gap solution to allow parsing of response models via the SDK while the full decryption functionality is not yet moved over to the SDK
#[wasm_bindgen]

@Hinton
Copy link
Member

Hinton commented Aug 11, 2025

This tightly couples consumers of the SDK to the server generated bindings which is somewhat dangerous. We've previously addressed this by wrapping requests/responses in a manner that more accurately represents the action from the SDK's perspective. There be dragons here and we should consider this a bit.

Do we expect to do similar things to other request/responses? If not I think we should simply wrap the logic with our own struct, while it's a few lines of boilerplate it reduces the complexity.

Includes Uniffi and WASM support.

There is no UniFFI support here.

Copy link
Member

Choose a reason for hiding this comment

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

question: Why are we implementing logic in wasm-internal? This would generally belong to our feature crates.

@@ -90,3 +110,5 @@ mockall = ["dep:mockall"]
bon = ["dep:bon"]
{{/useBonBuilder}}
{{/reqwestTrait}}
tsify = { workspace = true, optional = true, features = ["js"], default-features = false }
wasm-bindgen = { workspace = true, optional = true, features = ["serde-serialize"] }
Copy link
Member

Choose a reason for hiding this comment

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

We'd need some way to configure this in runtime, since this would now also apply to identity.

pub struct MasterPasswordUnlockResponseModel {
#[serde(rename = "kdf")]
pub kdf: Box<models::MasterPasswordUnlockKdfResponseModel>,
#[serde(rename = "masterKeyEncryptedUserKey")]
pub master_key_encrypted_user_key: Option<String>,
pub master_key_encrypted_user_key: String,
Copy link
Member

@Hinton Hinton Aug 11, 2025

Choose a reason for hiding this comment

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

Platform hasn't made any strides into making fields required as it has compatibility implications. This will throw an error should these fields not be available in the past, present or future. I.e. can we guarantee forever that this endpoint in the past and future has and always will include master_key_encrypted_user_key and salt? And if these are missing are we fine with it throwing?

This is related to https://bitwarden.atlassian.net/wiki/spaces/DEV/pages/760971277/Improve+API+bindings.

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