Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16603] Add userkey rotation v2 #5204

Draft
wants to merge 20 commits into
base: main
Choose a base branch
from
Draft

Conversation

quexten
Copy link
Contributor

@quexten quexten commented Dec 31, 2024

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-16603
Client PR: bitwarden/clients#12646

📔 Objective

Adds a new command for user-key rotation that includes the KDF settings too, and updates the password. Full description of why this change is made is on the client PR.

In short, adds a new endpoint rotate-user-account-keys which performs userkey rotation, while updating the masterpassword, and kdf parameters, to ensure a consistent update.

📸 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

Copy link
Contributor

github-actions bot commented Dec 31, 2024

Logo
Checkmarx One – Scan Summary & Details0d5f8dce-e52d-48d1-8db2-3f2c4539eba2

New Issues (5)

Checkmarx found the following issues in this Pull Request

Severity Issue Source File / Package Checkmarx Insight
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 93
detailsMethod RotateUserAccountKeysAsync at line 93 of /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs gets a parameter from a user ...
Attack Vector
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 93
detailsMethod RotateUserAccountKeysAsync at line 93 of /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs gets a parameter from a user ...
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1100
detailsMethod DeleteAttachment at line 1100 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from id. This paramete...
Attack Vector
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1100
detailsMethod DeleteAttachment at line 1100 of /src/Api/Vault/Controllers/CiphersController.cs gets a parameter from a user request from DeleteAttachment....
Attack Vector
LOW Log_Forging /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 93
detailsMethod RotateUserAccountKeysAsync at line 93 of /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs gets user input from element ...
Attack Vector
Fixed Issues (17)

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

Severity Issue Source File / Package
MEDIUM CSRF /src/Billing/Controllers/StripeController.cs: 164
MEDIUM CSRF /src/Billing/Controllers/RecoveryController.cs: 38
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1100
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1100
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1100
MEDIUM CSRF /src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs: 37
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1023
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 1046
MEDIUM CSRF /src/Api/Vault/Controllers/CiphersController.cs: 997
LOW Log_Forging /src/Api/AdminConsole/Controllers/ProvidersController.cs: 72
LOW Log_Forging /src/Billing/Controllers/RecoveryController.cs: 38
LOW Log_Forging /src/Billing/Controllers/RecoveryController.cs: 38
LOW Log_Forging /src/Billing/Controllers/RecoveryController.cs: 38
LOW Log_Forging /src/Api/AdminConsole/Controllers/ProvidersController.cs: 72
LOW Log_Forging /src/Api/AdminConsole/Controllers/ProvidersController.cs: 72
LOW Log_Forging /src/Billing/Controllers/StripeController.cs: 164
LOW Log_Forging /src/Billing/Controllers/StripeController.cs: 164

@quexten quexten changed the title Add userkey rotation v2 [PM-16603] Add userkey rotation v2 Jan 1, 2025
Copy link

codecov bot commented Jan 3, 2025

Codecov Report

Attention: Patch coverage is 78.26087% with 60 lines in your changes missing coverage. Please review.

Project coverage is 44.65%. Comparing base (a9a1230) to head (2820055).
Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
...frastructure.Dapper/Repositories/UserRepository.cs 0.00% 35 Missing ⚠️
...ure.EntityFramework/Repositories/UserRepository.cs 48.57% 16 Missing and 2 partials ⚠️
.../Request/Accounts/MasterPasswordUnlockDataModel.cs 90.69% 3 Missing and 1 partial ⚠️
...ey/Implementations/RotateUserAccountkeysCommand.cs 95.89% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5204      +/-   ##
==========================================
+ Coverage   44.32%   44.65%   +0.32%     
==========================================
  Files        1482     1491       +9     
  Lines       68376    68690     +314     
  Branches     6172     6198      +26     
==========================================
+ Hits        30307    30671     +364     
+ Misses      36761    36704      -57     
- Partials     1308     1315       +7     

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

@quexten quexten force-pushed the km/userkey-rotation-v2 branch from 7b07f5e to 865137d Compare January 3, 2025 11:20
@quexten quexten marked this pull request as ready for review January 3, 2025 11:31
@quexten quexten requested review from a team as code owners January 3, 2025 11:31
@quexten quexten requested a review from rr-bw January 3, 2025 11:31
@quexten quexten marked this pull request as draft January 9, 2025 14:53
@quexten quexten marked this pull request as ready for review January 9, 2025 15:00
@quexten quexten removed the request for review from rr-bw January 14, 2025 15:00
Copy link
Contributor

@JaredSnider-Bitwarden JaredSnider-Bitwarden left a comment

Choose a reason for hiding this comment

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

This is an excellent change. Thank you for drastically improving our key rotation processes. I have just a few comments / questions below:

