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:
15
src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs
Normal file
15
src/Core/KeyManagement/Kdf/IChangeKdfCommand.cs
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user