Skip to content

feat(salt): [PM-27060] - #22381

Draft
Patrick-Pimentel-Bitwarden wants to merge 4 commits into
mainfrom
auth/pm-27060/use-salt-for-sdk-prelogin
Draft

feat(salt): [PM-27060]#22381
Patrick-Pimentel-Bitwarden wants to merge 4 commits into
mainfrom
auth/pm-27060/use-salt-for-sdk-prelogin

Conversation

@Patrick-Pimentel-Bitwarden

Copy link
Copy Markdown
Contributor

🎟️ Tracking

📔 Objective

Added in logic so the prelogin response will have the salt present for key derivation when using the sdk for prelogin.

📸 Screenshots

… have the salt present for key derivation when using the sdk for prelogin.
@Patrick-Pimentel-Bitwarden Patrick-Pimentel-Bitwarden added the t:bugfix Change Type - Bugfix label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR threads the server-supplied prelogin salt through PasswordPreloginData and derives the master key from it in PasswordLoginStrategy when PM27060_PasswordPreloginFromSdk is enabled, with test coverage for both flag states. Alongside the salt work, the API response parser was reshaped from the flat Kdf/KdfIterations fields to a nested KdfSettings object — that part is not behind the flag and is the main concern. A second concern is that salt is documented as nullable but typed string, and an absent salt throws inside makeMasterKey rather than being handled.

Code Review Details
  • ⚠️ : API response shape change (KdfSettings) is not gated by the feature flag, so the flag-off path breaks against servers without the PM-28143 change
    • libs/common/src/auth/password-prelogin/password-prelogin.response.ts:12-13
  • ⚠️ : salt typed string while documented nullable; a missing salt throws a TypeError inside makeMasterKey instead of falling back or failing clearly
    • libs/common/src/auth/password-prelogin/password-prelogin.model.ts:21
  • ❓ : makeMasterKey applies trim().toLowerCase() to the salt, so it is not passed through verbatim as the specs claim
    • libs/auth/src/common/login-strategies/password-login.strategy.ts:150-155

The salt-selection branch is duplicated in both the prefetched and freshly-fetched paths (including a copy-pasted four-line comment) and could collapse to a single const salt = useSdkForPrelogin ? data.salt : email.

PR Metadata Assessment

  • QUESTION: Title carries only the ticket number and the Tracking section is empty
    • Suggested: "feat(auth): PM-27060 derive master key from server-supplied prelogin salt", plus the Jira link

