Skip to content

Conversation

@quexten
Copy link
Contributor

@quexten quexten commented Dec 2, 2025

🎟️ Tracking

📔 Objective

Unblock v2 account registration related tasks.

📸 Screenshots

⏰ 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

@quexten quexten requested a review from a team as a code owner December 2, 2025 15:07
@quexten quexten requested review from Thomas-Avery and removed request for a team December 2, 2025 15:07
@quexten quexten marked this pull request as draft December 2, 2025 15:08
@claude
Copy link
Contributor

claude bot commented Dec 2, 2025

Claude finished @quexten's task —— View job


Code Review Complete

Tasks

  • Analyze PR context and recent feedback
  • Review code changes for security, correctness, and best practices
  • Provide comprehensive review feedback

Overall Assessment

This PR adds a new repository method for updating user cryptographic state as part of v2 account registration. While the implementation follows Bitwarden patterns and handles transactions correctly, there are critical issues with the test suite that must be addressed before merge.

❌ Critical Issues

1. Test Suite Contains Invalid V1 Test Case

File: test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs:325-348

The test creates V1 account keys data (without SignatureKeyPairData and SecurityStateData) and attempts to pass it to SetV2AccountCryptographicStateAsync:

var accountKeysDataV1 = new UserAccountKeysData
{
    PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(
        wrappedPrivateKey: "v1-wrapped-private-key",
        publicKey: "v1-public-key"
    )
};
await sut.SetV2AccountCryptographicStateAsync(createdUser.Id, accountKeysDataV1);

Problem:

  • accountKeysDataV1.IsV2Encryption() returns false because all V2 fields are null
  • SetV2AccountCryptographicStateAsync checks if (!accountKeysData.IsV2Encryption()) and throws ArgumentException
  • This test will fail immediately and block CI

Fix Required: Either:

  1. Remove the V1 test entirely (since method is V2-only per documentation), or
  2. Change it to verify V1 data properly throws ArgumentException

2. Null-Forgiving Operators Risk Runtime Exceptions

File: src/Infrastructure.Dapper/Repositories/UserRepository.cs:318-323

SecurityState = accountKeysData.SecurityStateData!.SecurityState,
SecurityVersion = accountKeysData.SecurityStateData!.SecurityVersion,
// ... more null-forgiving operators

Problem: If IsV2Encryption() validation is ever bypassed or removed, these null-forgiving operators (!) will cause NullReferenceException at runtime.

Recommendation: Add defensive null checks:

if (accountKeysData.SecurityStateData == null || accountKeysData.SignatureKeyPairData == null)
{
    throw new InvalidOperationException("V2 encryption data incomplete.");
}

⚠️ Important Issues

3. Missing User Existence Validation in Dapper

File: src/Infrastructure.Dapper/Repositories/UserRepository.cs:310-328

The Dapper implementation calls the stored procedure without verifying the UPDATE affected any rows. If userId doesn't exist:

  • The stored procedure UPDATE affects 0 rows
  • It then attempts to INSERT into UserSignatureKeyPair with a non-existent UserId
  • This may succeed or fail depending on foreign key constraints, leading to inconsistent behavior

Comparison: The EF implementation properly validates user existence at line 261-265:

var userEntity = await dbContext.Users.FindAsync(userId);
if (userEntity == null)
{
    throw new ArgumentException("User not found", nameof(userId));
}

Recommendation: Add similar validation to Dapper implementation or modify the stored procedure to return affected row count.

4. Insufficient Test Coverage

File: test/Infrastructure.EFIntegration.Test/Repositories/UserRepositoryTests.cs

Missing test cases:

  • ❌ User not found scenario (should throw ArgumentException)
  • ❌ V1 data passed to V2 method (should throw ArgumentException)
  • ❌ Updating existing signature key pair (test only covers creation)
  • ❌ Transaction rollback on failure
  • ❌ Additional updateUserDataActions execution and failure

Recommendation: Add comprehensive error case testing:

[CiSkippedTheory, EfUserAutoData]
public async Task SetV2AccountCryptographicStateAsync_InvalidV1Data_ThrowsArgumentException(...)
{
    var accountKeysDataV1 = new UserAccountKeysData { ... };
    await Assert.ThrowsAsync<ArgumentException>(() => 
        sut.SetV2AccountCryptographicStateAsync(userId, accountKeysDataV1));
}

