Skip to content

[PM-39454] feat(km): reject unlock data whose user key id does not match the stored one - #8111

Draft
quexten wants to merge 1 commit into
km/user-key-id-05-write-pathfrom
km/user-key-id-06-validate-key-id
Draft

[PM-39454] feat(km): reject unlock data whose user key id does not match the stored one#8111
quexten wants to merge 1 commit into
km/user-key-id-05-write-pathfrom
km/user-key-id-06-validate-key-id

Conversation

@quexten

@quexten quexten commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

When changing the password or changing KDF settings, the user-key MUST NOT be changed. Thus, we add the key id in the unlock data so that the server can validate and assert it.

@quexten
quexten force-pushed the km/user-key-id-05-write-path branch from cd4e688 to 4c3dd87 Compare July 31, 2026 13:33
@quexten
quexten force-pushed the km/user-key-id-06-validate-key-id branch from 0e8999b to 39e02ae Compare July 31, 2026 13:33
@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.90%. Comparing base (dc7519e) to head (1dc7f99).

Additional details and impacted files
@@                      Coverage Diff                      @@
##           km/user-key-id-05-write-path    #8111   +/-   ##
=============================================================
  Coverage                         62.90%   62.90%           
=============================================================
  Files                              2304     2304           
  Lines                            100400   100412   +12     
  Branches                           9041     9043    +2     
=============================================================
+ Hits                              63152    63165   +13     
+ Misses                            35062    35061    -1     
  Partials                           2186     2186           

☔ View full report in Codecov by Harness.
📢 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.

@quexten
quexten force-pushed the km/user-key-id-05-write-path branch from 4c3dd87 to 86ae77d Compare July 31, 2026 15:20
@quexten
quexten force-pushed the km/user-key-id-06-validate-key-id branch from 39e02ae to f0f9329 Compare July 31, 2026 15:20
@quexten quexten added ai-review Request a Claude code review t:feature Change Type - Feature Development labels Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

This PR extends user-key-id validation to the KDF-change and key-rotation write paths: ChangeKdfCommand now asserts the unlock data's key id is unchanged, and BaseRotateUserAccountKeysData.ValidateContainedKeyIdMatches asserts the master-password unlock data names the same key as the rotation itself. The null-handling asymmetry (absent contained key id passes for pre-field clients; a contained key id with no rotation key id is rejected) is deliberate and covered by new unit tests, and KeyId defines value-based ==/!=, so the comparison behaves as intended. All rotation entry points validate before any persistence, and the new integration test confirms User.UserKeyId is left untouched on rejection. TDE and Key Connector rotation carry no unlock-data key id, so nothing is missing there.

Code Review Details
  • ♻️ : New KDF-change key-id rejection has no negative test, unlike the adjacent salt checks
    • src/Core/KeyManagement/Kdf/Implementations/ChangeKdfCommand.cs:53
  • ❓ : Rotation does not assert the request's key id differs from the stored one
    • src/Core/KeyManagement/UserKey/Models/Data/BaseRotateUserAccountKeysData.cs:38

Comment thread src/Core/Auth/UserFeatures/UserMasterPassword/MasterPasswordService.cs Outdated
Comment thread src/Core/Auth/UserFeatures/UserMasterPassword/Data/UpdateExistingPasswordData.cs Outdated
@quexten
quexten marked this pull request as ready for review July 31, 2026 15:40
@quexten
quexten requested review from a team as code owners July 31, 2026 15:40
@quexten
quexten requested review from eligrubb and rr-bw July 31, 2026 15:40
@quexten
quexten marked this pull request as draft July 31, 2026 16:14
@quexten
quexten force-pushed the km/user-key-id-05-write-path branch from a22d272 to dc7519e Compare July 31, 2026 17:29
@quexten
quexten force-pushed the km/user-key-id-06-validate-key-id branch 2 times, most recently from 1dc7f99 to dd34e69 Compare August 3, 2026 09:50
@quexten
quexten force-pushed the km/user-key-id-05-write-path branch 2 times, most recently from 00195fe to 2234b5a Compare August 3, 2026 10:11
@quexten
quexten force-pushed the km/user-key-id-06-validate-key-id branch 2 times, most recently from 0c87888 to 3d5c823 Compare August 3, 2026 10:29
@quexten
quexten force-pushed the km/user-key-id-05-write-path branch from 63ab678 to 62bce1e Compare August 3, 2026 10:35
@quexten
quexten force-pushed the km/user-key-id-06-validate-key-id branch from 3d5c823 to 06bfeac Compare August 3, 2026 10:35
// A KDF change re-wraps the existing user key, it does not replace it, so its key id must not
// change. Also checked in the MasterPasswordService via
// UpdateExistingKdfConfigurationData.ValidateDataForUser.
unlockData.ValidateUserKeyIdUnchangedForUser(user);

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.

♻️ DEBT: No test covers the rejection this line adds, unlike the salt checks right above it.

Details and fix

ChangeKdfCommandTests has ChangeKdfAsync_AuthDataSaltMismatch_Throws and ChangeKdfAsync_UnlockDataSaltMismatch_Throws for the two duplicated salt checks, each asserting BadRequestException and SaveUpdateExistingKdfConfigurationAsync received 0 calls. Every existing test in that file builds MasterPasswordUnlockData without a UserKeyId, so the new call only ever takes the early-return path — deleting the line would keep the suite green.

Suggest mirroring the salt tests:

[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_UnlockDataUserKeyIdMismatch_Throws(
    SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
{
    sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
        .Returns(true);
    // user.UserKeyId is set by KeyIdCustomization; send a different one.
    var unlockData = new MasterPasswordUnlockData
    {
        Kdf = kdf,
        MasterKeyWrappedUserKey = "new-wrapped-key",
        Salt = user.GetMasterPasswordSalt(),
        UserKeyId = KeyId.FromHexEncodedString("fedcba9876543210fedcba9876543210")
    };
    // ... authenticationData with matching salt/kdf
    await Assert.ThrowsAsync<BadRequestException>(() =>
        sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData));
    await sutProvider.GetDependency<IMasterPasswordService>()
        .Received(0)
        .SaveUpdateExistingKdfConfigurationAsync(Arg.Any<User>(), Arg.Any<UpdateExistingKdfConfigurationData>());
}

/// that the items directly encrypted by the user-key (cipher-keys, private-key, signature-key) are
/// encrypted by the correct key.
/// </summary>
public void ValidateContainedKeyIdMatches(KeyId? containedKeyId)

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: Should a rotation also reject a key id that equals the one already stored for the account?

Details

This validates the unlock data against the request's own key id, but nothing compares the request's key id to user.GetUserKeyId(). A rotation replaces the user key, so a request that reuses the stored key id makes two distinct keys share one id — exactly the confusion key ids exist to prevent, and it would be persisted as-is by BaseRotateUserAccountKeysAsync (RotateUserAccountKeysCommand.cs:330).

ValidateForUser runs before that assignment in all four rotation entry points, so a baseModel.UserKeyId != user.GetUserKeyId() assertion would be cheap to add here. Is this deliberately deferred to a later PR in the stack, or is the reuse case considered a client-only concern?

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:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant