Skip to content

Conversation

@mzieniukbw
Copy link
Contributor

@mzieniukbw mzieniukbw commented Dec 9, 2025

🎟️ Tracking

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

📔 Objective

Support v2 encryption on key-connector signups.
Changes inPOST /accounts/key-management/set-key-connector-key controller:

  • New keyConnectorKeyWrappedUserKey and AccountKeys request fields.
    • Deprecated old request fields
    • Kdf are not needed, so we use a default
    • Backwards compatible for V1, until remove dlater
  • The V2 logic in dedicated command with one helper query.
  • Added feature flag, later to be used in client's enable-account-encryption-v2-key-connector-registration

📸 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

@mzieniukbw mzieniukbw changed the base branch from main to km/account-keys-command December 9, 2025 13:50
@github-actions
Copy link
Contributor

github-actions bot commented Dec 9, 2025

Logo
Checkmarx One – Scan Summary & Details020369ea-9c80-4afe-9c95-346baaddd220

New Issues (3)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 146
detailsMethod at line 146 of /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs gets a parameter from a user request from model. Thi...
ID: T1r%2BjKa12nVQ5xsFNJVayNPQOAw%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 (2)

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

Severity Issue Source File / Package
MEDIUM CSRF /src/Api/AdminConsole/Public/Controllers/MembersController.cs: 207
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 300

Base automatically changed from km/account-keys-command to main December 11, 2025 11:10
@mzieniukbw mzieniukbw force-pushed the km/pm-27280-v2-encryption-on-key-connector-signups branch from 5e1c1cc to e424f19 Compare December 12, 2025 15:25
@mzieniukbw mzieniukbw marked this pull request as ready for review December 12, 2025 15:36
@mzieniukbw mzieniukbw requested review from a team as code owners December 12, 2025 15:36
@claude
Copy link
Contributor

claude bot commented Dec 12, 2025

Claude finished @mzieniukbw's task —— View job


Code Review Complete ✓

Tasks

  • Analyze PR context and changes
  • Review PR changes with bitwarden-code-reviewer agent
  • Complete review

Review Summary

I've completed a comprehensive security-focused code review of this PR. The bitwarden-code-reviewer agent has posted detailed inline comments on specific issues found in the code.

Key findings:

Critical Issues:

  • Missing V2 data validation before database operations
  • Silent update failures in stored procedures (no row count validation)
  • Missing organization/SSO configuration validation

High Priority:

  • Incomplete request validation for V2 requests
  • Race condition risk with UsesKeyConnector check
  • Event logging timing issue

Positive Aspects:

  • Good CQRS pattern implementation
  • Comprehensive test coverage
  • Maintains zero-knowledge architecture
  • Transaction-based updates for atomicity
  • Backward compatibility with V1

The implementation follows Bitwarden's security principles overall, but the validation gaps and transaction safety issues should be addressed before merging to ensure data integrity and security.


@codecov
Copy link

codecov bot commented Dec 12, 2025

Codecov Report

❌ Patch coverage is 98.75000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.86%. Comparing base (3de2f98) to head (2fc9629).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ure.EntityFramework/Repositories/UserRepository.cs 92.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6712      +/-   ##
==========================================
+ Coverage   53.86%   57.86%   +4.00%     
==========================================
  Files        1917     1924       +7     
  Lines       85126    85350     +224     
  Branches     7620     7635      +15     
==========================================
+ Hits        45853    49389    +3536     
+ Misses      37508    34118    -3390     
- Partials     1765     1843      +78     

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

@@ -0,0 +1,35 @@
IF OBJECT_ID('[dbo].[User_UpdateKeyConnectorUserKey]') IS NOT NULL
Copy link
Contributor

Choose a reason for hiding this comment

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

✅ Migration Safety: Good DROP IF EXISTS pattern

The migration script properly checks for existence before dropping, which is good practice.

Minor suggestions:

  1. Consider adding a comment explaining what this stored procedure does:
