You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
820 lines
39 KiB
820 lines
39 KiB
using Bit.Core.Entities; |
|
using Bit.Core.Enums; |
|
using Bit.Core.Exceptions; |
|
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.Repositories; |
|
using Bit.Core.Services; |
|
using Bit.Core.Tools.Entities; |
|
using Bit.Core.Tools.Repositories; |
|
using Bit.Core.Vault.Entities; |
|
using Bit.Core.Vault.Repositories; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using Microsoft.AspNetCore.Identity; |
|
using NSubstitute; |
|
using NSubstitute.ReturnsExtensions; |
|
using Xunit; |
|
|
|
namespace Bit.Core.Test.KeyManagement.UserKey; |
|
|
|
[SutProviderCustomize] |
|
public class RotateUserAccountKeysCommandTests |
|
{ |
|
private static readonly string _mockEncryptedType2String = |
|
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="; |
|
private static readonly string _mockEncryptedType2String2 = |
|
"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 PasswordChangeAndRotateUserAccountKeysAsync_WrongOldMasterPassword_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, |
|
PasswordChangeAndRotateUserAccountKeysData model) |
|
{ |
|
user.Email = model.MasterPasswordUnlockData.Salt; |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(false); |
|
|
|
var result = await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
Assert.NotEqual(IdentityResult.Success, result); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
public async Task PasswordChangeAndRotateUserAccountKeysAsync_UserIsNull_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, |
|
PasswordChangeAndRotateUserAccountKeysData model) |
|
{ |
|
await Assert.ThrowsAsync<ArgumentNullException>(async () => await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(null, model)); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
model.MasterPasswordUnlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = model.MasterPasswordUnlockData.Kdf, |
|
Salt = user.Email + ".different-domain", |
|
MasterKeyWrappedUserKey = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey |
|
}; |
|
|
|
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_KdfChange_Rejects(SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, |
|
PasswordChangeAndRotateUserAccountKeysData model) |
|
{ |
|
SetTestKdfAndSaltForUserAndModel(user, model); |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
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 |
|
}; |
|
|
|
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_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"; |
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV1ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType7String; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV1ModelUser(model); |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.SignatureKeyPairData.VerifyingKey = "different-verifying-key"; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.SignatureKeyPairData.WrappedSigningKey = _mockEncryptedType2String; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.SignatureKeyPairData.VerifyingKey = ""; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = _mockEncryptedType2String; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey = null; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.SecurityStateData = null; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model); |
|
model.AccountKeys.SignatureKeyPairData = null; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV1ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = ""; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV1ModelUser(model); |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey = "9.xxx"; |
|
|
|
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, BaseRotateUserAccountKeysData model) |
|
{ |
|
var oldDate = new DateTime(2017, 1, 1); |
|
|
|
var cipher = Substitute.For<Cipher>(); |
|
cipher.RevisionDate = oldDate; |
|
model.Ciphers = [cipher]; |
|
|
|
var folder = Substitute.For<Folder>(); |
|
folder.RevisionDate = oldDate; |
|
model.Folders = [folder]; |
|
|
|
var send = Substitute.For<Send>(); |
|
send.RevisionDate = oldDate; |
|
model.Sends = [send]; |
|
|
|
var saveEncryptedDataActions = new List<UpdateEncryptedDataForKeyRotation>(); |
|
|
|
sutProvider.Sut.UpdateUserData(model, user, saveEncryptedDataActions); |
|
foreach (var dataAction in saveEncryptedDataActions) |
|
{ |
|
await dataAction.Invoke(); |
|
} |
|
|
|
var updatedCiphers = sutProvider.GetDependency<ICipherRepository>() |
|
.ReceivedCalls() |
|
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? |
|
.GetArguments()[1] as IEnumerable<Cipher>; |
|
foreach (var updatedCipher in updatedCiphers!) |
|
{ |
|
var oldCipher = model.Ciphers.FirstOrDefault(c => c.Id == updatedCipher.Id); |
|
Assert.NotEqual(oldDate, updatedCipher.RevisionDate); |
|
} |
|
|
|
var updatedFolders = sutProvider.GetDependency<IFolderRepository>() |
|
.ReceivedCalls() |
|
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? |
|
.GetArguments()[1] as IEnumerable<Folder>; |
|
foreach (var updatedFolder in updatedFolders!) |
|
{ |
|
var oldFolder = model.Folders.FirstOrDefault(f => f.Id == updatedFolder.Id); |
|
Assert.NotEqual(oldDate, updatedFolder.RevisionDate); |
|
} |
|
|
|
var updatedSends = sutProvider.GetDependency<ISendRepository>() |
|
.ReceivedCalls() |
|
.FirstOrDefault(call => call.GetMethodInfo().Name == "UpdateForKeyRotation")? |
|
.GetArguments()[1] as IEnumerable<Send>; |
|
foreach (var updatedSend in updatedSends!) |
|
{ |
|
var oldSend = model.Sends.FirstOrDefault(s => s.Id == updatedSend.Id); |
|
Assert.NotEqual(oldDate, updatedSend.RevisionDate); |
|
} |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
|
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(true); |
|
|
|
// Act |
|
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
// Assert - Security stamp is not updated |
|
Assert.Equal(originalSecurityStamp, user.SecurityStamp); |
|
|
|
// Assert - Token is stored on user |
|
Assert.NotNull(user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType7String, user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType2String, user.V2UpgradeToken); |
|
|
|
// Assert - Push notification sent with KeyRotation reason |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id, false, Enums.PushNotificationLogOutReason.KeyRotation); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
user.V2UpgradeToken = null; |
|
model.BaseData.V2UpgradeToken = null; |
|
|
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(true); |
|
|
|
// Act |
|
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
// Assert - Security stamp is updated |
|
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); |
|
|
|
// Assert - Token is not stored on user |
|
Assert.Null(user.V2UpgradeToken); |
|
|
|
// Assert - Push notification sent without reason |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
|
|
// User has existing stale token from previous rotation |
|
var staleToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
user.V2UpgradeToken = staleToken.ToJson(); |
|
|
|
// Model does NOT provide new token |
|
model.BaseData.V2UpgradeToken = null; |
|
|
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(true); |
|
|
|
// Act |
|
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
// Assert - Stale token explicitly cleared |
|
Assert.Null(user.V2UpgradeToken); |
|
|
|
// Assert - Security stamp is updated (logout behavior) |
|
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); |
|
|
|
// Assert - Push notification sent without reason (standard logout) |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
|
|
// User has existing token from previous rotation |
|
var oldToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
user.V2UpgradeToken = oldToken.ToJson(); |
|
|
|
// Model provides NEW token |
|
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String2, |
|
WrappedUserKey2 = _mockEncryptedType2String2 |
|
}; |
|
|
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(true); |
|
|
|
// Act |
|
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
// Assert - Security stamp is not updated (no logout) |
|
Assert.Equal(originalSecurityStamp, user.SecurityStamp); |
|
|
|
// Assert - Token contains new wrapped keys |
|
Assert.NotNull(user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType7String2, user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType2String2, user.V2UpgradeToken); |
|
|
|
// Assert - Token does NOT contain old wrapped keys |
|
Assert.DoesNotContain(oldToken.WrappedUserKey1, user.V2UpgradeToken); |
|
Assert.DoesNotContain(oldToken.WrappedUserKey2, user.V2UpgradeToken); |
|
|
|
// Assert - Push notification sent with KeyRotation reason (no logout) |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id, false, Enums.PushNotificationLogOutReason.KeyRotation); |
|
} |
|
|
|
[Theory, BitAutoData] |
|
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.BaseData); |
|
|
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
|
|
sutProvider.GetDependency<IUserService>() |
|
.CheckPasswordAsync(user, model.OldMasterKeyAuthenticationHash) |
|
.Returns(true); |
|
|
|
// Act |
|
await sutProvider.Sut.PasswordChangeAndRotateUserAccountKeysAsync(user, model); |
|
|
|
// Assert - Token is NOT stored (V2 users don't need upgrade token) |
|
Assert.Null(user.V2UpgradeToken); |
|
|
|
// Assert - Security stamp IS updated (full logout) |
|
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); |
|
|
|
// Assert - Standard logout push, not KeyRotation reason |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_MissingUser_Throws( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, MasterPasswordRotateUserAccountKeysData model) => |
|
await Assert.ThrowsAsync<ArgumentNullException>(async () => |
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(null, model)); |
|
|
|
[Theory] |
|
[BitAutoData(true, true)] |
|
[BitAutoData(false, true)] |
|
[BitAutoData(true, false)] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_UserIsNotMasterPasswordUser_Throws(bool keyNull, |
|
bool masterPasswordNull, |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
if (keyNull) |
|
{ |
|
user.Key = null; |
|
} |
|
|
|
if (masterPasswordNull) |
|
{ |
|
user.MasterPassword = null; |
|
} |
|
|
|
await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_EmailChange_Throws( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
model = SetupTestData(model); |
|
SetupUserKdf(user, model); |
|
user.Email += ".different-domain"; |
|
|
|
await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_ChangedKdf_Throws( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
model = SetupTestData(model); |
|
SetupUserKdf(user, model); |
|
user.Kdf = KdfType.PBKDF2_SHA256; |
|
|
|
await Assert.ThrowsAsync<ArgumentException>(async () => |
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_V2User_Success( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
model = SetupTestData(model); |
|
SetupUserKdf(user, model); |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model.BaseData); |
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
|
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model); |
|
|
|
Assert.Equal(model.MasterPasswordUnlockData.MasterKeyWrappedUserKey, user.Key); |
|
await sutProvider.GetDependency<IUserRepository>().Received(1) |
|
.UpdateUserKeyAndEncryptedDataV2Async(user, Arg.Any<IEnumerable<UpdateEncryptedDataForKeyRotation>>()); |
|
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_V1User_WithNewV2UpgradeToken_PersistsToken( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
model = SetupTestData(model); |
|
SetupUserKdf(user, model); |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV1ExistingUser(user, signatureRepository); |
|
SetV1ModelUser(model.BaseData); |
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
|
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model); |
|
|
|
Assert.NotNull(user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType7String, user.V2UpgradeToken); |
|
Assert.Contains(_mockEncryptedType2String, user.V2UpgradeToken); |
|
Assert.Equal(originalSecurityStamp, user.SecurityStamp); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KeyRotation); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task MasterPasswordRotateUserAccountKeysAsync_V2User_WithV2UpgradeToken_IgnoresTokenAndLogsOut( |
|
SutProvider<RotateUserAccountKeysCommand> sutProvider, User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
model = SetupTestData(model); |
|
SetupUserKdf(user, model); |
|
var signatureRepository = sutProvider.GetDependency<IUserSignatureKeyPairRepository>(); |
|
SetV2ExistingUser(user, signatureRepository); |
|
SetV2ModelUser(model.BaseData); |
|
var originalSecurityStamp = user.SecurityStamp = Guid.NewGuid().ToString(); |
|
model.BaseData.V2UpgradeToken = new V2UpgradeTokenData |
|
{ |
|
WrappedUserKey1 = _mockEncryptedType7String, |
|
WrappedUserKey2 = _mockEncryptedType2String |
|
}; |
|
|
|
await sutProvider.Sut.MasterPasswordRotateUserAccountKeysAsync(user, model); |
|
|
|
Assert.Null(user.V2UpgradeToken); |
|
Assert.NotEqual(originalSecurityStamp, user.SecurityStamp); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id); |
|
} |
|
|
|
// Helper functions to set valid test parameters that match each other to the model and user. |
|
private static void SetTestKdfAndSaltForUserAndModel(User user, PasswordChangeAndRotateUserAccountKeysData model) |
|
{ |
|
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.Salt; |
|
user.MasterPasswordSalt = null; |
|
} |
|
|
|
private static void SetV1ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository) |
|
{ |
|
user.PrivateKey = _mockEncryptedType2String; |
|
user.PublicKey = "public"; |
|
user.SignedPublicKey = null; |
|
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).ReturnsNull(); |
|
} |
|
|
|
private static void SetV2ExistingUser(User user, IUserSignatureKeyPairRepository userSignatureKeyPairRepository) |
|
{ |
|
user.PrivateKey = _mockEncryptedType7String; |
|
user.PublicKey = "public"; |
|
user.SignedPublicKey = "signed-public"; |
|
userSignatureKeyPairRepository.GetByUserIdAsync(user.Id).Returns(new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, "verifying-key")); |
|
} |
|
|
|
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(BaseRotateUserAccountKeysData model) |
|
{ |
|
model.AccountKeys.PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData(_mockEncryptedType7String, "public", "signed-public"); |
|
model.AccountKeys.SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, _mockEncryptedType7String, "verifying-key"); |
|
model.AccountKeys.SecurityStateData = new SecurityStateData |
|
{ |
|
SecurityState = "abc", |
|
SecurityVersion = 2, |
|
}; |
|
} |
|
|
|
private static MasterPasswordRotateUserAccountKeysData SetupTestData(MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
var testKdf = new KdfSettings { KdfType = KdfType.Argon2id, Iterations = 3, Memory = 64, Parallelism = 4 }; |
|
model = new MasterPasswordRotateUserAccountKeysData |
|
{ |
|
MasterPasswordUnlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = testKdf, |
|
MasterKeyWrappedUserKey = _mockEncryptedType2String, |
|
Salt = _mockSalt |
|
}, |
|
BaseData = model.BaseData |
|
}; |
|
return model; |
|
} |
|
|
|
private static void SetupUserKdf(User user, MasterPasswordRotateUserAccountKeysData model) |
|
{ |
|
user.Kdf = model.MasterPasswordUnlockData.Kdf.KdfType; |
|
user.KdfIterations = model.MasterPasswordUnlockData.Kdf.Iterations; |
|
user.KdfMemory = model.MasterPasswordUnlockData.Kdf.Memory; |
|
user.KdfParallelism = model.MasterPasswordUnlockData.Kdf.Parallelism; |
|
// For now email and salt are coupled. This will be changed later to read from user.Salt. |
|
user.Email = model.MasterPasswordUnlockData.Salt; |
|
user.MasterPasswordSalt = null; |
|
user.Key = _mockEncryptedType2String; |
|
user.MasterPassword = "mockMasterPasswordAuthenticationHash"; |
|
} |
|
}
|
|
|