[PM-33162] Refactor user key rotation (#7201)

* Refactor user key rotation to use base data composition

* Update tests
This commit is contained in:
Thomas Avery
2026-03-20 10:31:25 -05:00
committed by GitHub
parent 212a0609c0
commit 53907c2f14
14 changed files with 585 additions and 369 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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.");
}
}
}

View File

@@ -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);

View File

@@ -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]

View File

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

View File

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

View File

@@ -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");

View File

@@ -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]