-- Creates/updates the User_UpdateKeyConnectorUserKey stored procedure
-- Used during Key Connector setup to update user's cryptographic key state
-- and KDF parameters when enrolling in Key Connector
  1. Consider adding a rollback script in comments for easy reference:
-- To rollback: DROP PROCEDURE [dbo].[User_UpdateKeyConnectorUserKey]

This helps with understanding and potential rollback scenarios.

}

[Theory, DatabaseData]
public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database)
Copy link
Contributor

Choose a reason for hiding this comment

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

✅ Good Test Coverage: Integration test for SetKeyConnectorUserKey

This test validates that the method updates the user key correctly. However, consider expanding test coverage:

Missing test scenarios:

  1. Non-existent user: What happens if the userId doesn't exist?
  2. KDF parameters: Test verifies Key is updated but doesn't check if KDF parameters are set correctly
  3. UsesKeyConnector flag: Verify the flag is set to true
  4. Revision dates: Confirm RevisionDate and AccountRevisionDate are updated
  5. Concurrent updates: Test for race conditions

Suggested additions:

[Theory, DatabaseData]
public async Task SetKeyConnectorUserKey_SetsKdfParametersCorrectly(IUserRepository userRepository, Database database)
{
    var user = await userRepository.CreateTestUserAsync();
    var keyConnectorWrappedKey = "key-connector-wrapped-user-key";
    
    var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey);
    await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database);
    
    var updatedUser = await userRepository.GetByIdAsync(user.Id);
    
    Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
    Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations);
    Assert.True(updatedUser.UsesKeyConnector);
    Assert.InRange(updatedUser.RevisionDate, DateTime.UtcNow.AddMinutes(-1), DateTime.UtcNow);
}

var setKeyConnectorUserKeyTask =
_userRepository.SetKeyConnectorUserKey(user.Id, requestModel.KeyConnectorKeyWrappedUserKey);

await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, requestModel.AccountKeys.ToAccountKeysData(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Security Concern: Null Reference Exception Risk

The code calls requestModel.AccountKeys.ToAccountKeysData() without null checking, even though validation only checks if AccountKeys is not null at line 38. However, there's a potential race condition or programming error risk here.

More importantly, the ToAccountKeysData() method can return different types of data structures depending on whether certain optional fields are present (see AccountKeysRequestModel.cs lines 19-48). For V2 encryption with Key Connector, we should be explicitly validating that ALL required V2 fields are present:

  • PublicKeyEncryptionKeyPair
  • SignatureKeyPair
  • SecurityState

Without this validation, if a client sends only partial V2 data, the call to SetV2AccountCryptographicStateAsync at line 297 of UserRepository.cs will throw an ArgumentException saying "Provided account keys data is not valid V2 encryption data", but this happens after we've already called SetKeyConnectorUserKey.

Recommendation: Add explicit validation that all V2 encryption fields are present before making any repository calls:

if (requestModel.AccountKeys.PublicKeyEncryptionKeyPair == null ||
    requestModel.AccountKeys.SignatureKeyPair == null ||
    requestModel.AccountKeys.SecurityState == null)
{
    throw new BadRequestException("Complete V2 encryption data must be provided for Key Connector registration");
}

[Required]
public required string OrgIdentifier { get; init; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing Validation for V2 Requests

The Validate method returns early for V2 requests (line 35-38) without performing any validation. This means V2 requests bypass all validation logic, relying solely on:

  1. The [EncryptedString] attribute on KeyConnectorKeyWrappedUserKey (line 26)
  2. The [Required] attribute on OrgIdentifier (line 30)

However, this doesn't validate that AccountKeys is not null or that it contains all required V2 encryption fields (PublicKeyEncryptionKeyPair, SignatureKeyPair, SecurityState).

Security Risk: A client could send a V2 request with:

  • Valid KeyConnectorKeyWrappedUserKey
  • Valid OrgIdentifier
  • AccountKeys = new AccountKeysRequestModel() with only minimal fields

This would pass validation here but fail later in the repository layer, potentially after partial database updates.

Recommendation: Add validation for V2 requests:

if (IsV2Request())
{
    // V2 registration - validate all required fields are present
    if (AccountKeys!.PublicKeyEncryptionKeyPair == null)
    {
        yield return new ValidationResult("PublicKeyEncryptionKeyPair is required for V2 encryption.");
    }
    if (AccountKeys!.SignatureKeyPair == null)
    {
        yield return new ValidationResult("SignatureKeyPair is required for V2 encryption.");
    }
    if (AccountKeys!.SecurityState == null)
    {
        yield return new ValidationResult("SecurityState is required for V2 encryption.");
    }
    yield break;
}

return result.SingleOrDefault();
}

public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey)
Copy link
Contributor

