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}">