-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[PM-35395] MasterPasswordService Key Management Integration #7637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
eb942e8
dcdf3d1
d65c1de
e54f074
530983e
80733d1
4af1648
36bfc44
212a60b
c27b015
d6e86c6
8474b19
f564c17
c22cc13
d55dffe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,51 +1,36 @@ | ||
| ο»Ώusing System.ComponentModel.DataAnnotations; | ||
| using Bit.Core.Auth.Utilities; | ||
| using Bit.Core.KeyManagement.Models.Api.Request; | ||
| using Bit.Core.Utilities; | ||
|
|
||
| namespace Bit.Api.Auth.Models.Request.Accounts; | ||
|
|
||
| /// <summary> | ||
| /// Dual-shape request: validation accepts either the legacy | ||
| /// (<see cref="NewMasterPasswordHash"/>, <see cref="Key"/>) or new | ||
| /// (<see cref="AuthenticationData"/>, <see cref="UnlockData"/>) payload so the wire contract | ||
| /// can stabilize ahead of caller wiring. <c>PostKdf</c> currently honors only the new shape; | ||
| /// legacy-shape dispatch arrives with <c>ChangeKdfCommand</c>'s dual-path refactor. All legacy | ||
| /// fields are removed in PM-33141. | ||
| /// Request model for changing the KDF settings for a user's account. | ||
| /// </summary> | ||
| public class ChangeKdfRequestModel : IValidatableObject | ||
| { | ||
| // The current master password hash; proves user has access to the MP | ||
| [Required] | ||
| public required string MasterPasswordHash { get; set; } | ||
| [Obsolete("To be removed in PM-33141")] | ||
| [StringLength(300)] | ||
| public string? NewMasterPasswordHash { get; set; } | ||
| [Obsolete("To be removed in PM-33141")] | ||
| public string? Key { get; set; } | ||
|
|
||
| // Should be made required in PM-33141 | ||
| public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; } | ||
| // Should be made required in PM-33141 | ||
| public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; } | ||
| [Required] | ||
| public required MasterPasswordAuthenticationDataRequestModel AuthenticationData { get; set; } | ||
| [Required] | ||
| public required MasterPasswordUnlockDataRequestModel UnlockData { get; set; } | ||
|
|
||
| public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) | ||
| { | ||
| var hasNewPayloads = AuthenticationData is not null && UnlockData is not null; | ||
| var hasLegacyPayloads = NewMasterPasswordHash is not null && Key is not null; | ||
|
|
||
| foreach (var validationResult in MasterPasswordPayloadVariantValidator.ValidatePresence( | ||
| hasNewPayloads, hasLegacyPayloads)) | ||
| // [Required] on AuthenticationData/UnlockData reports null-field errors via ModelState; | ||
| // this method only runs cross-field validation when both are present. | ||
| if (AuthenticationData == null || UnlockData == null) | ||
| { | ||
| yield return validationResult; | ||
| yield break; | ||
| } | ||
|
|
||
| if (hasNewPayloads) | ||
| foreach (var validationResult in KdfSettingsValidator.ValidateAuthenticationAndUnlockData( | ||
| AuthenticationData.ToData(), UnlockData.ToData())) | ||
| { | ||
| foreach (var validationResult in KdfSettingsValidator.ValidateAuthenticationAndUnlockData( | ||
| AuthenticationData!.ToData(), UnlockData!.ToData())) | ||
| { | ||
| yield return validationResult; | ||
| } | ||
| yield return validationResult; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,13 @@ | ||
| ο»Ώusing Bit.Core.Entities; | ||
| ο»Ώusing Bit.Core.Auth.UserFeatures.UserMasterPassword.Data; | ||
| using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; | ||
| using Bit.Core.Entities; | ||
| using Bit.Core.Enums; | ||
| 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; | ||
|
|
||
|
|
@@ -16,20 +16,18 @@ public class ChangeKdfCommand : IChangeKdfCommand | |
| { | ||
| private readonly IUserService _userService; | ||
| private readonly IPushNotificationService _pushService; | ||
| private readonly IUserRepository _userRepository; | ||
| private readonly IMasterPasswordService _masterPasswordService; | ||
| private readonly IdentityErrorDescriber _identityErrorDescriber; | ||
| private readonly ILogger<ChangeKdfCommand> _logger; | ||
| private readonly IFeatureService _featureService; | ||
|
|
||
| public ChangeKdfCommand(IUserService userService, IPushNotificationService pushService, | ||
| IUserRepository userRepository, IdentityErrorDescriber describer, ILogger<ChangeKdfCommand> logger, | ||
| IMasterPasswordService masterPasswordService, IdentityErrorDescriber describer, | ||
| IFeatureService featureService) | ||
| { | ||
| _userService = userService; | ||
| _pushService = pushService; | ||
| _userRepository = userRepository; | ||
| _masterPasswordService = masterPasswordService; | ||
| _identityErrorDescriber = describer; | ||
| _logger = logger; | ||
| _featureService = featureService; | ||
| } | ||
|
|
||
|
|
@@ -44,7 +42,8 @@ public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswor | |
|
|
||
| // 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 | ||
| // Prevent a de-synced salt value from creating an un-decryptable unlock method. | ||
| // Also checked in the MasterPasswordService via UpdateExistingKdfConfigurationData.ValidateDataForUser. | ||
| authenticationData.ValidateSaltUnchangedForUser(user); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I have elected not to remove it from the command at this time, considering that up to Key Management's discretion. |
||
| unlockData.ValidateSaltUnchangedForUser(user); | ||
|
|
||
|
|
@@ -62,42 +61,21 @@ public async Task<IdentityResult> ChangeKdfAsync(User user, string masterPasswor | |
|
|
||
| var logoutOnKdfChange = !_featureService.IsEnabled(FeatureFlagKeys.NoLogoutOnKdfChange); | ||
|
|
||
| // 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. | ||
| var data = new UpdateExistingKdfConfigurationData | ||
| { | ||
| MasterPasswordAuthentication = authenticationData, | ||
| MasterPasswordUnlock = unlockData, | ||
| ValidatePassword = true, | ||
| RefreshStamp = logoutOnKdfChange, | ||
| MasterPasswordHint = user.MasterPasswordHint, // KDF rotation does not change the hint; carry existing value through | ||
| }; | ||
|
|
||
| // 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, | ||
| refreshStamp: logoutOnKdfChange); | ||
| if (!result.Succeeded) | ||
| var result = await _masterPasswordService.SaveUpdateExistingKdfConfigurationAsync(user, data); | ||
| if (result.TryPickT1(out var errors, out _)) | ||
| { | ||
| _logger.LogWarning("Change KDF failed for user {userId}.", user.Id); | ||
| return result; | ||
| return IdentityResult.Failed(errors); | ||
| } | ||
|
|
||
| // 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); | ||
| if (logoutOnKdfChange) | ||
| { | ||
| await _pushService.PushLogOutAsync(user.Id); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method incorrectly indexed on a change of hash to warrant an update to the
LastPasswordChangeDate. KDF- and Master Password-change operations are separate, andLastPasswordChangeDateis intended to capture the user action/intent to change the Master Password.LastKdfChangeDateis provided to separate these concerns.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent catch.