Choose a reason for hiding this comment

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

Security Concern: Transaction Safety Issue

The SetKeyConnectorUserKey method returns an UpdateUserData delegate that will be executed within a transaction by SetV2AccountCryptographicStateAsync. However, there's no validation that the user exists before returning the delegate.

Problem: If the user doesn't exist, the stored procedure User_UpdateKeyConnectorUserKey will execute successfully (with 0 rows affected) but won't raise an error. This means:

  1. The Key Connector key update silently fails
  2. But SetV2AccountCryptographicStateAsync continues and updates account keys
  3. The user ends up in an inconsistent state with V2 account keys but without the Key Connector key

Recommendation: The stored procedure should validate that exactly 1 row was affected:

DECLARE @RowsAffected INT

UPDATE [dbo].[User]
SET ...
WHERE [Id] = @Id

SET @RowsAffected = @@ROWCOUNT

IF @RowsAffected = 0
    THROW 50000, 'User not found', 1

Or alternatively, add a check in the delegate:

var result = await connection!.ExecuteAsync(...);
if (result == 0)
{
    throw new ArgumentException("User not found", nameof(userId));
}

using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);

var userEntity = await dbContext.Users.FindAsync(userId);
Copy link
Contributor

Choose a reason for hiding this comment

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

Inconsistency: User Not Found Handling

The Entity Framework implementation throws an ArgumentException if the user is not found (line 463), but the Dapper implementation does not have this check and would silently succeed with 0 rows updated.

This inconsistency means:

  • EF Implementation: Will fail early with a clear error
  • Dapper Implementation: Will silently fail, leaving the user in an inconsistent state

Recommendation: Ensure both implementations handle the "user not found" case consistently by adding validation to the Dapper version as well (see comment on Dapper implementation).

BEGIN
SET NOCOUNT ON

UPDATE
Copy link
Contributor

Choose a reason for hiding this comment

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

Security: Missing Row Count Validation

The stored procedure updates the user record but doesn't verify that a row was actually updated. If the user doesn't exist (wrong @Id), the UPDATE succeeds with 0 rows affected, and the procedure returns successfully.

This can lead to silent failures where the Key Connector key is not set, but the calling code assumes it was successful and proceeds with other operations, leaving the user in an inconsistent state.

Recommendation: Add validation that exactly one row was affected:

UPDATE [dbo].[User]
SET ...
WHERE [Id] = @Id

IF @@ROWCOUNT = 0
    THROW 50000, 'User not found for Key Connector key update', 1;


_canUseKeyConnectorQuery.VerifyCanUseKeyConnector(user);

var setKeyConnectorUserKeyTask =
Copy link
Contributor

Choose a reason for hiding this comment

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

Potential Race Condition: Multiple Simultaneous Calls

If a client makes multiple simultaneous calls to this endpoint (which could happen due to network issues or client bugs), there's no protection against race conditions. The VerifyCanUseKeyConnector check at line 43 happens outside the transaction, so two concurrent requests could both pass the check.

