[PM-39454] feat(km): reject unlock data whose user key id does not match the stored one - #8111
[PM-39454] feat(km): reject unlock data whose user key id does not match the stored one#8111quexten wants to merge 1 commit into
Conversation
cd4e688 to
4c3dd87
Compare
0e8999b to
39e02ae
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
4c3dd87 to
86ae77d
Compare
39e02ae to
f0f9329
Compare
🤖 Bitwarden Claude Code ReviewOverall Assessment: APPROVE This PR extends user-key-id validation to the KDF-change and key-rotation write paths: Code Review Details
|
a22d272 to
dc7519e
Compare
1dc7f99 to
dd34e69
Compare
00195fe to
2234b5a
Compare
0c87888 to
3d5c823
Compare
63ab678 to
62bce1e
Compare
3d5c823 to
06bfeac
Compare
| // 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); |
There was a problem hiding this comment.
♻️ 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) |
There was a problem hiding this comment.
❓ 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?
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.