1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-18 16:11:28 -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
8 changed files with 13 additions and 384 deletions

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;
}
}