mirror of
https://github.com/bitwarden/server.git
synced 2026-04-28 09:11:06 -05:00
[PM-33162] Refactor user key rotation (#7201)
* Refactor user key rotation to use base data composition * Update tests
This commit is contained in:
@@ -12,9 +12,9 @@ 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.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -93,7 +93,7 @@ public class AccountsKeyManagementController : Controller
|
||||
|
||||
|
||||
[HttpPost("key-management/rotate-user-account-keys")]
|
||||
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
@@ -101,25 +101,32 @@ public class AccountsKeyManagementController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var dataModel = new RotateUserAccountKeysData
|
||||
var dataModel = new PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,
|
||||
|
||||
AccountKeys = model.AccountKeys.ToAccountKeysData(),
|
||||
|
||||
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),
|
||||
DeviceKeys = await _deviceValidator.ValidateAsync(user, model.AccountUnlockData.DeviceKeyUnlockData),
|
||||
V2UpgradeToken = model.AccountUnlockData.V2UpgradeToken?.ToData(),
|
||||
|
||||
Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),
|
||||
Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),
|
||||
Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends),
|
||||
MasterPasswordHint = model.AccountUnlockData.MasterPasswordUnlockData.MasterPasswordHint,
|
||||
MasterPasswordAuthenticationData = model.AccountUnlockData.MasterPasswordUnlockData.ToAuthenticationData(),
|
||||
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToMasterPasswordUnlockData(),
|
||||
BaseData = new BaseRotateUserAccountKeysData
|
||||
{
|
||||
AccountKeys = model.AccountKeys.ToAccountKeysData(),
|
||||
EmergencyAccesses =
|
||||
await _emergencyAccessValidator.ValidateAsync(user,
|
||||
model.AccountUnlockData.EmergencyAccessUnlockData),
|
||||
OrganizationUsers =
|
||||
await _organizationUserValidator.ValidateAsync(user,
|
||||
model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),
|
||||
WebAuthnKeys =
|
||||
await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),
|
||||
DeviceKeys = await _deviceValidator.ValidateAsync(user, model.AccountUnlockData.DeviceKeyUnlockData),
|
||||
V2UpgradeToken = model.AccountUnlockData.V2UpgradeToken?.ToData(),
|
||||
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);
|
||||
var result = await _rotateUserAccountKeysCommand.PasswordChangeAndRotateUserAccountKeysAsync(user, dataModel);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
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;
|
||||
namespace Bit.Api.KeyManagement.Models.Requests;
|
||||
|
||||
public class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject
|
||||
{
|
||||
@@ -45,22 +43,35 @@ public class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject
|
||||
}
|
||||
}
|
||||
|
||||
public MasterPasswordUnlockAndAuthenticationData ToUnlockData()
|
||||
public MasterPasswordAuthenticationData ToAuthenticationData()
|
||||
{
|
||||
var data = new MasterPasswordUnlockAndAuthenticationData
|
||||
return new MasterPasswordAuthenticationData
|
||||
{
|
||||
KdfType = KdfType,
|
||||
KdfIterations = KdfIterations,
|
||||
KdfMemory = KdfMemory,
|
||||
KdfParallelism = KdfParallelism,
|
||||
|
||||
Email = Email,
|
||||
|
||||
MasterKeyAuthenticationHash = MasterKeyAuthenticationHash,
|
||||
MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey,
|
||||
MasterPasswordHint = MasterPasswordHint
|
||||
Kdf = new KdfSettings
|
||||
{
|
||||
KdfType = KdfType,
|
||||
Iterations = KdfIterations,
|
||||
Memory = KdfMemory,
|
||||
Parallelism = KdfParallelism,
|
||||
},
|
||||
Salt = Email,
|
||||
MasterPasswordAuthenticationHash = MasterKeyAuthenticationHash,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
public MasterPasswordUnlockData ToMasterPasswordUnlockData()
|
||||
{
|
||||
return new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = new KdfSettings
|
||||
{
|
||||
KdfType = KdfType,
|
||||
Iterations = KdfIterations,
|
||||
Memory = KdfMemory,
|
||||
Parallelism = KdfParallelism,
|
||||
},
|
||||
Salt = Email,
|
||||
MasterKeyWrappedUserKey = MasterKeyEncryptedUserKey,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class MasterPasswordUnlockAndAuthenticationData
|
||||
{
|
||||
public KdfType KdfType { get; set; }
|
||||
public int KdfIterations { get; set; }
|
||||
public int? KdfMemory { get; set; }
|
||||
public int? KdfParallelism { get; set; }
|
||||
|
||||
public required string Email { get; set; }
|
||||
public required string MasterKeyAuthenticationHash { get; set; }
|
||||
/// <summary>
|
||||
/// The user's symmetric key encrypted with their master key.
|
||||
/// Also known as "MasterKeyWrappedUserKey"
|
||||
/// </summary>
|
||||
public required string MasterKeyEncryptedUserKey { get; set; }
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
public bool ValidateForUser(User user)
|
||||
{
|
||||
if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if (Email != user.Email)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace Bit.Core.KeyManagement.UserKey;
|
||||
public interface IRotateUserAccountKeysCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a new user key and updates all encrypted data.
|
||||
/// Sets a new user key and updates all encrypted data and data associated with a password change.
|
||||
/// </summary>
|
||||
/// <param name="model">All necessary information for rotation. If data is not included, this will lead to the change being rejected.</param>
|
||||
/// <param name="model">All necessary information for rotation and password change. If data is not included, this will lead to the change being rejected.</param>
|
||||
/// <returns>An IdentityResult for verification of the master password hash</returns>
|
||||
/// <exception cref="ArgumentNullException">User must be provided.</exception>
|
||||
/// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception>
|
||||
Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
|
||||
Task<IdentityResult> PasswordChangeAndRotateUserAccountKeysAsync(User user, PasswordChangeAndRotateUserAccountKeysData model);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Bit.Core.KeyManagement.Utilities;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -32,7 +32,6 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||
private readonly IPasswordHasher<User> _passwordHasher;
|
||||
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
|
||||
@@ -56,8 +55,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
IDeviceRepository deviceRepository,
|
||||
IPasswordHasher<User> passwordHasher,
|
||||
IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository,
|
||||
IUserSignatureKeyPairRepository userSignatureKeyPairRepository,
|
||||
IFeatureService featureService)
|
||||
IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
@@ -72,11 +70,10 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
_credentialRepository = credentialRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model)
|
||||
public async Task<IdentityResult> PasswordChangeAndRotateUserAccountKeysAsync(User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
@@ -88,45 +85,22 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
user.RevisionDate = user.AccountRevisionDate = now;
|
||||
user.LastKeyRotationDate = now;
|
||||
|
||||
// V2UpgradeToken is only valid for V1 users transitioning to V2.
|
||||
// For V2 users the token is semantically invalid — discard it and perform a full logout.
|
||||
var shouldPersistV2UpgradeToken = model.V2UpgradeToken != null && !IsV2EncryptionUserAsync(user);
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
user.V2UpgradeToken = model.V2UpgradeToken!.ToJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.V2UpgradeToken = null;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
}
|
||||
model.ValidateForUser(user);
|
||||
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
|
||||
var shouldPersistV2UpgradeToken = await BaseRotateUserAccountKeysAsync(model.BaseData, user, saveEncryptedDataActions);
|
||||
|
||||
await UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
UpdateUnlockMethods(model, user, saveEncryptedDataActions);
|
||||
UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
user.Key = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash);
|
||||
user.MasterPasswordHint = model.MasterPasswordHint;
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id,
|
||||
reason: PushNotificationLogOutReason.KeyRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
|
||||
await HandlePushNotificationAsync(shouldPersistV2UpgradeToken, user);
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
public async Task RotateV2AccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
private async Task RotateV2AccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidateV2Encryption(model);
|
||||
await ValidateVerifyingKeyUnchangedAsync(model, user);
|
||||
@@ -137,7 +111,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
|
||||
}
|
||||
|
||||
public void UpgradeV1ToV2Keys(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
private void UpgradeV1ToV2Keys(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidateV2Encryption(model);
|
||||
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData));
|
||||
@@ -146,11 +120,11 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
user.SecurityVersion = model.AccountKeys.SecurityStateData.SecurityVersion;
|
||||
}
|
||||
|
||||
public async Task UpdateAccountKeysAsync(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
internal async Task UpdateAccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
ValidatePublicKeyEncryptionKeyPairUnchanged(model, user);
|
||||
|
||||
if (IsV2EncryptionUserAsync(user))
|
||||
if (IsV2EncryptionUser(user))
|
||||
{
|
||||
await RotateV2AccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
}
|
||||
@@ -171,7 +145,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
user.PrivateKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;
|
||||
}
|
||||
|
||||
public void UpdateUserData(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
internal void UpdateUserData(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
// The revision date has to be updated so that de-synced clients don't accidentally post over the re-encrypted data
|
||||
// with an old-user key-encrypted copy
|
||||
@@ -196,39 +170,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateUnlockMethods(RotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
if (!model.MasterPasswordUnlockData.ValidateForUser(user))
|
||||
{
|
||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
||||
}
|
||||
// Update master password authentication & unlock
|
||||
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
|
||||
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);
|
||||
user.MasterPasswordHint = model.MasterPasswordUnlockData.MasterPasswordHint;
|
||||
|
||||
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 (model.DeviceKeys.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsV2EncryptionUserAsync(User user)
|
||||
private static bool IsV2EncryptionUser(User user)
|
||||
{
|
||||
// Returns whether the user is a V2 user based on the private key's encryption type.
|
||||
ArgumentNullException.ThrowIfNull(user);
|
||||
@@ -236,7 +178,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
return isPrivateKeyEncryptionV2;
|
||||
}
|
||||
|
||||
private async Task ValidateVerifyingKeyUnchangedAsync(RotateUserAccountKeysData model, User user)
|
||||
private async Task ValidateVerifyingKeyUnchangedAsync(BaseRotateUserAccountKeysData model, User user)
|
||||
{
|
||||
var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id) ?? throw new InvalidOperationException("User does not have a signature key pair.");
|
||||
if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair!.VerifyingKey)
|
||||
@@ -245,7 +187,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePublicKeyEncryptionKeyPairUnchanged(RotateUserAccountKeysData model, User user)
|
||||
private static void ValidatePublicKeyEncryptionKeyPairUnchanged(BaseRotateUserAccountKeysData model, User user)
|
||||
{
|
||||
var publicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
if (publicKey != user.PublicKey)
|
||||
@@ -254,7 +196,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateV2Encryption(RotateUserAccountKeysData model)
|
||||
private static void ValidateV2Encryption(BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
if (model.AccountKeys.SignatureKeyPairData == null)
|
||||
{
|
||||
@@ -282,4 +224,66 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
throw new InvalidOperationException("No signed security state provider for V2 user");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBaseUnlockMethods(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
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 (model.DeviceKeys.Any())
|
||||
{
|
||||
saveEncryptedDataActions.Add(_deviceRepository.UpdateKeysForRotationAsync(user.Id, model.DeviceKeys));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> BaseRotateUserAccountKeysAsync(BaseRotateUserAccountKeysData baseModel, User user,
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
user.RevisionDate = user.AccountRevisionDate = now;
|
||||
user.LastKeyRotationDate = now;
|
||||
|
||||
// V2UpgradeToken is only valid for V1 users transitioning to V2.
|
||||
// For V2 users the token is semantically invalid — discard it and perform a full logout.
|
||||
var shouldPersistV2UpgradeToken = baseModel.V2UpgradeToken != null && !IsV2EncryptionUser(user);
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
user.V2UpgradeToken = baseModel.V2UpgradeToken!.ToJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.V2UpgradeToken = null;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
await UpdateAccountKeysAsync(baseModel, user, saveEncryptedDataActions);
|
||||
UpdateBaseUnlockMethods(baseModel, user, saveEncryptedDataActions);
|
||||
UpdateUserData(baseModel, user, saveEncryptedDataActions);
|
||||
return shouldPersistV2UpgradeToken;
|
||||
}
|
||||
|
||||
private async Task HandlePushNotificationAsync(bool shouldPersistV2UpgradeToken, User user)
|
||||
{
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id,
|
||||
reason: PushNotificationLogOutReason.KeyRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
namespace Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
|
||||
public class RotateUserAccountKeysData
|
||||
public class BaseRotateUserAccountKeysData
|
||||
{
|
||||
// Authentication for this requests
|
||||
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||
|
||||
public required UserAccountKeysData AccountKeys { get; set; }
|
||||
|
||||
// All methods to get to the userkey
|
||||
public required MasterPasswordUnlockAndAuthenticationData MasterPasswordUnlockData { get; set; }
|
||||
// Common methods to get the userKey
|
||||
public required IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
|
||||
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
public required IEnumerable<Device> DeviceKeys { get; set; }
|
||||
public V2UpgradeTokenData? V2UpgradeToken { get; set; }
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
// User vault data encrypted by the userKey
|
||||
public required IEnumerable<Cipher> Ciphers { get; set; }
|
||||
public required IEnumerable<Folder> Folders { get; set; }
|
||||
public required IReadOnlyList<Send> Sends { get; set; }
|
||||
@@ -0,0 +1,31 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
|
||||
public class PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
// Authentication for this request
|
||||
public required string OldMasterKeyAuthenticationHash { get; set; }
|
||||
|
||||
public required MasterPasswordAuthenticationData MasterPasswordAuthenticationData { get; set; }
|
||||
public required MasterPasswordUnlockData MasterPasswordUnlockData { get; set; }
|
||||
public string? MasterPasswordHint { get; set; }
|
||||
|
||||
public required BaseRotateUserAccountKeysData BaseData { get; set; }
|
||||
|
||||
public void ValidateForUser(User user)
|
||||
{
|
||||
try
|
||||
{
|
||||
MasterPasswordAuthenticationData.ValidateSaltUnchangedForUser(user);
|
||||
MasterPasswordAuthenticationData.Kdf.ValidateUnchangedForUser(user);
|
||||
MasterPasswordUnlockData.ValidateSaltUnchangedForUser(user);
|
||||
MasterPasswordUnlockData.Kdf.ValidateUnchangedForUser(user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,7 +175,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_NotLoggedIn_Unauthorized(
|
||||
RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
var response = await _client.PostAsJsonAsync("/accounts/key-management/rotate-user-account-keys", request);
|
||||
@@ -185,7 +185,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_Success(RotateUserAccountKeysAndDataRequestModel request)
|
||||
{
|
||||
var user = await SetupUserForKeyRotationAsync();
|
||||
SetupRotateUserAccountUnlockData(request, user);
|
||||
|
||||
@@ -17,6 +17,7 @@ using Bit.Core.KeyManagement.Models.Api.Request;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -86,15 +87,15 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_UserCryptoV1_Success(
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_UserCryptoV1_Success(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
|
||||
@@ -111,26 +112,31 @@ public class AccountsKeyManagementControllerTests
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
|
||||
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<PasswordChangeAndRotateUserAccountKeysData>(d =>
|
||||
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
|
||||
|
||||
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordUnlockData.Kdf.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.Kdf.Iterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.Kdf.Memory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.Kdf.Parallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Salt == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordUnlockData.MasterKeyWrappedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Iterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Memory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Parallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordAuthenticationData.Salt == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
&& d.BaseData.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.BaseData.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_UserCryptoV2_Success_Async(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = new SignatureKeyPairRequestModel
|
||||
@@ -140,9 +146,9 @@ public class AccountsKeyManagementControllerTests
|
||||
VerifyingKey = "verifyingKey"
|
||||
};
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data);
|
||||
|
||||
await sutProvider.GetDependency<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>>().Received(1)
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountUnlockData.EmergencyAccessUnlockData));
|
||||
@@ -159,53 +165,58 @@ public class AccountsKeyManagementControllerTests
|
||||
.ValidateAsync(Arg.Any<User>(), Arg.Is(data.AccountData.Sends));
|
||||
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<PasswordChangeAndRotateUserAccountKeysData>(d =>
|
||||
d.OldMasterKeyAuthenticationHash == data.OldMasterKeyAuthenticationHash
|
||||
|
||||
&& d.MasterPasswordUnlockData.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.KdfIterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.KdfMemory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.KdfParallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Email == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordUnlockData.Kdf.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordUnlockData.Kdf.Iterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordUnlockData.Kdf.Memory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordUnlockData.Kdf.Parallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordUnlockData.Salt == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordUnlockData.MasterKeyWrappedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
|
||||
&& d.MasterPasswordUnlockData.MasterKeyAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
&& d.MasterPasswordUnlockData.MasterKeyEncryptedUserKey == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyEncryptedUserKey
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.KdfType == data.AccountUnlockData.MasterPasswordUnlockData.KdfType
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Iterations == data.AccountUnlockData.MasterPasswordUnlockData.KdfIterations
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Memory == data.AccountUnlockData.MasterPasswordUnlockData.KdfMemory
|
||||
&& d.MasterPasswordAuthenticationData.Kdf.Parallelism == data.AccountUnlockData.MasterPasswordUnlockData.KdfParallelism
|
||||
&& d.MasterPasswordAuthenticationData.Salt == data.AccountUnlockData.MasterPasswordUnlockData.Email
|
||||
&& d.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash == data.AccountUnlockData.MasterPasswordUnlockData.MasterKeyAuthenticationHash
|
||||
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
&& d.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519
|
||||
&& d.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey
|
||||
&& d.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey
|
||||
&& d.BaseData.AccountKeys!.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.WrappedPrivateKey
|
||||
&& d.BaseData.AccountKeys!.PublicKeyEncryptionKeyPairData.PublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.PublicKey
|
||||
&& d.BaseData.AccountKeys!.PublicKeyEncryptionKeyPairData.SignedPublicKey == data.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey
|
||||
&& d.BaseData.AccountKeys!.SignatureKeyPairData!.SignatureAlgorithm == Core.KeyManagement.Enums.SignatureAlgorithm.Ed25519
|
||||
&& d.BaseData.AccountKeys!.SignatureKeyPairData.WrappedSigningKey == data.AccountKeys.SignatureKeyPair!.WrappedSigningKey
|
||||
&& d.BaseData.AccountKeys!.SignatureKeyPairData.VerifyingKey == data.AccountKeys.SignatureKeyPair!.VerifyingKey
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserKeyNoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_NoUser_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
User? user = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.RotateUserAccountKeysAsync(data));
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserKeyWrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WrongData_Throws(SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data, User user, IdentityErrorDescriber _identityErrorDescriber)
|
||||
{
|
||||
data.AccountKeys.SignatureKeyPair = null;
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>().PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()));
|
||||
try
|
||||
{
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data);
|
||||
Assert.Fail("Should have thrown");
|
||||
}
|
||||
catch (BadRequestException ex)
|
||||
@@ -216,7 +227,7 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_WithV2UpgradeToken_PassesTokenToCommand(
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithV2UpgradeToken_PassesTokenToCommand(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data,
|
||||
User user)
|
||||
@@ -231,23 +242,23 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>()
|
||||
.RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
d.V2UpgradeToken != null &&
|
||||
d.V2UpgradeToken.WrappedUserKey1 == _mockEncryptedType7String &&
|
||||
d.V2UpgradeToken.WrappedUserKey2 == _mockEncryptedType2String));
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<PasswordChangeAndRotateUserAccountKeysData>(d =>
|
||||
d.BaseData.V2UpgradeToken != null &&
|
||||
d.BaseData.V2UpgradeToken.WrappedUserKey1 == _mockEncryptedType7String &&
|
||||
d.BaseData.V2UpgradeToken.WrappedUserKey2 == _mockEncryptedType2String));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task RotateUserAccountKeys_WithoutV2UpgradeToken_PassesNullToCommand(
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithoutV2UpgradeToken_PassesNullToCommand(
|
||||
SutProvider<AccountsKeyManagementController> sutProvider,
|
||||
RotateUserAccountKeysAndDataRequestModel data,
|
||||
User user)
|
||||
@@ -258,16 +269,16 @@ public class AccountsKeyManagementControllerTests
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
|
||||
sutProvider.GetDependency<IRotateUserAccountKeysCommand>()
|
||||
.RotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<RotateUserAccountKeysData>())
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Any<User>(), Arg.Any<PasswordChangeAndRotateUserAccountKeysData>())
|
||||
.Returns(IdentityResult.Success);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(data);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(data);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IRotateUserAccountKeysCommand>().Received(1)
|
||||
.RotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<RotateUserAccountKeysData>(d =>
|
||||
d.V2UpgradeToken == null));
|
||||
.PasswordChangeAndRotateUserAccountKeysAsync(Arg.Is(user), Arg.Is<PasswordChangeAndRotateUserAccountKeysData>(d =>
|
||||
d.BaseData.V2UpgradeToken == null));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.KeyManagement.Models.Requests;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.KeyManagement.UserKey.Models.Data;
|
||||
|
||||
public class PasswordChangeAndRotateUserAccountKeysDataTests
|
||||
{
|
||||
private const string _mockOldMasterKeyAuthenticationHash = "hash";
|
||||
private const string _mockMasterPasswordAuthenticationHash = "mockAuthenticationHash";
|
||||
private const string _mockMasterKeyWrappedUserKey = "mockMasterKeyWrappedUserKey";
|
||||
|
||||
private static KdfSettings ValidKdf
|
||||
{
|
||||
get => new() { KdfType = KdfType.PBKDF2_SHA256, Iterations = 600000, Memory = null, Parallelism = null };
|
||||
}
|
||||
|
||||
private static void SetupValidUser(User user)
|
||||
{
|
||||
user.Email = "test@example.com";
|
||||
user.MasterPasswordSalt = null;
|
||||
user.Kdf = ValidKdf.KdfType;
|
||||
user.KdfIterations = ValidKdf.Iterations;
|
||||
user.KdfMemory = ValidKdf.Memory;
|
||||
user.KdfParallelism = ValidKdf.Parallelism;
|
||||
}
|
||||
|
||||
private static PasswordChangeAndRotateUserAccountKeysData CreateValidModel(string salt, KdfSettings kdf) =>
|
||||
new()
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = _mockOldMasterKeyAuthenticationHash,
|
||||
MasterPasswordAuthenticationData =
|
||||
new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterPasswordAuthenticationHash = _mockMasterPasswordAuthenticationHash,
|
||||
Salt = salt
|
||||
},
|
||||
MasterPasswordUnlockData =
|
||||
new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = kdf,
|
||||
MasterKeyWrappedUserKey = _mockMasterKeyWrappedUserKey,
|
||||
Salt = salt
|
||||
},
|
||||
BaseData = new BaseRotateUserAccountKeysData
|
||||
{
|
||||
AccountKeys = new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData =
|
||||
new PublicKeyEncryptionKeyPairData("mockWrappedPrivateKey", "mockPublicKey")
|
||||
},
|
||||
EmergencyAccesses = [],
|
||||
OrganizationUsers = [],
|
||||
WebAuthnKeys = [],
|
||||
DeviceKeys = [],
|
||||
Ciphers = [],
|
||||
Folders = [],
|
||||
Sends = []
|
||||
}
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ValidateForUser_ValidData_DoesNotThrow(User user)
|
||||
{
|
||||
SetupValidUser(user);
|
||||
var model = CreateValidModel(user.Email, ValidKdf);
|
||||
|
||||
model.ValidateForUser(user);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ValidateForUser_AuthenticationSaltMismatch_ThrowsInvalidOperationException(User user)
|
||||
{
|
||||
SetupValidUser(user);
|
||||
var validModel = CreateValidModel(user.Email, ValidKdf);
|
||||
|
||||
var model = new PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = validModel.OldMasterKeyAuthenticationHash,
|
||||
MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = ValidKdf,
|
||||
MasterPasswordAuthenticationHash = _mockMasterPasswordAuthenticationHash,
|
||||
Salt = "wrong@example.com"
|
||||
},
|
||||
MasterPasswordUnlockData = validModel.MasterPasswordUnlockData,
|
||||
BaseData = validModel.BaseData
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => model.ValidateForUser(user));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ValidateForUser_UnlockSaltMismatch_ThrowsInvalidOperationException(User user)
|
||||
{
|
||||
SetupValidUser(user);
|
||||
var validModel = CreateValidModel(user.Email, ValidKdf);
|
||||
|
||||
var model = new PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = validModel.OldMasterKeyAuthenticationHash,
|
||||
MasterPasswordAuthenticationData = validModel.MasterPasswordAuthenticationData,
|
||||
MasterPasswordUnlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = ValidKdf,
|
||||
MasterKeyWrappedUserKey = _mockMasterKeyWrappedUserKey,
|
||||
Salt = "wrong@example.com"
|
||||
},
|
||||
BaseData = validModel.BaseData
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => model.ValidateForUser(user));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ValidateForUser_AuthenticationKdfMismatch_ThrowsInvalidOperationException(User user)
|
||||
{
|
||||
SetupValidUser(user);
|
||||
var validModel = CreateValidModel(user.Email, ValidKdf);
|
||||
|
||||
var model = new PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = validModel.OldMasterKeyAuthenticationHash,
|
||||
MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Kdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = 3, Memory = 64, Parallelism = 4 },
|
||||
MasterPasswordAuthenticationHash =
|
||||
validModel.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash,
|
||||
Salt = validModel.MasterPasswordAuthenticationData.Salt
|
||||
},
|
||||
MasterPasswordUnlockData = validModel.MasterPasswordUnlockData,
|
||||
BaseData = CreateValidModel(user.Email, ValidKdf).BaseData
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => model.ValidateForUser(user));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void ValidateForUser_UnlockKdfMismatch_ThrowsInvalidOperationException(User user)
|
||||
{
|
||||
SetupValidUser(user);
|
||||
var validModel = CreateValidModel(user.Email, ValidKdf);
|
||||
|
||||
var model = new PasswordChangeAndRotateUserAccountKeysData
|
||||
{
|
||||
OldMasterKeyAuthenticationHash = validModel.OldMasterKeyAuthenticationHash,
|
||||
MasterPasswordAuthenticationData = validModel.MasterPasswordAuthenticationData,
|
||||
MasterPasswordUnlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = new KdfSettings
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = 3,
|
||||
Memory = 64,
|
||||
Parallelism = 4
|
||||
},
|
||||
MasterKeyWrappedUserKey = validModel.MasterPasswordUnlockData.MasterKeyWrappedUserKey,
|
||||
Salt = validModel.MasterPasswordUnlockData.Salt
|
||||
},
|
||||
BaseData = validModel.BaseData
|
||||
};
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => model.ValidateForUser(user));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
using Bit.Core.KeyManagement.UserKey;
|
||||
using Bit.Core.KeyManagement.UserKey.Implementations;
|
||||
using Bit.Core.KeyManagement.UserKey.Models.Data;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
@@ -27,322 +30,321 @@ public class RotateUserAccountKeysCommandTests
|
||||
"2.06CDSJjTZaigYHUuswIq5A==|trxgZl2RCkYrrmCvGE9WNA==|w5p05eI5wsaYeSyWtsAPvBX63vj798kIMxBTfSB0BQg=";
|
||||
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
|
||||
private static readonly string _mockEncryptedType7String2 = "7.Mi1iaXR3YXJkZW4tZGF0YQo=";
|
||||
private static readonly string _mockSalt = "salt@example.com";
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
user.Email = model.MasterPasswordUnlockData.Email;
|
||||
user.Email = model.MasterPasswordUnlockData.Salt;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
var result = await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
Assert.NotEqual(IdentityResult.Success, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||
RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(null, model));
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(null, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_EmailChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
model.MasterPasswordUnlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = model.MasterPasswordUnlockData.Kdf,
|
||||
Salt = user.Email + ".different-domain",
|
||||
MasterKeyWrappedUserKey = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey
|
||||
};
|
||||
|
||||
model.MasterPasswordUnlockData.Email = user.Email + ".different-domain";
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
model.MasterPasswordUnlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Kdf = new KdfSettings
|
||||
{
|
||||
KdfType = KdfType.PBKDF2_SHA256,
|
||||
Iterations = 600000,
|
||||
Memory = null,
|
||||
Parallelism = null
|
||||
},
|
||||
Salt = model.MasterPasswordUnlockData.Salt,
|
||||
MasterKeyWrappedUserKey = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey
|
||||
};
|
||||
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.PBKDF2_SHA256;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 600000;
|
||||
model.MasterPasswordUnlockData.KdfMemory = null;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = null;
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
model.BaseData.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model.BaseData);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
Assert.Equal(user.SecurityState, model.BaseData.AccountKeys.SecurityStateData!.SecurityState);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.RotateUserAccountKeysAsync(user, model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_UpgradeV1ToV2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user,
|
||||
RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
Assert.Equal(IdentityResult.Success, result);
|
||||
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_PublicKeyChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey = "new-public";
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V2User_PrivateKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V1User_PrivateKeyNotAesCbcHmac_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType7String;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided account private key was not wrapped with AES-256-CBC-HMAC", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V1_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
Assert.Empty(saveEncryptedDataActions);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V2_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions);
|
||||
Assert.NotEmpty(saveEncryptedDataActions);
|
||||
Assert.Equal(user.SecurityState, model.AccountKeys.SecurityStateData!.SecurityState);
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V2User_VerifyingKeyMismatch_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided verifying key does not match the user's current verifying key.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V2User_SignedPublicKeyNullOrEmpty_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_V2User_WrappedSigningKeyNotXChaCha20_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = _mockEncryptedType2String;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided signing key data is not wrapped with XChaCha20-Poly1305.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeys_UpgradeToV2_InvalidVerifyingKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided signature key pair data does not contain a valid verifying key.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_IncorrectlyWrappedPrivateKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSignedPublicKey_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed public key provided, but the user already has a signature key pair.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_UpgradeToV2_NoSecurityState_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SecurityStateData = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("No signed security state provider for V2 user", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_RotateV2_NoSignatureKeyPair_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
model.AccountKeys.SignatureKeyPairData = null;
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Signature key pair data is required for V2 encryption.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_EmptyString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Invalid encryption type string.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateAccountKeysAsync_GetEncryptionType_InvalidString_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx";
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
var ex = await Assert.ThrowsAsync<ArgumentException>(async () => await sutProvider.Sut.UpdateAccountKeysAsync(model, user, saveEncryptedDataActions));
|
||||
Assert.Equal("Invalid encryption type string.", ex.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task UpdateUserData_RevisionDateChanged_Success(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
var oldDate = new DateTime(2017, 1, 1);
|
||||
|
||||
@@ -358,7 +360,7 @@ public class RotateUserAccountKeysCommandTests
|
||||
send.RevisionDate = oldDate;
|
||||
model.Sends = [send];
|
||||
|
||||
var saveEncryptedDataActions = new List<Core.KeyManagement.UserKey.UpdateEncryptedDataForKeyRotation>();
|
||||
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>();
|
||||
|
||||
sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
foreach (var dataAction in saveEncryptedDataActions)
|
||||
@@ -398,17 +400,17 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_WithV2UpgradeToken_NoLogout(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithV2UpgradeToken_NoLogout(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
// Arrange
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
model.V2UpgradeToken = new V2UpgradeTokenData
|
||||
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData
|
||||
{
|
||||
WrappedUserKey1 = _mockEncryptedType7String,
|
||||
WrappedUserKey2 = _mockEncryptedType2String
|
||||
@@ -418,7 +420,7 @@ public class RotateUserAccountKeysCommandTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
// Assert - Security stamp is not updated
|
||||
Assert.Equal(originalSecurityStamp, user.SecurityStamp);
|
||||
@@ -434,24 +436,24 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_WithoutV2UpgradeToken_Logout(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithoutV2UpgradeToken_Logout(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
// Arrange
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
user.V2UpgradeToken = null;
|
||||
model.V2UpgradeToken = null;
|
||||
model.BaseData.V2UpgradeToken = null;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
// Assert - Security stamp is updated
|
||||
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp);
|
||||
@@ -465,14 +467,14 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_WithExistingToken_WithoutNewToken_ClearsStaleToken(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithExistingToken_WithoutNewToken_ClearsStaleToken(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
// Arrange
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
@@ -485,13 +487,13 @@ public class RotateUserAccountKeysCommandTests
|
||||
user.V2UpgradeToken = staleToken.ToJson();
|
||||
|
||||
// Model does NOT provide new token
|
||||
model.V2UpgradeToken = null;
|
||||
model.BaseData.V2UpgradeToken = null;
|
||||
|
||||
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash)
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
// Assert - Stale token explicitly cleared
|
||||
Assert.Null(user.V2UpgradeToken);
|
||||
@@ -505,14 +507,14 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_WithExistingToken_WithNewToken_UpdatesToken(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_WithExistingToken_WithNewToken_UpdatesToken(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
// Arrange
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV1ExistingUser(user, signatureRepository);
|
||||
SetV1ModelUser(model);
|
||||
SetV1ModelUser(model.BaseData);
|
||||
|
||||
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
@@ -525,7 +527,7 @@ public class RotateUserAccountKeysCommandTests
|
||||
user.V2UpgradeToken = oldToken.ToJson();
|
||||
|
||||
// Model provides NEW token
|
||||
model.V2UpgradeToken = new V2UpgradeTokenData
|
||||
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData
|
||||
{
|
||||
WrappedUserKey1 = _mockEncryptedType7String2,
|
||||
WrappedUserKey2 = _mockEncryptedType2String2
|
||||
@@ -535,7 +537,7 @@ public class RotateUserAccountKeysCommandTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
// Assert - Security stamp is not updated (no logout)
|
||||
Assert.Equal(originalSecurityStamp, user.SecurityStamp);
|
||||
@@ -555,17 +557,17 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RotateUserAccountKeysAsync_V2User_WithV2UpgradeToken_IgnoresTokenAndLogsOut(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, RotateUserAccountKeysData model)
|
||||
public async Task PasswordChangeAndRotateUserAccountKeysAsync_V2User_WithV2UpgradeToken_IgnoresTokenAndLogsOut(
|
||||
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
// Arrange
|
||||
SetTestKdfAndSaltForUserAndModel(user, model);
|
||||
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>();
|
||||
SetV2ExistingUser(user, signatureRepository);
|
||||
SetV2ModelUser(model);
|
||||
SetV2ModelUser(model.BaseData);
|
||||
|
||||
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
model.V2UpgradeToken = new V2UpgradeTokenData
|
||||
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData
|
||||
{
|
||||
WrappedUserKey1 = _mockEncryptedType7String,
|
||||
WrappedUserKey2 = _mockEncryptedType2String
|
||||
@@ -576,7 +578,7 @@ public class RotateUserAccountKeysCommandTests
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.RotateUserAccountKeysAsync(user, model);
|
||||
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model);
|
||||
|
||||
// Assert - Token is NOT stored (V2 users don't need upgrade token)
|
||||
Assert.Null(user.V2UpgradeToken);
|
||||
@@ -590,18 +592,36 @@ public class RotateUserAccountKeysCommandTests
|
||||
}
|
||||
|
||||
// Helper functions to set valid test parameters that match each other to the model and user.
|
||||
private static void SetTestKdfAndSaltForUserAndModel(User user, RotateUserAccountKeysData model)
|
||||
private static void SetTestKdfAndSaltForUserAndModel(User user, PasswordChangeAndRotateUserAccountKeysData model)
|
||||
{
|
||||
user.Kdf = Enums.KdfType.Argon2id;
|
||||
user.KdfIterations = 3;
|
||||
user.KdfMemory = 64;
|
||||
user.KdfParallelism = 4;
|
||||
model.MasterPasswordUnlockData.KdfType = Enums.KdfType.Argon2id;
|
||||
model.MasterPasswordUnlockData.KdfIterations = 3;
|
||||
model.MasterPasswordUnlockData.KdfMemory = 64;
|
||||
model.MasterPasswordUnlockData.KdfParallelism = 4;
|
||||
var testKdf = new KdfSettings
|
||||
{
|
||||
KdfType = KdfType.Argon2id,
|
||||
Iterations = 3,
|
||||
Memory = 64,
|
||||
Parallelism = 4,
|
||||
};
|
||||
model.MasterPasswordUnlockData = new MasterPasswordUnlockData
|
||||
{
|
||||
Salt = _mockSalt,
|
||||
Kdf = testKdf,
|
||||
MasterKeyWrappedUserKey = _mockEncryptedType2String,
|
||||
};
|
||||
model.MasterPasswordAuthenticationData = new MasterPasswordAuthenticationData
|
||||
{
|
||||
Salt = _mockSalt,
|
||||
Kdf = testKdf,
|
||||
MasterPasswordAuthenticationHash = _mockEncryptedType2String,
|
||||
};
|
||||
|
||||
user.Kdf = testKdf.KdfType;
|
||||
user.KdfIterations = testKdf.Iterations;
|
||||
user.KdfMemory = testKdf.Memory;
|
||||
user.KdfParallelism = testKdf.Parallelism;
|
||||
|
||||
// The email is the salt for the KDF and is validated currently.
|
||||
user.Email = model.MasterPasswordUnlockData.Email;
|
||||
user.Email = model.MasterPasswordUnlockData.Salt;
|
||||
user.MasterPasswordSalt = null;
|
||||
}
|
||||
|
||||
private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
|
||||
@@ -620,14 +640,14 @@ public class RotateUserAccountKeysCommandTests
|
||||
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, "verifying-key"));
|
||||
}
|
||||
|
||||
private static void SetV1ModelUser(RotateUserAccountKeysData model)
|
||||
private static void SetV1ModelUser(BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(_mockEncryptedType2String, "public", null);
|
||||
model.AccountKeys.SignatureKeyPairData = null;
|
||||
model.AccountKeys.SecurityStateData = null;
|
||||
}
|
||||
|
||||
private static void SetV2ModelUser(RotateUserAccountKeysData model)
|
||||
private static void SetV2ModelUser(BaseRotateUserAccountKeysData model)
|
||||
{
|
||||
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(_mockEncryptedType7String, "public", "signed-public");
|
||||
model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, "verifying-key");
|
||||
|
||||
@@ -468,7 +468,7 @@ public class ProfileServiceTests
|
||||
/// this service should expose the stamp as invalid.
|
||||
/// See also examples for stamp invalidation (non-exhaustive):
|
||||
/// </summary>
|
||||
/// <seealso cref="Bit.Core.KeyManagement.UserKey.Implementations.RotateUserAccountKeysCommand.RotateUserAccountKeysAsync"/>
|
||||
/// <seealso cref="Bit.Core.KeyManagement.UserKey.Implementations.RotateUserAccountKeysCommand.PasswordChangeAndRotateUserAccountKeys"/>
|
||||
/// <seealso cref="Bit.Core.Services.UserService.ChangePasswordAsync"/>
|
||||
/// <seealso cref="Bit.Core.Services.UserService.UpdatePasswordHash"/>
|
||||
[Theory]
|
||||
|
||||
Reference in New Issue
Block a user