While SetV2AccountCryptographicStateAsync uses a transaction, the validation user.UsesKeyConnector in CanUseKeyConnectorQuery (line 20) happens before the transaction starts. This means:

  1. Request A checks user.UsesKeyConnector → false ✓
  2. Request B checks user.UsesKeyConnector → false ✓
  3. Both proceed and one overwrites the other

Recommendation: Consider adding optimistic concurrency control or checking UsesKeyConnector within the transaction. Alternatively, document that this is expected behavior (last write wins) if that's acceptable.


var result = await _userService.SetKeyConnectorKeyAsync(model.ToUser(user), model.Key, model.OrgIdentifier);
if (result.Succeeded)
if (model.IsV2Request())
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing Error Handling for V2 Path

The V2 request path (lines 154-158) doesn't have any error handling or result checking, unlike the V1 path which checks result.Succeeded and adds model errors.

If SetKeyConnectorKeyForUserAsync completes without throwing an exception, the method returns successfully. However, if the command doesn't throw but also doesn't fully succeed (which shouldn't happen with current implementation, but could with future changes), there's no feedback to the client.

Recommendation for Consistency: While the current implementation is correct (the command throws exceptions on failure), consider adding explicit success tracking for consistency with other endpoints and to make the contract clearer:

if (model.IsV2Request())
{
    // V2 account registration
    await _setKeyConnectorKeyCommand.SetKeyConnectorKeyForUserAsync(user, model);
    return; // Explicitly return on success
}

The current code is functionally correct, but the explicit return makes it clearer that this is the success path.

await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, requestModel.AccountKeys.ToAccountKeysData(),
[setKeyConnectorUserKeyTask]);

await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
Copy link
Contributor

Choose a reason for hiding this comment

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

Event Logging Timing: Log Before Full Success

The event User_MigratedKeyToKeyConnector is logged at line 51, but AcceptOrgUserByOrgSsoIdAsync is called at line 53. If the organization user acceptance fails (line 53), the event will have already been logged, creating an inconsistent audit trail.

Risk: The audit log will show the user migrated to Key Connector, but they won't actually be accepted into the organization. This could cause confusion during security audits.

Recommendation: Move the event logging to after all operations complete successfully:

await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, requestModel.AccountKeys.ToAccountKeysData(),
    [setKeyConnectorUserKeyTask]);

await _acceptOrgUserCommand.AcceptOrgUserByOrgSsoIdAsync(requestModel.OrgIdentifier, user, _userService);

// Log event only after all operations succeed
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);

throw new BadRequestException("Already uses Key Connector.");
}

if (_currentContext.Organizations.Any(u =>
Copy link
Contributor

Choose a reason for hiding this comment

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

Security: Missing Verification of Organization SSO Configuration

The validation checks if the user is an admin/owner of ANY organization, but it doesn't verify:

  1. That the organization identified by OrgIdentifier (passed in the request) exists
  2. That the organization has Key Connector enabled/configured
  3. That the user is actually invited to or a member of that specific organization

This means a user could potentially call this endpoint with any OrgIdentifier, and as long as they're not an admin/owner of other orgs and don't already use Key Connector, the validation passes.

Recommendation: Add validation that:

  • The organization exists and has Key Connector configured
  • The user has a valid invitation or membership to that specific organization

This validation might belong in the command itself rather than this query, but it should exist somewhere before the key is set.

BEGIN
SET NOCOUNT ON

UPDATE
Copy link
Contributor

Choose a reason for hiding this comment

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

Security: Missing Row Count Validation (Migration Script)

This migration script has the same issue as the stored procedure definition. The UPDATE statement doesn't verify that a row was actually updated. If the user doesn't exist, the procedure completes successfully with 0 rows affected.

Recommendation: Add row count validation:

UPDATE [dbo].[User]
SET ...
WHERE [Id] = @Id

IF @@ROWCOUNT = 0
    THROW 50000, 'User not found for Key Connector key update', 1;

This should match the fix applied to the stored procedure in /src/Sql/dbo/KeyManagement/Stored Procedures/User_UpdateKeyConnectorUserKey.sql.

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.

2 participants