@quexten quexten requested a review from a team as a code owner January 21, 2025 12:23
Copy link
Contributor

@JaredSnider-Bitwarden JaredSnider-Bitwarden left a comment

Choose a reason for hiding this comment

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

Nice work! LGTM!

@quexten
Copy link
Contributor Author

quexten commented Jan 22, 2025

Forgot to add the feature flag server-side; added it now.

@@ -415,6 +419,54 @@ public async Task PostKey([FromBody] UpdateKeyRequestModel model)
throw new BadRequestException(ModelState);
}

[HttpPost("rotate-user-account-keys")]
public async Task PostUserAccountKeys([FromBody] RotateUserAccountKeysModel model)
Copy link
Contributor

Choose a reason for hiding this comment

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

no unit test and integration test coverage 😢

Copy link
Contributor Author

@quexten quexten Jan 28, 2025

Choose a reason for hiding this comment

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

Added unit tests and integration tests, please confirm if I missed any obvious cases

Comment on lines +12 to +13
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

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

add custom validator instead to cover this scenario for Argon2. Example

/// <param name="model">All necessary information for rotation. Warning: Any encrypted data not included will be lost.</param>
/// <returns>An IdentityResult for verification of the master password hash</returns>
/// <exception cref="ArgumentNullException">User must be provided.</exception>
Task<IdentityResult> rotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
Copy link
Contributor

Choose a reason for hiding this comment

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

in C#, aren't methods supposed to start uppercase ?

Suggested change
Task<IdentityResult> rotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);

/// </summary>
/// <param name="model">All necessary information for rotation. Warning: Any encrypted data not included will be lost.</param>
/// <returns>An IdentityResult for verification of the master password hash</returns>
/// <exception cref="ArgumentNullException">User must be provided.</exception>
Copy link
Contributor

Choose a reason for hiding this comment

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

should also mention that InvalidOperationException is possible


if (!await _userService.CheckPasswordAsync(user, model.OldMasterPasswordHash))
{
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just throw error instead ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This just copies the existing behavior for RotateUserKeyAsync. Are you saying we should remove the entire "IdentityResult" and make it either return nothing (on success) or throw on password mismatch?

Comment on lines 98 to 127
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() ||
model.OrganizationUsers.Any() || model.WebAuthnKeys.Any())
{
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();

if (model.Ciphers.Any())
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}

if (model.Folders.Any())
{
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
}

if (model.Sends.Any())
{
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
}

if (model.EmergencyAccesses.Any())
{
saveEncryptedDataActions.Add(
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
}

if (model.OrganizationUsers.Any())
{
saveEncryptedDataActions.Add(
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
}

if (model.WebAuthnKeys.Any())
{
saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys));
}

await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions);
}
else
{
await _userRepository.ReplaceAsync(user);
}
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 do individual checks anyway below, it would be sufficient to just guard the _userRepository.UpdateUserKeyAndEncryptedDataAsync with the if (saveEncryptedDataActions.Any()), like so:

Suggested change
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() ||
model.OrganizationUsers.Any() || model.WebAuthnKeys.Any())
{
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
if (model.Ciphers.Any())
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}
if (model.Folders.Any())
{
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
}
if (model.Sends.Any())
{
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
}
if (model.EmergencyAccesses.Any())
{
saveEncryptedDataActions.Add(
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
}
if (model.OrganizationUsers.Any())
{
saveEncryptedDataActions.Add(
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
}
if (model.WebAuthnKeys.Any())
{
saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys));
}
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions);
}
else
{
await _userRepository.ReplaceAsync(user);
}
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new();
if (model.Ciphers.Any())
{
saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers));
}
if (model.Folders.Any())
{
saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders));
}
if (model.Sends.Any())
{
saveEncryptedDataActions.Add(_sendRepository.UpdateForKeyRotation(user.Id, model.Sends));
}
if (model.EmergencyAccesses.Any())
{
saveEncryptedDataActions.Add(
_emergencyAccessRepository.UpdateForKeyRotation(user.Id, model.EmergencyAccesses));
}
if (model.OrganizationUsers.Any())
{
saveEncryptedDataActions.Add(
_organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers));
}
if (model.WebAuthnKeys.Any())
{
saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys));
}
if (saveEncryptedDataActions.Any())
{
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions);
}
else
{
await _userRepository.ReplaceAsync(user);
}