[CiSkippedTheory, EfUserAutoData]
public async Task SetV2AccountCryptographicStateAsync_UserNotFound_ThrowsArgumentException(...)
{
    await Assert.ThrowsAsync<ArgumentException>(() => 
        sut.SetV2AccountCryptographicStateAsync(Guid.NewGuid(), validV2Data));
}

5. Stored Procedure Lacks Input Validation

File: src/Sql/dbo/Stored Procedures/User_UpdateAccountCryptographicState.sql

The stored procedure accepts all parameters as nullable and doesn't validate whether the user exists. Silent failures can occur if the user doesn't exist.

Recommendation: Add validation at the beginning:

IF NOT EXISTS (SELECT 1 FROM [dbo].[User] WHERE [Id] = @Id)
BEGIN
    RAISERROR('User not found', 16, 1)
    RETURN
END

🎨 Technical Debt Concerns

6. Inconsistent Signature Key Pair ID Generation

  • EF Implementation (line 302): Uses SetNewId() after setting properties
  • Dapper Implementation (line 302): Uses CoreHelpers.GenerateComb() directly

Recommendation: Call SetNewId() before setting other properties for consistency.

7. Documentation Typo

File: src/Core/Repositories/IUserRepository.cs:50

/// The provided MUST be a V2 encryption state.

Should be: "The provided data MUST be a V2 encryption state."

✅ Security Assessment - Positive Findings

  1. ✅ Cryptographic data handling follows existing patterns (encrypted strings)
  2. ✅ No logging of sensitive cryptographic data
  3. ✅ Zero-knowledge principles maintained (server stores encrypted keys only)
  4. ✅ Proper transaction isolation prevents race conditions
  5. ✅ Parameterized queries prevent SQL injection
  6. ✅ V2 encryption validation enforces data integrity via IsV2Encryption() check

✅ Transaction Atomicity - Correct Implementation

Both implementations correctly handle transactions:

  1. ✅ Explicit transaction scope for user + signature key pair updates
  2. ✅ Optional additional actions executed within same transaction
  3. ✅ Proper rollback on exception
  4. ✅ Async commit/rollback operations

✅ Adherence to Bitwarden Patterns

  • ✅ Follows existing repository patterns
  • ✅ Consistent with other transaction-based update methods
  • ✅ Proper separation of Dapper and EF implementations
  • ✅ Uses CoreHelpers.GenerateComb() for ID generation
  • ✅ Follows nullable reference type conventions

Summary of Required Actions

Must fix before merge:

  1. ❌ Fix or remove V1 test case (lines 325-348 in UserRepositoryTests.cs)
  2. ❌ Add error case test coverage (user not found, invalid V1 data)
  3. ⚠️ Add user existence validation to Dapper implementation

Should address:

  1. Add validation to stored procedure
  2. Fix documentation typo in IUserRepository.cs
  3. Consider defensive null checks instead of null-forgiving operators

Consider for future:

  1. Add test for existing signature key pair update scenario
  2. Add audit logging for cryptographic state changes (without sensitive data)

The implementation demonstrates solid understanding of transaction management and cryptographic data handling, but the test suite issues must be resolved before merge as the current V1 test will cause CI failures.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 2, 2025

Logo
Checkmarx One – Scan Summary & Details5091afa9-74cd-4635-b6f6-85f272e1f5e8

New Issues (3)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/Auth/Controllers/AccountsController.cs: 427
detailsMethod at line 427 of /src/Api/Auth/Controllers/AccountsController.cs gets a parameter from a user request from model. This parameter value flow...
ID: VU8sgdorwW4N9vJXTUl%2BCpjmMcY%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1519
detailsMethod at line 1519 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: dMGF5qNfAN72zlvQcA1MgbhHv%2Fc%3D
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1395
detailsMethod at line 1395 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This parameter value flows ...
ID: iOCFr11iI9znjDnv46yLfiS4aDY%3D
Attack Vector
Fixed Issues (6)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Controllers/OrganizationUsersController.cs: 507
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 97
MEDIUM CSRF /src/Api/AdminConsole/Public/Controllers/MembersController.cs: 207
MEDIUM CSRF /bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs: 87
MEDIUM CSRF /bitwarden_license/src/Scim/Controllers/v2/GroupsController.cs: 97
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 300

@quexten quexten marked this pull request as ready for review December 2, 2025 15:42
@quexten quexten marked this pull request as draft December 2, 2025 15:46
@quexten quexten marked this pull request as ready for review December 2, 2025 15:52
@sonarqubecloud
Copy link

sonarqubecloud bot commented Dec 2, 2025

