1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-24 20:58:49 -05:00

Remove key rotation v1 (#5939)

This commit is contained in:
Bernd Schoolmann 2025-06-24 16:32:38 +02:00 committed by GitHub
parent 70703cb3b0
commit 34580f0472
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 13 additions and 384 deletions

View File

@ -1,34 +1,21 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Auth.Models.Request;
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -45,22 +32,9 @@ public class AccountsController : Controller
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_organizationUserValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator;
public AccountsController(
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository,
@ -69,17 +43,8 @@ public class AccountsController : Controller
IPolicyService policyService,
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
IRotateUserKeyCommand rotateUserKeyCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
IFeatureService featureService
)
{
_organizationService = organizationService;
@ -89,15 +54,8 @@ public class AccountsController : Controller
_policyService = policyService;
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
_rotateUserKeyCommand = rotateUserKeyCommand;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_cipherValidator = cipherValidator;
_folderValidator = folderValidator;
_sendValidator = sendValidator;
_emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator;
}
@ -313,45 +271,6 @@ public class AccountsController : Controller
throw new BadRequestException(ModelState);
}
[Obsolete("Replaced by the safer rotate-user-account-keys endpoint.")]
[HttpPost("key")]
public async Task PostKey([FromBody] UpdateKeyRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
var dataModel = new RotateUserKeyData
{
MasterPasswordHash = model.MasterPasswordHash,
Key = model.Key,
PrivateKey = model.PrivateKey,
Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.Folders),
Sends = await _sendValidator.ValidateAsync(user, model.Sends),
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys),
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys),
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.WebAuthnKeys)
};
var result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel);
if (result.Succeeded)
{
return;
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await Task.Delay(2000);
throw new BadRequestException(ModelState);
}
[HttpPost("security-stamp")]
public async Task PostSecurityStamp([FromBody] SecretVerificationRequestModel model)
{

View File

@ -38,7 +38,6 @@ public static class UserServiceCollectionExtensions
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
services.AddScoped<IRotateUserAccountKeysCommand, RotateUserAccountKeysCommand>();
}

View File

@ -1,20 +0,0 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Core.KeyManagement.Models.Data;
public class RotateUserKeyData
{
public string MasterPasswordHash { get; set; }
public string Key { get; set; }
public string PrivateKey { get; set; }
public IEnumerable<Cipher> Ciphers { get; set; }
public IEnumerable<Folder> Folders { get; set; }
public IReadOnlyList<Send> Sends { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.SqlClient;
namespace Bit.Core.KeyManagement.UserKey;
@ -18,3 +19,12 @@ public interface IRotateUserAccountKeysCommand
/// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception>
Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
}
/// <summary>
/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted
/// data during a key rotation should use this to implement its own database call. The user repository loops through
/// these during a key rotation.
/// <para>Note: connection and transaction are only used for Dapper. They won't be available in EF</para>
/// </summary>
public delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null,
SqlTransaction transaction = null);

View File

@ -1,29 +0,0 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.SqlClient;
namespace Bit.Core.KeyManagement.UserKey;
/// <summary>
/// Responsible for rotation of a user key and updating database with re-encrypted data
/// </summary>
public interface IRotateUserKeyCommand
{
/// <summary>
/// Sets a new user key and updates all encrypted data.
/// </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>
Task<IdentityResult> RotateUserKeyAsync(User user, RotateUserKeyData model);
}
/// <summary>
/// A type used to implement updates to the database for key rotations. Each domain that requires an update of encrypted
/// data during a key rotation should use this to implement its own database call. The user repository loops through
/// these during a key rotation.
/// <para>Note: connection and transaction are only used for Dapper. They won't be available in EF</para>
/// </summary>
public delegate Task UpdateEncryptedDataForKeyRotation(SqlConnection connection = null,
SqlTransaction transaction = null);

View File