@@ -219,31 +219,19 @@ public async Task UpdateUserKeyAndEncryptedDataAsync(
await using var transaction = connection.BeginTransaction();
try
{
// Update user
await using (var cmd = new SqlCommand("[dbo].[User_UpdateKeys]", connection, transaction))
if (user.AccountRevisionDate == null)
Copy link
Contributor

@mzieniukbw mzieniukbw Jan 23, 2025

Choose a reason for hiding this comment

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

The User model suggests, this field can never be null

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 wonder why the SP had handling for this then:

[AccountRevisionDate] = ISNULL(@AccountRevisionDate, @RevisionDate),

}

// Update user
await ReplaceAsync(user);
Copy link
Contributor

Choose a reason for hiding this comment

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

this method will operate under new connection, hence commit immediately. There is a reason why the previous SP existed.
This repository overrides ReplaceAsync and protects Key and Master Password in some way, might be worth to check and possibly revert to use the previous SP.
Note: Previously used SP could be refactored with connection.ExecuteAsync, where transaction can be passed in. That way we don't have to deal with SqlDbType and @ named arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This repository overrides ReplaceAsync and protects Key and Master Password in some way, might be worth to check and possibly revert to use the previous SP.

Hmm, this is actually a bug leading to keys not being adequately protected when doing a key-rotation :| So if we want to use the stored procedure, we would have to fix it to also protect the data properly.

Copy link
Contributor Author

@quexten quexten Jan 27, 2025

Choose a reason for hiding this comment

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

Replaced with:

            ProtectData(user);
            await connection.ExecuteAsync(
                $"[{Schema}].[{Table}_Update]",
                user,
                transaction: transaction,
                commandType: CommandType.StoredProcedure);

@@ -0,0 +1,7 @@
--- Remove old updatekeys procedure that is now unused ---

IF OBJECT_ID('[dbo].[User_UpdateKeys]') IS NOT NULL
Copy link
Contributor

@mzieniukbw mzieniukbw Jan 23, 2025

Choose a reason for hiding this comment

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

Do you want to delete this SP from repo ? Location:

CREATE PROCEDURE [dbo].[User_UpdateKeys]

@quexten quexten marked this pull request as draft January 27, 2025 10:50
@quexten quexten force-pushed the km/userkey-rotation-v2 branch from f3c884d to 5dd74db Compare January 27, 2025 13:50
@quexten quexten removed the request for review from a team January 27, 2025 13:51
@quexten quexten requested a review from mzieniukbw January 28, 2025 11:31
@quexten quexten marked this pull request as ready for review January 28, 2025 11:31
Copy link
Contributor

@Thomas-Avery Thomas-Avery left a comment

Choose a reason for hiding this comment

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

Looking good, a few minor things to take a look at.

An overall suggestion for net new code, can we use #nullable enable at the top of the files to enable nullable reference types? This is currently a mix of using that feature in some places and not in others, which I find confusing.

[StringLength(50)]
public string MasterPasswordHint { get; set; }

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.

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
using System.ComponentModel.DataAnnotations;
#nullable enable
using System.ComponentModel.DataAnnotations;

I would recommend keeping consistent with using nullable reference types on all these request models.
At the moment, the AccountsKeyManagementController uses it.

So AccountKeysRequestModel, RotateUserAccountKeysAndDataRequestModel, MasterPasswordUnlockDataModel, UnlockDataRequestModel, and AccountDataRequestModel.

Feel free to reach out if you have questions around this.

@@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Api.KeyManagement.Models.Request.Accounts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
namespace Bit.Api.KeyManagement.Models.Request.Accounts;
namespace Bit.Api.KeyManagement.Models.Requests;

⛏️ Optional.

There is a common convention in .NET projects to have the namespace match the file location.

Some IDEs will scan for it https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0130.

In the server project for the most part we follow it. There are areas we don't currently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed these

@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Api.KeyManagement.Models.Request.Accounts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
namespace Bit.Api.KeyManagement.Models.Request.Accounts;
namespace Bit.Api.KeyManagement.Models.Requests;

using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;

namespace Bit.Api.KeyManagement.Models.Request.Accounts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
namespace Bit.Api.KeyManagement.Models.Request.Accounts;
namespace Bit.Api.KeyManagement.Models.Requests;

using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;

namespace Bit.Api.KeyManagement.Models.Request.Accounts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
namespace Bit.Api.KeyManagement.Models.Request.Accounts;
namespace Bit.Api.KeyManagement.Models.Requests;

Comment on lines +206 to +239
try
{
// Update user
var userEntity = await dbContext.Users.FindAsync(user.Id);
if (userEntity == null)
{
throw new ArgumentException("User not found", nameof(user));
}

userEntity.SecurityStamp = user.SecurityStamp;
userEntity.Key = user.Key;
userEntity.MasterPassword = user.MasterPassword;
userEntity.PrivateKey = user.PrivateKey;
userEntity.LastKeyRotationDate = user.LastKeyRotationDate;
userEntity.AccountRevisionDate = user.AccountRevisionDate;
userEntity.RevisionDate = user.RevisionDate;

await dbContext.SaveChangesAsync();

// Update re-encrypted data
foreach (var action in updateDataActions)
{
// connection and transaction aren't used in EF
await action();
}

await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
try
{
// Update user
var userEntity = await dbContext.Users.FindAsync(user.Id);
if (userEntity == null)
{
throw new ArgumentException("User not found", nameof(user));
}
userEntity.SecurityStamp = user.SecurityStamp;
userEntity.Key = user.Key;
userEntity.MasterPassword = user.MasterPassword;
userEntity.PrivateKey = user.PrivateKey;
userEntity.LastKeyRotationDate = user.LastKeyRotationDate;
userEntity.AccountRevisionDate = user.AccountRevisionDate;
userEntity.RevisionDate = user.RevisionDate;
await dbContext.SaveChangesAsync();
// Update re-encrypted data
foreach (var action in updateDataActions)
{
// connection and transaction aren't used in EF
await action();
}
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
// Update user
var userEntity = await dbContext.Users.FindAsync(user.Id);
if (userEntity == null)
{
throw new ArgumentException("User not found", nameof(user));
}
userEntity.SecurityStamp = user.SecurityStamp;
userEntity.Key = user.Key;
userEntity.MasterPassword = user.MasterPassword;
userEntity.PrivateKey = user.PrivateKey;
userEntity.LastKeyRotationDate = user.LastKeyRotationDate;
userEntity.AccountRevisionDate = user.AccountRevisionDate;
userEntity.RevisionDate = user.RevisionDate;
await dbContext.SaveChangesAsync();
// Update re-encrypted data
foreach (var action in updateDataActions)
{
// connection and transaction aren't used in EF
await action();
}
await transaction.CommitAsync();

⛏️ Optional

With the default behavior, you get rollbacks without having to do the try catch https://learn.microsoft.com/en-us/ef/core/saving/transactions#default-transaction-behavior

@@ -93,4 +105,46 @@ await sutProvider.GetDependency<IRegenerateUserAsymmetricKeysCommand>().Received
Arg.Is(orgUsers),
Arg.Is(accessDetails));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Would expect unit tests for failure cases.

_userService.GetUserByPrincipalAsync returning null and throwing UnauthorizedAccessException.

var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel); returning result.Errors and throwing BadRequestException


ProtectData(user);
await connection.ExecuteAsync(
$"[{Schema}].[{Table}_Update]",
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems odd to be doing a complete replacement of the user entity when this is called UpdateUserKeyAndEncryptedDataV2Async. If anything changes on that user between the controller fetch and calling this repository method, we will overwrite it.

Can we do the following or is there something I'm missing?:

            if (user.AccountRevisionDate == null)
            {
                user.AccountRevisionDate = user.RevisionDate;
            }
            
            // Update user values using data protection
            ProtectData(user);
            
              // Update user columns needed for key rotation
                   await connection.ExecuteAsync(
               $"[{Schema}].[User_UpdateKeys]",
               new
               {
                   user.Id,
                   user.SecurityStamp,
                   user.Key,
                   user.PrivateKey,
                   user.RevisionDate,
                   user.AccountRevisionDate,
                   user.LastKeyRotationDate
               },
               commandType: CommandType.StoredProcedure);

            //  Update re-encrypted data
            foreach (var action in updateDataActions)
            {
                await action(connection, transaction);
            }      

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The above is missing at least the masterpassword hash and masterpassword hint.

As it is implemented right now, we do our sanity/safety checks regarding the: public key, email, kdf settings, before calling UpdateUserKeyAndEncryptedDataV2Async, with one user object that has been validated.

If we just update the above, it could be the case that f.e the kdf setting changes after the sanity check, but before the above update, leading to a key that does not match the kdf settings being stored.

So we would either need to include:
public key, email, kdf settings, in the update too, replacing them?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like we're missing updating those fields in the EF code too then?

I'm confused here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, that means that the EF code is wrong. I think that means the EF code needs better test coverage in this case because this could have slipped through.

Copy link
Contributor

@Thomas-Avery Thomas-Avery Jan 29, 2025

Choose a reason for hiding this comment

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

I know we recently published some new contributing docs info on repository integration tests.
https://contributing.bitwarden.com/contributing/testing/database/
https://contributing.bitwarden.com/getting-started/server/database/ef/#testing-ef-changes

Might be something to look into here.

src/Infrastructure.Dapper/Repositories/UserRepository.cs Outdated Show resolved Hide resolved
Copy link
Contributor

LaunchDarkly flag references

🔍 1 flag added or modified

Name Key Aliases found Info
UserKey Rotation v2 userkey-rotation-v2

@quexten quexten marked this pull request as draft January 29, 2025 14:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants