mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[PM-16603] Add userkey rotation v2 (#5204)
* Implement userkey rotation v2 * Update request models * Cleanup * Update tests * Improve test * Add tests * Fix formatting * Fix test * Remove whitespace * Fix namespace * Enable nullable on models * Fix build * Add tests and enable nullable on masterpasswordunlockdatamodel * Fix test * Remove rollback * Add tests * Make masterpassword hint optional * Update user query * Add EF test * Improve test * Cleanup * Set masterpassword hint * Remove connection close * Add tests for invalid kdf types * Update test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommandTests.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix formatting * Update src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Fix imports * Fix tests * Remove null check * Add rollback --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
@ -355,6 +355,7 @@ 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)
|
||||
{
|
||||
|
@ -0,0 +1,66 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class MasterPasswordUnlockDataModel : IValidatableObject
|
||||
{
|
||||
public required KdfType KdfType { get; set; }
|
||||
public required int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
[StrictEmailAddress]
|
||||
[StringLength(256)]
|
||||
public required string Email { get; set; }
|
||||
[StringLength(300)]
|
||||
public required string MasterKeyAuthenticationHash { get; set; }
|
||||
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; set; }
|
||||
[StringLength(50)]
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (KdfType == KdfType.PBKDF2_SHA256)
|
||||
{
|
||||
if (KdfMemory.HasValue || KdfParallelism.HasValue)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory and KdfParallelism must be null for PBKDF2_SHA256", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
|
||||
}
|
||||
}
|
||||
else if (KdfType == KdfType.Argon2id)
|
||||
{
|
||||
if (!KdfMemory.HasValue || !KdfParallelism.HasValue)
|
||||
{
|
||||
yield return new ValidationResult("KdfMemory and KdfParallelism must have values for Argon2id", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ValidationResult("Invalid KdfType", new[] { nameof(KdfType) });
|
||||
}
|
||||
}
|
||||
|
||||
public MasterPasswordUnlockData ToUnlockData()
|
||||
{
|
||||
var data = new MasterPasswordUnlockData
|
||||
{
|
||||
KdfType = KdfType,
|
||||
KdfIterations = KdfIterations,
|
||||
KdfMemory = KdfMemory,
|
||||
KdfParallelism = KdfParallelism,
|
||||
|
||||
Email = Email,
|
||||
|
||||
MasterKeyAuthenticationHash = MasterKeyAuthenticationHash,
|
||||
MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey,
|
||||
MasterPasswordHint = MasterPasswordHint
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
@ -1,10 +1,23 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -19,18 +32,45 @@ public class AccountsKeyManagementController : Controller
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IRotateUserAccountKeysCommand _rotateUserAccountKeysCommand;
|
||||
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 AccountsKeyManagementController(IUserService userService,
|
||||
IFeatureService featureService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IEmergencyAccessRepository emergencyAccessRepository,
|
||||
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand)
|
||||
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,
|
||||
IRotateUserAccountKeysCommand rotateUserKeyCommandV2,
|
||||
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)
|
||||
{
|
||||
_userService = userService;
|
||||
_featureService = featureService;
|
||||
_regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_emergencyAccessRepository = emergencyAccessRepository;
|
||||
_rotateUserAccountKeysCommand = rotateUserKeyCommandV2;
|
||||
_cipherValidator = cipherValidator;
|
||||
_folderValidator = folderValidator;
|
||||
_sendValidator = sendValidator;
|
||||
_emergencyAccessValidator = emergencyAccessValidator;
|
||||
_organizationUserValidator = organizationUserValidator;
|
||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||
}
|
||||
|
||||
[HttpPost("regenerate-keys")]
|
||||
@ -47,4 +87,45 @@ public class AccountsKeyManagementController : Controller
|
||||
await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),
|
||||
usersOrganizationAccounts, designatedEmergencyAccess);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("rotate-user-account-keys")]
|
||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var dataModel = new RotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||
|
||||
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
|
||||
AccountPublicKey = model.AccountKeys.AccountPublicKey,
|
||||
|
||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
|
||||
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
|
||||
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),
|
||||
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),
|
||||
|
||||
Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),
|
||||
Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),
|
||||
Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends),
|
||||
};
|
||||
|
||||
var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class AccountKeysRequestModel
|
||||
{
|
||||
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
|
||||
public required string AccountPublicKey { get; set; }
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class RotateUserAccountKeysAndDataRequestModel
|
||||
{
|
||||
[StringLength(300)]
|
||||
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||
public required UnlockDataRequestModel AccountUnlockData { get; set; }
|
||||
public required AccountKeysRequestModel AccountKeys { get; set; }
|
||||
public required AccountDataRequestModel AccountData { get; set; }
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class UnlockDataRequestModel
|
||||
{
|
||||
// All methods to get to the userkey
|
||||
public required MasterPasswordUnlockDataModel MasterPasswordUnlockData { get; set; }
|
||||
public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; }
|
||||
public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class AccountDataRequestModel
|
||||
{
|
||||
public required IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }
|
||||
public required IEnumerable<FolderWithIdRequestModel> Folders { get; set; }
|
||||
public required IEnumerable<SendWithIdRequestModel> Sends { get; set; }
|
||||
}
|
Reference in New Issue
Block a user