From b108b4e71d5abb4b4d09541916b2e4a2e4a6a780 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Mon, 17 Apr 2023 07:35:50 -0700 Subject: [PATCH] [AC-1070] Enforce master password policy on login/unlock (#2410) * [AC-1070] Add EnforceOnLogin property to MasterPasswordPolicyOptions * [AC-1070] Add MasterPasswordPolicy property to Identity responses * [AC-1070] Add policy service dependency to auth service * [AC-1070] Introduce logic to evaluate master password after successful login * [AC-1070] Add optional ForcePasswordResetReason to profile / state service * [AC-1070] Save ForcePasswordResetReason to state when a weak master password is found during login - Additionally, save the AdminForcePasswordReset reason if the identity result indicates an admin password reset is in effect. * [AC-1070] Check for a saved ForcePasswordReset reason on TabsPage load force show the update password page * [AC-1070] Make InitAsync virtual Allow the UpdateTempPasswordPage to override the InitAsync method to check for a reset password reason in the state service * [AC-1070] Modify UpdateTempPassword page appearance - Load the force password reset reason from the state service - Make warning text dynamic based on force password reason - Conditionally show the Current master password field if updating a weak master password * [AC-1070] Add update password method to Api service * [AC-1070] Introduce logic to update both temp and regular passwords - Check the Reason to use the appropriate request/endpoint when submitting. - Verify the users current password locally using the user verification service. * [AC-1070] Introduce VerifyMasterPasswordResponse * [AC-1070] Add logic to evaluate master password on unlock * [AC-1070] Add support 2FA login flow Keep track of the reset password reason after a password login requires 2FA. During 2FA submission, check if there is a saved reason, and if so, force the user to update their password. * [AC-1070] Formatting * [AC-1070] Remove string key from service resolution * [AC-1070] Change master password options to method variable to avoid class field Add null check for password strength result and log an error as this is an unexpected flow * [AC-1070] Remove usage of i18nService * [AC-1070] Use AsyncCommand for SubmitCommand * [AC-1070] Remove type from ShowToast call * [AC-1070] Simplify UpdatePassword methods to accept string for the new encryption key * [AC-1070] Use full text for key for the CurrentMasterPassword resource * [AC-1070] Convert Reason to a private class field * [AC-1070] Formatting changes * [AC-1070] Simplify if statements in master password options policy service method * [AC-1070] Use the saved force password reset reason after 2FA login * [AC-1070] Use constant for ForceUpdatePassword message command * [AC-1070] Move shared RequirePasswordChangeOnLogin method into PolicyService * Revert "[AC-1070] Move shared RequirePasswordChangeOnLogin method into PolicyService" This reverts commit e4feac130feee51c9657bfa3d073d2cc75021ed8. * [AC-1070] Add check for null password strength response * [AC-1070] Fix broken show password icon * [AC-1070] Add show password icon for current master password --- src/App/App.xaml.cs | 8 ++ .../Accounts/BaseChangePasswordViewModel.cs | 9 +- src/App/Pages/Accounts/LockPageViewModel.cs | 48 +++++++- .../Accounts/UpdateTempPasswordPage.xaml | 36 +++++- .../UpdateTempPasswordPageViewModel.cs | 104 ++++++++++++++++-- src/App/Pages/TabsPage.cs | 10 ++ src/App/Resources/AppResources.Designer.cs | 18 +++ src/App/Resources/AppResources.resx | 6 + src/Core/Abstractions/IApiService.cs | 3 +- src/Core/Abstractions/IStateService.cs | 2 + src/Core/Constants.cs | 1 + src/Core/Models/Domain/Account.cs | 2 + .../Models/Domain/ForcePasswordResetReason.cs | 16 +++ .../Domain/MasterPasswordPolicyOptions.cs | 1 + src/Core/Models/Request/PasswordRequest.cs | 10 ++ .../Models/Response/IdentityTokenResponse.cs | 5 +- .../Response/IdentityTwoFactorResponse.cs | 2 + .../Response/VerifyMasterPasswordResponse.cs | 9 ++ src/Core/Services/ApiService.cs | 11 +- src/Core/Services/AuthService.cs | 79 ++++++++++++- src/Core/Services/PolicyService.cs | 14 ++- src/Core/Services/StateService.cs | 16 +++ src/Core/Utilities/ServiceContainer.cs | 2 +- 23 files changed, 379 insertions(+), 33 deletions(-) create mode 100644 src/Core/Models/Domain/ForcePasswordResetReason.cs create mode 100644 src/Core/Models/Request/PasswordRequest.cs create mode 100644 src/Core/Models/Response/VerifyMasterPasswordResponse.cs diff --git a/src/App/App.xaml.cs b/src/App/App.xaml.cs index e10bf85c0..059159eb7 100644 --- a/src/App/App.xaml.cs +++ b/src/App/App.xaml.cs @@ -161,6 +161,14 @@ namespace Bit.App new NavigationPage(new RemoveMasterPasswordPage())); }); } + else if (message.Command == Constants.ForceUpdatePassword) + { + Device.BeginInvokeOnMainThread(async () => + { + await Application.Current.MainPage.Navigation.PushModalAsync( + new NavigationPage(new UpdateTempPasswordPage())); + }); + } else if (message.Command == Constants.PasswordlessLoginRequestKey || message.Command == "unlocked" || message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED) diff --git a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs index e90162158..a488a3b5f 100644 --- a/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs +++ b/src/App/Pages/Accounts/BaseChangePasswordViewModel.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; +using System.Text; using System.Threading.Tasks; using Bit.App.Abstractions; using Bit.App.Resources; -using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Models.Domain; @@ -73,13 +70,13 @@ namespace Bit.App.Pages set => SetProperty(ref _policy, value); } - public string ShowPasswordIcon => ShowPassword ? "" : ""; + public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public string MasterPassword { get; set; } public string ConfirmMasterPassword { get; set; } public string Hint { get; set; } - public async Task InitAsync(bool forceSync = false) + public virtual async Task InitAsync(bool forceSync = false) { if (forceSync) { diff --git a/src/App/Pages/Accounts/LockPageViewModel.cs b/src/App/Pages/Accounts/LockPageViewModel.cs index 1ed66fc34..fd62bc05a 100644 --- a/src/App/Pages/Accounts/LockPageViewModel.cs +++ b/src/App/Pages/Accounts/LockPageViewModel.cs @@ -31,6 +31,8 @@ namespace Bit.App.Pages private readonly ILogger _logger; private readonly IWatchDeviceService _watchDeviceService; private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager(); + private readonly IPolicyService _policyService; + private readonly IPasswordGenerationService _passwordGenerationService; private string _email; private string _masterPassword; @@ -61,6 +63,8 @@ namespace Bit.App.Pages _keyConnectorService = ServiceContainer.Resolve("keyConnectorService"); _logger = ServiceContainer.Resolve("logger"); _watchDeviceService = ServiceContainer.Resolve(); + _policyService = ServiceContainer.Resolve(); + _passwordGenerationService = ServiceContainer.Resolve(); PageTitle = AppResources.VerifyMasterPassword; TogglePasswordCommand = new Command(TogglePassword); @@ -294,6 +298,7 @@ namespace Bit.App.Pages var key = await _cryptoService.MakeKeyAsync(MasterPassword, _email, kdfConfig); var storedKeyHash = await _cryptoService.GetKeyHashAsync(); var passwordValid = false; + MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null; if (storedKeyHash != null) { @@ -305,9 +310,11 @@ namespace Bit.App.Pages var keyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.ServerAuthorization); var request = new PasswordVerificationRequest(); request.MasterPasswordHash = keyHash; + try { - await _apiService.PostAccountVerifyPasswordAsync(request); + var response = await _apiService.PostAccountVerifyPasswordAsync(request); + enforcedMasterPasswordOptions = response.MasterPasswordPolicy; passwordValid = true; var localKeyHash = await _cryptoService.HashPasswordAsync(MasterPassword, key, HashPurpose.LocalAuthorization); await _cryptoService.SetKeyHashAsync(localKeyHash); @@ -328,6 +335,14 @@ namespace Bit.App.Pages var pinKey = await _cryptoService.MakePinKeyAysnc(decPin, _email, kdfConfig); await _stateService.SetPinProtectedKeyAsync(await _cryptoService.EncryptAsync(key.Key, pinKey)); } + + if (await RequirePasswordChangeAsync(enforcedMasterPasswordOptions)) + { + // Save the ForcePasswordResetReason to force a password reset after unlock + await _stateService.SetForcePasswordResetReasonAsync( + ForcePasswordResetReason.WeakMasterPasswordOnLogin); + } + MasterPassword = string.Empty; await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await SetKeyAndContinueAsync(key); @@ -352,6 +367,37 @@ namespace Bit.App.Pages } } + /// + /// Checks if the master password requires updating to meet the enforced policy requirements + /// + /// + private async Task RequirePasswordChangeAsync(MasterPasswordPolicyOptions options = null) + { + // If no policy options are provided, attempt to load them from the policy service + var enforcedOptions = options ?? await _policyService.GetMasterPasswordPolicyOptions(); + + // No policy to enforce on login/unlock + if (!(enforcedOptions is { EnforceOnLogin: true })) + { + return false; + } + + var strength = _passwordGenerationService.PasswordStrength( + MasterPassword, _passwordGenerationService.GetPasswordStrengthUserInput(_email))?.Score; + + if (!strength.HasValue) + { + _logger.Error("Unable to evaluate master password strength during unlock"); + return false; + } + + return !await _policyService.EvaluateMasterPassword( + strength.Value, + MasterPassword, + enforcedOptions + ); + } + public async Task LogOutAsync() { var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, diff --git a/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml b/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml index ba76573b0..f559de05a 100644 --- a/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml +++ b/src/App/Pages/Accounts/UpdateTempPasswordPage.xaml @@ -46,7 +46,7 @@ BackgroundColor="Transparent" BorderColor="{DynamicResource PrimaryColor}">