1
0
mirror of https://github.com/bitwarden/server synced 2026-01-10 12:33:49 +00:00

[PM-23229] Add extra validation to kdf changes + authentication data + unlock data (#6121)

* Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response

* Implement support for authentication data and unlock data in kdf change

* Extract to kdf command and add tests

* Fix namespace

* Delete empty file

* Fix build

* Clean up tests

* Fix tests

* Add comments

* Cleanup

* Cleanup

* Cleanup

* Clean-up and fix build

* Address feedback; force new parameters on KDF change request

* Clean-up and add tests

* Re-add logger

* Update logger to interface

* Clean up, remove Kdf Request Model

* Remove kdf request model tests

* Fix types in test

* Address feedback to rename request model and re-add tests

* Fix namespace

* Move comments

* Rename InnerKdfRequestModel to KdfRequestModel

---------

Co-authored-by: Maciej Zieniuk <mzieniuk@bitwarden.com>
This commit is contained in:
Bernd Schoolmann
2025-09-24 05:10:46 +09:00
committed by GitHub
parent 744f11733d
commit ff092a031e
25 changed files with 729 additions and 173 deletions

View File

@@ -0,0 +1,15 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;
namespace Bit.Core.KeyManagement.Kdf;
/// <summary>
/// Command to change the Key Derivation Function (KDF) settings for a user. This includes
/// changing the masterpassword authentication hash, and the masterkey encrypted userkey.
/// The salt must not change during the KDF change.
/// </summary>
public interface IChangeKdfCommand
{
public Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData);
}

View File

@@ -0,0 +1,94 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
namespace Bit.Core.KeyManagement.Kdf.Implementations;
/// <inheritdoc />
public class ChangeKdfCommand : IChangeKdfCommand
{
private readonly IUserService _userService;
private readonly IPushNotificationService _pushService;
private readonly IUserRepository _userRepository;
private readonly IdentityErrorDescriber _identityErrorDescriber;
private readonly ILogger<ChangeKdfCommand> _logger;
public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, IUserRepository userRepository, IdentityErrorDescriber describer, ILogger<ChangeKdfCommand> logger)
{
_userService = userService;
_pushService = pushService;
_userRepository = userRepository;
_identityErrorDescriber = describer;
_logger = logger;
}
public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswordAuthenticationHash, MasterPasswordAuthenticationData authenticationData, MasterPasswordUnlockData unlockData)
{
ArgumentNullException.ThrowIfNull(user);
if (!await _userService.CheckPasswordAsync(user, masterPasswordAuthenticationHash))
{
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
// Validate to prevent user account from becoming un-decryptable from invalid parameters
//
// Prevent a de-synced salt value from creating an un-decryptable unlock method
authenticationData.ValidateSaltUnchangedForUser(user);
unlockData.ValidateSaltUnchangedForUser(user);
// Currently KDF settings are not saved separately for authentication and unlock and must therefore be equal
if (!authenticationData.Kdf.Equals(unlockData.Kdf))
{
throw new BadRequestException("KDF settings must be equal for authentication and unlock.");
}
var validationErrors = KdfSettingsValidator.Validate(unlockData.Kdf);
if (validationErrors.Any())
{
throw new BadRequestException("KDF settings are invalid.");
}
// Update the user with the new KDF settings
// This updates the authentication data and unlock data for the user separately. Currently these still
// use shared values for KDF settings and salt.
// The authentication hash, and the unlock data each are dependent on:
// - The master password (entered by the user every time)
// - The KDF settings (iterations, memory, parallelism)
// - The salt
// These combinations - (password, authentication hash, KDF settings, salt) and (password, unlock data, KDF settings, salt)
// must remain consistent to unlock correctly.
// Authentication
// Note: This mutates the user but does not yet save it to DB. That is done atomically, later.
// This entire operation MUST be atomic to prevent a user from being locked out of their account.
// Salt is ensured to be the same as unlock data, and the value stored in the account and not updated.
// KDF is ensured to be the same as unlock data above and updated below.
var result = await _userService.UpdatePasswordHash(user, authenticationData.MasterPasswordAuthenticationHash);
if (!result.Succeeded)
{
_logger.LogWarning("Change KDF failed for user {userId}.", user.Id);
return result;
}
// Salt is ensured to be the same as authentication data, and the value stored in the account, and is not updated.
// Kdf - These will be seperated in the future, but for now are ensured to be the same as authentication data above.
user.Key = unlockData.MasterKeyWrappedUserKey;
user.Kdf = unlockData.Kdf.KdfType;
user.KdfIterations = unlockData.Kdf.Iterations;
user.KdfMemory = unlockData.Kdf.Memory;
user.KdfParallelism = unlockData.Kdf.Parallelism;
var now = DateTime.UtcNow;
user.RevisionDate = user.AccountRevisionDate = now;
user.LastKdfChangeDate = now;
await _userRepository.ReplaceAsync(user);
await _pushService.PushLogOutAsync(user.Id);
return IdentityResult.Success;
}
}