Comment thread libs/common/src/auth/password-prelogin/password-prelogin.model.ts
Comment on lines +150 to +155
if (useSdkForPrelogin) {
return this.legacyCompatKeyService.makeMasterKey(
masterPassword,
preFetchedPreloginData.salt,
preFetchedPreloginData.kdfConfig,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

QUESTION: makeMasterKey re-normalizes the salt — is the server salt guaranteed to already be trimmed and lower-cased?

Details

makeMasterKey applies its own normalization before deriving:

// libs/legacy-crypto/src/services/legacy-compat-key.service.ts:86
email = email.trim().toLowerCase();

So the server-supplied salt is not used verbatim, despite the spec comment ("passes the salt through verbatim without re-normalizing it… Trimming or lower-casing here would produce a different master key"). If the server ever dictates a salt whose casing or whitespace differs from trim().toLowerCase(), the client derives a different master key than intended — precisely the divergence PM-27060 is meant to eliminate.

If server normalization is guaranteed to match, consider updating the spec comment so it doesn't assert a guarantee the runtime doesn't provide. If not, the salt needs a derivation path that skips the legacy normalization.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@Patrick-Pimentel-Bitwarden , this is something we have to consider.

@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.46%. Comparing base (4845f9d) to head (41847d1).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #22381   +/-   ##
=======================================
  Coverage   53.45%   53.46%           
=======================================
  Files        4275     4275           
  Lines      135303   135308    +5     
  Branches    21326    21327    +1     
=======================================
+ Hits        72328    72336    +8     
+ Misses      57688    57685    -3     
  Partials     5287     5287           

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

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment on lines +16 to +19
* The salt the server dictates for master key derivation. Only consumed when
* {@link FeatureFlag.PM27060_PasswordPreloginFromSdk} is on; callers otherwise derive from the
* user-entered email. Nullable while PM-28143 is in flight — the server does not populate it on
* every deployment.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Claude loves to make fragile comments like this where it details that something is only consumed while the flag is enabled - it should just document what it is - generally not usage sites unless there is a very good reason. Also, the PM-28143 comment is not useful. All supported servers send the salt now even though it is optional from the server response model perspective (which we could have just made non-nullish from the start since they were additive model changes on the respond model side).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This comment applies elsewhere where we mention PM-28143 as well.

Comment on lines +109 to +126
it("carries the SDK-supplied salt onto the model when the flag is on", async () => {
configService.getFeatureFlag.mockResolvedValue(true);

const result = await firstValueFrom(sut.getPreloginData$(email));

// PasswordLoginStrategy derives the master key from this salt when the flag is on, so it
// must come from the SDK response rather than the email the caller passed in.
expect(result.salt).toBe(sdkSalt);
expect(result.salt).not.toBe(email);
});

it("carries the API-supplied salt onto the model when the flag is off", async () => {
const result = await firstValueFrom(sut.getPreloginData$(email));

// The salt is still mapped when the flag is off — PasswordLoginStrategy simply ignores it
// and derives from the entered email instead.
expect(result.salt).toBe(apiSalt);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ : these two tests are duplicative of the two tests above them.

Comment on lines +128 to +142
it("emits an undefined salt when the SDK omits one", async () => {
// PM-28143: salt is nullable while the server transition is in flight. Documents current
// behavior — the service does not substitute a fallback.
configService.getFeatureFlag.mockResolvedValue(true);
sdkService.client.auth
.mockDeep()
.login.mockDeep()
.get_password_prelogin.mockResolvedValue({
kdf: { pBKDF2: { iterations: PBKDF2KdfConfig.ITERATIONS.defaultValue } },
} as SdkPasswordPreloginResponse);

const result = await firstValueFrom(sut.getPreloginData$(email));

expect(result.salt).toBeUndefined();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ : this test is documenting a server state that we no longer support. All supported servers send salt so... I think we delete this.

Comment on lines +150 to +162
if (useSdkForPrelogin) {
return this.legacyCompatKeyService.makeMasterKey(
masterPassword,
preFetchedPreloginData.salt,
preFetchedPreloginData.kdfConfig,
);
} else {
return this.legacyCompatKeyService.makeMasterKey(
masterPassword,
email,
preFetchedPreloginData.kdfConfig,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 : It's a bit more clear to me if we use this ternary construction + many less lines of code.

// Flag lets us revert to email-derived salt if SDK salt encounters issues
const salt = useSdkForPrelogin ? preloginData.salt : email;
return this.legacyCompatKeyService.makeMasterKey(masterPassword, salt, preloginData.kdfConfig);

Comment on lines +172 to +188
// If we are using the sdk to fetch the prelogin data, only then do we want to
// use the salt that is passed back from the prelogin response in building the master key.
// This gives us the ability to turn off the feature of using the returned salt from salt
// in the event of bad normalization occurring during the transition.
if (useSdkForPrelogin) {
return this.legacyCompatKeyService.makeMasterKey(
masterPassword,
preloginData.salt,
preloginData.kdfConfig,
);
} else {
return this.legacyCompatKeyService.makeMasterKey(
masterPassword,
email,
preloginData.kdfConfig,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

const email = "hello@world.com";
const masterPassword = "password";
const hashedPassword = "HASHED_PASSWORD";
// Deliberately not equal to `email`. The whole point of PM-27060 is that the server dictates the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎨 : leaving a comment w/ the PM-27060 reference is worse vs just explaining that server dictates salt.

Comment on lines +339 to +359
it("passes an absent salt straight through", async () => {
// PM-28143: Salt is nullable server-side during the transition. This documents current
// behavior — the strategy does not fall back to the email and does not throw.
// Built inline rather than via the helper, whose default parameter would substitute
// preloginSalt for an explicit undefined.
const noSaltCredentials = new PasswordLoginCredentials(
email,
masterPassword,
undefined,
undefined,
new PasswordPreloginData(kdfConfig, undefined as unknown as string),
);

await passwordLoginStrategy.logIn(noSaltCredentials);

expect(legacyCompatKeyService.makeMasterKey).toHaveBeenCalledWith(
masterPassword,
undefined,
kdfConfig,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ : another unsupported server test case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:bugfix Change Type - Bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants