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.
382 lines
15 KiB
382 lines
15 KiB
#nullable enable |
|
|
|
using Bit.Core.Entities; |
|
using Bit.Core.Enums; |
|
using Bit.Core.Exceptions; |
|
using Bit.Core.KeyManagement.Kdf.Implementations; |
|
using Bit.Core.KeyManagement.Models.Data; |
|
using Bit.Core.Platform.Push; |
|
using Bit.Core.Repositories; |
|
using Bit.Core.Services; |
|
using Bit.Test.Common.AutoFixture; |
|
using Bit.Test.Common.AutoFixture.Attributes; |
|
using Microsoft.AspNetCore.Identity; |
|
using NSubstitute; |
|
using Xunit; |
|
|
|
namespace Bit.Core.Test.KeyManagement.Kdf; |
|
|
|
[SutProviderCustomize] |
|
public class ChangeKdfCommandTests |
|
{ |
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_ChangesKdfAsync(SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(IdentityResult.Success)); |
|
|
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "newMasterPassword", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
|
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); |
|
|
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u => |
|
u.Id == user.Id |
|
&& u.Kdf == Enums.KdfType.Argon2id |
|
&& u.KdfIterations == 4 |
|
&& u.KdfMemory == 512 |
|
&& u.KdfParallelism == 4 |
|
)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_UserIsNull_ThrowsArgumentNullException(SutProvider<ChangeKdfCommand> sutProvider) |
|
{ |
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "newMasterPassword", |
|
Salt = "salt" |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", |
|
Salt = "salt" |
|
}; |
|
|
|
await Assert.ThrowsAsync<ArgumentNullException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(null!, "masterPassword", authenticationData, unlockData)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, |
|
User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(false)); |
|
|
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "newMasterPassword", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
|
|
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); |
|
Assert.False(result.Succeeded); |
|
Assert.Contains(result.Errors, e => e.Code == "PasswordMismatch"); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task |
|
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut( |
|
SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
var constantKdf = new KdfSettings |
|
{ |
|
KdfType = Enums.KdfType.Argon2id, |
|
Iterations = 5, |
|
Memory = 1024, |
|
Parallelism = 4 |
|
}; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = constantKdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = constantKdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
sutProvider.GetDependency<IUserService>() |
|
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>()) |
|
.Returns(Task.FromResult(IdentityResult.Success)); |
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(false); |
|
|
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); |
|
|
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u => |
|
u.Id == user.Id |
|
&& u.Kdf == constantKdf.KdfType |
|
&& u.KdfIterations == constantKdf.Iterations |
|
&& u.KdfMemory == constantKdf.Memory |
|
&& u.KdfParallelism == constantKdf.Parallelism |
|
&& u.Key == "new-wrapped-key" |
|
)); |
|
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user, |
|
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: true); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushLogOutAsync(user.Id); |
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task |
|
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut( |
|
SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
var constantKdf = new KdfSettings |
|
{ |
|
KdfType = Enums.KdfType.Argon2id, |
|
Iterations = 5, |
|
Memory = 1024, |
|
Parallelism = 4 |
|
}; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = constantKdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = constantKdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
sutProvider.GetDependency<IUserService>() |
|
.UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<bool>()) |
|
.Returns(Task.FromResult(IdentityResult.Success)); |
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(Arg.Any<string>()).Returns(true); |
|
|
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); |
|
|
|
await sutProvider.GetDependency<IUserRepository>().Received(1).ReplaceAsync(Arg.Is<User>(u => |
|
u.Id == user.Id |
|
&& u.Kdf == constantKdf.KdfType |
|
&& u.KdfIterations == constantKdf.Iterations |
|
&& u.KdfMemory == constantKdf.Memory |
|
&& u.KdfParallelism == constantKdf.Parallelism |
|
&& u.Key == "new-wrapped-key" |
|
)); |
|
await sutProvider.GetDependency<IUserService>().Received(1).UpdatePasswordHash(user, |
|
authenticationData.MasterPasswordAuthenticationHash, validatePassword: true, refreshStamp: false); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1) |
|
.PushLogOutAsync(user.Id, false, PushNotificationLogOutReason.KdfChange); |
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncSettingsAsync(user.Id); |
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException( |
|
SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
|
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = new KdfSettings |
|
{ |
|
KdfType = Enums.KdfType.Argon2id, |
|
Iterations = 4, |
|
Memory = 512, |
|
Parallelism = 4 |
|
}, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = new KdfSettings { KdfType = Enums.KdfType.PBKDF2_SHA256, Iterations = 100000 }, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, |
|
KdfSettings kdf) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
|
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = "different-salt" |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_UnlockDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, |
|
KdfSettings kdf) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
|
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = "different-salt" |
|
}; |
|
await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_UpdatePasswordHashFails_ReturnsFailure(SutProvider<ChangeKdfCommand> sutProvider, |
|
User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
var failedResult = IdentityResult.Failed(new IdentityError { Code = "TestFail", Description = "Test fail" }); |
|
sutProvider.GetDependency<IUserService>().UpdatePasswordHash(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(failedResult)); |
|
|
|
var kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 }; |
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = kdf, |
|
MasterPasswordAuthenticationHash = "newMasterPassword", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = kdf, |
|
MasterKeyWrappedUserKey = "masterKeyWrappedUserKey", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
|
|
var result = await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData); |
|
|
|
Assert.False(result.Succeeded); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException( |
|
SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
|
|
// Create invalid KDF settings (iterations too low for PBKDF2) |
|
var invalidKdf = new KdfSettings |
|
{ |
|
KdfType = Enums.KdfType.PBKDF2_SHA256, |
|
Iterations = 1000, // This is below the minimum of 600,000 |
|
Memory = null, |
|
Parallelism = null |
|
}; |
|
|
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = invalidKdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = invalidKdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
|
|
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); |
|
|
|
Assert.Equal("KDF settings are invalid.", exception.Message); |
|
} |
|
|
|
[Theory] |
|
[BitAutoData] |
|
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException( |
|
SutProvider<ChangeKdfCommand> sutProvider, User user) |
|
{ |
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()) |
|
.Returns(Task.FromResult(true)); |
|
|
|
// Create invalid Argon2 KDF settings (memory too high) |
|
var invalidKdf = new KdfSettings |
|
{ |
|
KdfType = Enums.KdfType.Argon2id, |
|
Iterations = 3, // Valid |
|
Memory = 2048, // This is above the maximum of 1024 |
|
Parallelism = 4 // Valid |
|
}; |
|
|
|
var authenticationData = new MasterPasswordAuthenticationData |
|
{ |
|
Kdf = invalidKdf, |
|
MasterPasswordAuthenticationHash = "new-auth-hash", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
var unlockData = new MasterPasswordUnlockData |
|
{ |
|
Kdf = invalidKdf, |
|
MasterKeyWrappedUserKey = "new-wrapped-key", |
|
Salt = user.GetMasterPasswordSalt() |
|
}; |
|
|
|
var exception = await Assert.ThrowsAsync<BadRequestException>(async () => |
|
await sutProvider.Sut.ChangeKdfAsync(user, "masterPassword", authenticationData, unlockData)); |
|
|
|
Assert.Equal("KDF settings are invalid.", exception.Message); |
|
} |
|
}
|
|
|