@ -1,121 +0,0 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.KeyManagement.UserKey.Implementations;
/// <inheritdoc />
public class RotateUserKeyCommand : IRotateUserKeyCommand
{
private readonly IUserService _userService;
private readonly IUserRepository _userRepository;
private readonly ICipherRepository _cipherRepository;
private readonly IFolderRepository _folderRepository;
private readonly ISendRepository _sendRepository;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPushNotificationService _pushService;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly IWebAuthnCredentialRepository _credentialRepository;
/// <summary>
/// Instantiates a new <see cref="RotateUserKeyCommand"/>
/// </summary>
/// <param name="userService">Master password hash validation</param>
/// <param name="userRepository">Updates user keys and re-encrypted data if needed</param>
/// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param>
/// <param name="folderRepository">Provides a method to update re-encrypted folder data</param>
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository,
ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository,
IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository,
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository)
{
_userService = userService;
_userRepository = userRepository;
_cipherRepository = cipherRepository;
_folderRepository = folderRepository;
_sendRepository = sendRepository;
_emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository;
_pushService = pushService;
_identityErrorDescriber = errors;
_credentialRepository = credentialRepository;
}
/// <inheritdoc />
public async Task<IdentityResult> RotateUserKeyAsync(User user, RotateUserKeyData model)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (!await _userService.CheckPasswordAsync(user, model.MasterPasswordHash))
{
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString();
user.Key = model.Key;
user.PrivateKey = model.PrivateKey;
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);
}
await _pushService.PushLogOutAsync(user.Id, excludeCurrentContextFromPush: true);
return IdentityResult.Success;
}
}

View File

@ -1,27 +1,16 @@
using System.Security.Claims;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
@ -39,23 +28,10 @@ public class AccountsControllerTests : IDisposable
private readonly IProviderUserRepository _providerUserRepository;
private readonly IPolicyService _policyService;
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
private readonly IFeatureService _featureService;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_resetPasswordValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyRotationValidator;
public AccountsControllerTests()
{
_userService = Substitute.For<IUserService>();
@ -64,21 +40,9 @@ public class AccountsControllerTests : IDisposable
_providerUserRepository = Substitute.For<IProviderUserRepository>();
_policyService = Substitute.For<IPolicyService>();
_setInitialMasterPasswordCommand = Substitute.For<ISetInitialMasterPasswordCommand>();
_rotateUserKeyCommand = Substitute.For<IRotateUserKeyCommand>();
_twoFactorIsEnabledQuery = Substitute.For<ITwoFactorIsEnabledQuery>();
_tdeOffboardingPasswordCommand = Substitute.For<ITdeOffboardingPasswordCommand>();
_featureService = Substitute.For<IFeatureService>();
_cipherValidator =
Substitute.For<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>>();
_folderValidator =
Substitute.For<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>>();
_sendValidator = Substitute.For<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>>();
_emergencyAccessValidator = Substitute.For<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>,
IEnumerable<EmergencyAccess>>>();
_webauthnKeyRotationValidator = Substitute.For<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>>();
_resetPasswordValidator = Substitute
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>>();
_sut = new AccountsController(
_organizationService,
@ -88,15 +52,8 @@ public class AccountsControllerTests : IDisposable
_policyService,
_setInitialMasterPasswordCommand,
_tdeOffboardingPasswordCommand,
_rotateUserKeyCommand,
_twoFactorIsEnabledQuery,
_featureService,
_cipherValidator,
_folderValidator,
_sendValidator,
_emergencyAccessValidator,
_resetPasswordValidator,
_webauthnKeyRotationValidator
_featureService
);
}

View File

@ -1,86 +0,0 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey.Implementations;
using Bit.Core.Platform.Push;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.UserKey;
[SutProviderCustomize]
public class RotateUserKeyCommandTests
{
[Theory, BitAutoData]
public async Task RotateUserKeyAsync_Success(SutProvider<RotateUserKeyCommand> sutProvider, User user,
RotateUserKeyData model)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(true);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
var result = await sutProvider.Sut.RotateUserKeyAsync(user, model);
Assert.Equal(IdentityResult.Success, result);
}
[Theory, BitAutoData]
public async Task RotateUserKeyAsync_InvalidMasterPasswordHash_ReturnsFailedIdentityResult(
SutProvider<RotateUserKeyCommand> sutProvider, User user, RotateUserKeyData model)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(false);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
var result = await sutProvider.Sut.RotateUserKeyAsync(user, model);
Assert.False(result.Succeeded);
}
[Theory, BitAutoData]
public async Task RotateUserKeyAsync_LogsOutUser(
SutProvider<RotateUserKeyCommand> sutProvider, User user, RotateUserKeyData model)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.MasterPasswordHash)
.Returns(true);
foreach (var webauthnCred in model.WebAuthnKeys)
{
var dbWebauthnCred = new WebAuthnCredential
{
EncryptedPublicKey = "encryptedPublicKey",
EncryptedUserKey = "encryptedUserKey"
};
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetByIdAsync(webauthnCred.Id, user.Id)
.Returns(dbWebauthnCred);
}
await sutProvider.Sut.RotateUserKeyAsync(user, model);
await sutProvider.GetDependency<IPushNotificationService>().ReceivedWithAnyArgs()
.PushLogOutAsync(default, default);
}
}