1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 21:23:39 +00:00

[PM-23249] Prevent log-out when changing KDF settings (#6349)

* Prevent log-out when changing KDF settings with feature flag.

* validate salt unchanged for user to throw bad request (400), not internal server error (500)

* change kdf integration tests

* failing tests

* iuncorrect tests wording

* conditional logout

* log out reason as enum

* explicit naming
This commit is contained in:
Maciej Zieniuk
2025-10-21 19:03:25 +02:00
committed by GitHub
parent 8d52ae869c
commit 6324f692b8
18 changed files with 675 additions and 115 deletions

View File

@@ -1,9 +1,11 @@
#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;
@@ -21,16 +23,12 @@ public class ChangeKdfCommandTests
[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));
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 kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -59,13 +57,7 @@ public class ChangeKdfCommandTests
[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 kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -85,17 +77,13 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WrongPassword_ReturnsPasswordMismatch(SutProvider<ChangeKdfCommand> sutProvider, User user)
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));
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 kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -116,7 +104,9 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_WithAuthenticationAndUnlockData_UpdatesUserCorrectly(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOff_UpdatesUserCorrectlyAndLogsOut(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
var constantKdf = new KdfSettings
{
@@ -137,8 +127,12 @@ public class ChangeKdfCommandTests
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>()).Returns(Task.FromResult(IdentityResult.Success));
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);
@@ -150,17 +144,79 @@ public class ChangeKdfCommandTests
&& 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_KdfNotEqualBetweenAuthAndUnlock_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
public async Task
ChangeKdfAsync_WithAuthenticationAndUnlockDataAndNoLogoutOnKdfChangeFeatureFlagOn_UpdatesUserCorrectlyAndDoesNotLogOut(
SutProvider<ChangeKdfCommand> sutProvider, User user)
{
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>()).Returns(Task.FromResult(true));
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 },
Kdf = new KdfSettings
{
KdfType = Enums.KdfType.Argon2id,
Iterations = 4,
Memory = 512,
Parallelism = 4
},
MasterPasswordAuthenticationHash = "new-auth-hash",
Salt = user.GetMasterPasswordSalt()
};
@@ -176,9 +232,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_AuthDataSaltMismatch_Throws(SutProvider<ChangeKdfCommand> sutProvider, User user, KdfSettings kdf)
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));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
@@ -192,15 +250,17 @@ public class ChangeKdfCommandTests
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = user.GetMasterPasswordSalt()
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
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)
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));
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(Arg.Any<User>(), Arg.Any<string>())
.Returns(Task.FromResult(true));
var authenticationData = new MasterPasswordAuthenticationData
{
@@ -214,25 +274,22 @@ public class ChangeKdfCommandTests
MasterKeyWrappedUserKey = "new-wrapped-key",
Salt = "different-salt"
};
await Assert.ThrowsAsync<ArgumentException>(async () =>
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)
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));
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));
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 kdf = new KdfSettings { KdfType = Enums.KdfType.Argon2id, Iterations = 4, Memory = 512, Parallelism = 4 };
var authenticationData = new MasterPasswordAuthenticationData
{
Kdf = kdf,
@@ -253,9 +310,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidKdfSettings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
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));
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
@@ -287,9 +346,11 @@ public class ChangeKdfCommandTests
[Theory]
[BitAutoData]
public async Task ChangeKdfAsync_InvalidArgon2Settings_ThrowsBadRequestException(SutProvider<ChangeKdfCommand> sutProvider, User user)
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));
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
@@ -318,5 +379,4 @@ public class ChangeKdfCommandTests
Assert.Equal("KDF settings are invalid.", exception.Message);
}
}