@codecov
Copy link

codecov bot commented Dec 2, 2025

Codecov Report

❌ Patch coverage is 0% with 104 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.66%. Comparing base (b1390c9) to head (b4681cc).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
...ure.EntityFramework/Repositories/UserRepository.cs 0.00% 51 Missing ⚠️
...frastructure.Dapper/Repositories/UserRepository.cs 0.00% 43 Missing ⚠️
...e/KeyManagement/Models/Data/UserAccountKeysData.cs 0.00% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6669      +/-   ##
==========================================
+ Coverage   53.65%   57.66%   +4.01%     
==========================================
  Files        1926     1914      -12     
  Lines       85720    85061     -659     
  Branches     7687     7610      -77     
==========================================
+ Hits        45989    49049    +3060     
+ Misses      37958    34177    -3781     
- Partials     1773     1835      +62     

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

Base automatically changed from km/move-models to main December 2, 2025 18:54
Copy link
Contributor

@mzieniukbw mzieniukbw left a comment

Choose a reason for hiding this comment

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

I am not entirely sure whether we can and should use the same command for SSO JIT and master password user registration flows, since in one we update the existing user and in the other we need to create the user with all the keys.

user.SecurityVersion = accountKeysData.SecurityStateData.SecurityVersion;
await _userSignatureKeyPairRepository.UpsertAsync(new UserSignatureKeyPair
{
Id = CoreHelpers.GenerateComb(),
Copy link
Contributor

Choose a reason for hiding this comment

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

With the Id set, this will trigger Update, instead of Create / Insert, is it intended ?
Note: when Id is omitted, upsert would trigger Create and set the Id automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed this to CreateAsync, and added docs clarifying this command should only be used for setting the initial keys, not updating.

user.SignedPublicKey = accountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey;
user.SecurityState = accountKeysData.SecurityStateData.SecurityState;
user.SecurityVersion = accountKeysData.SecurityStateData.SecurityVersion;
await _userSignatureKeyPairRepository.UpsertAsync(new UserSignatureKeyPair
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 want to use this for user registration for now, it would make sense to call CreateAsync directly.

var user = await _userRepository.GetByIdAsync(userId);
if (user == null)
{
throw new ArgumentException("User not found", nameof(userId));
Copy link
Contributor

Choose a reason for hiding this comment

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

While this is true for SSO JIT, user registration with master password will not have user created yet (check

await _registerUserCommand.RegisterUserViaEmailVerificationToken(user, model.MasterPasswordHash,
).
Unless the intention is to ensure that user is created first without the keys, and then the keys are updated with this command. Does not feel great.

Copy link
Contributor Author

@quexten quexten Dec 3, 2025

Choose a reason for hiding this comment

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

As mentioned in the other comment, I've now updated the comment and the command is now meant for just setting initial keys, so JIT password / regular MP have to find a different solution / write their own command that atomically sets all their data.


public async Task SetAccountKeysForUserAsync(Guid userId, AccountKeysRequestModel accountKeys)
{
var user = await _userRepository.GetByIdAsync(userId);
Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 For the case where user is created, maybe it would make more sense to provide User user instead of Guid ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Updated to take a user. I also added a comment since the user that is passed in does get mutated in the process, which may be unintuitive.


namespace Bit.Core.KeyManagement.Commands.Interfaces;

public interface ISetAccountKeysForUserCommand
Copy link
Contributor

Choose a reason for hiding this comment

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

some docs would be useful

@quexten quexten marked this pull request as draft December 2, 2025 22:26
@quexten quexten marked this pull request as ready for review December 3, 2025 12:27
@quexten quexten requested a review from a team as a code owner December 3, 2025 12:27
mzieniukbw
mzieniukbw previously approved these changes Dec 3, 2025
@quexten quexten marked this pull request as draft December 8, 2025 14:53
@quexten quexten force-pushed the km/account-keys-command branch from 08db72c to e4a8a41 Compare December 8, 2025 14:58
@quexten quexten changed the title Add set account keys command Add UpdateAccountCryptographicState repository function Dec 8, 2025
@quexten quexten marked this pull request as ready for review December 8, 2025 15:48
Copy link
Contributor

@mkincaid-bw mkincaid-bw left a comment

Choose a reason for hiding this comment

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

LGTM

@quexten quexten merged commit 919d0be into main Dec 11, 2025
55 checks passed
@quexten quexten deleted the km/account-keys-command branch December 11, 2025 11:10
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.

6 participants