using System; using System.Threading.Tasks; using System.Windows.Input; using Bit.App.Abstractions; using Bit.App.Controls; using Bit.Core.Resources.Localization; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Models.Request; using Bit.Core.Services; using Bit.Core.Utilities; using CommunityToolkit.Maui.Converters; using Microsoft.Maui.Controls; using Microsoft.Maui; using AsyncAwaitBestPractices; namespace Bit.App.Pages { public class LockPageViewModel : BaseViewModel { private readonly IApiService _apiService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IDeviceActionService _deviceActionService; private readonly IVaultTimeoutService _vaultTimeoutService; private readonly ICryptoService _cryptoService; private readonly IMessagingService _messagingService; private readonly IEnvironmentService _environmentService; private readonly IStateService _stateService; private readonly IBiometricService _biometricService; private readonly IUserVerificationService _userVerificationService; private readonly ILogger _logger; private readonly IWatchDeviceService _watchDeviceService; private readonly WeakEventManager _secretEntryFocusWeakEventManager = new WeakEventManager(); private readonly IPolicyService _policyService; private readonly IPasswordGenerationService _passwordGenerationService; private IDeviceTrustCryptoService _deviceTrustCryptoService; private readonly ISyncService _syncService; private string _email; private string _masterPassword; private string _pin; private bool _showPassword; private PinLockType _pinStatus; private bool _pinEnabled; private bool _biometricEnabled; private bool _biometricIntegrityValid = true; private bool _biometricButtonVisible; private bool _hasMasterPassword; private string _biometricButtonText; private string _loggedInAsText; private string _lockedVerifyText; public LockPageViewModel() { _apiService = ServiceContainer.Resolve("apiService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); _cryptoService = ServiceContainer.Resolve("cryptoService"); _messagingService = ServiceContainer.Resolve("messagingService"); _environmentService = ServiceContainer.Resolve("environmentService"); _stateService = ServiceContainer.Resolve("stateService"); _biometricService = ServiceContainer.Resolve("biometricService"); _userVerificationService = ServiceContainer.Resolve(); _logger = ServiceContainer.Resolve("logger"); _watchDeviceService = ServiceContainer.Resolve(); _policyService = ServiceContainer.Resolve(); _passwordGenerationService = ServiceContainer.Resolve(); _deviceTrustCryptoService = ServiceContainer.Resolve(); _syncService = ServiceContainer.Resolve(); PageTitle = AppResources.VerifyMasterPassword; TogglePasswordCommand = new Command(TogglePassword); SubmitCommand = CreateDefaultAsyncRelayCommand(SubmitAsync, onException: _logger.Exception, allowsMultipleExecutions: false); AccountSwitchingOverlayViewModel = new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger) { AllowAddAccountRow = true, AllowActiveAccountSelection = true }; } public string MasterPassword { get => _masterPassword; set => SetProperty(ref _masterPassword, value); } public string Pin { get => _pin; set => SetProperty(ref _pin, value); } public bool ShowPassword { get => _showPassword; set => SetProperty(ref _showPassword, value, additionalPropertyNames: new string[] { nameof(ShowPasswordIcon), nameof(PasswordVisibilityAccessibilityText), }); } public bool PinEnabled { get => _pinEnabled; set => SetProperty(ref _pinEnabled, value); } public bool HasMasterPassword { get => _hasMasterPassword; } public bool BiometricEnabled { get => _biometricEnabled; set => SetProperty(ref _biometricEnabled, value); } public bool BiometricIntegrityValid { get => _biometricIntegrityValid; set => SetProperty(ref _biometricIntegrityValid, value); } public bool BiometricButtonVisible { get => _biometricButtonVisible; set => SetProperty(ref _biometricButtonVisible, value); } public string BiometricButtonText { get => _biometricButtonText; set => SetProperty(ref _biometricButtonText, value); } public string LoggedInAsText { get => _loggedInAsText; set => SetProperty(ref _loggedInAsText, value); } public string LockedVerifyText { get => _lockedVerifyText; set => SetProperty(ref _lockedVerifyText, value); } public bool CheckPendingAuthRequests { get; set; } public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; } public ICommand SubmitCommand { get; } public Command TogglePasswordCommand { get; } public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye; public string PasswordVisibilityAccessibilityText => ShowPassword ? AppResources.PasswordIsVisibleTapToHide : AppResources.PasswordIsNotVisibleTapToShow; public Action UnlockedAction { get; set; } public event Action FocusSecretEntry { add => _secretEntryFocusWeakEventManager.AddEventHandler(value); remove => _secretEntryFocusWeakEventManager.RemoveEventHandler(value); } public async Task InitAsync() { var pendingRequest = await _stateService.GetPendingAdminAuthRequestAsync(); if (pendingRequest != null && CheckPendingAuthRequests) { await _vaultTimeoutService.LogOutAsync(); return; } _pinStatus = await _vaultTimeoutService.GetPinLockTypeAsync(); var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync() ?? await _stateService.GetPinProtectedKeyAsync(); PinEnabled = (_pinStatus == PinLockType.Transient && ephemeralPinSet != null) || _pinStatus == PinLockType.Persistent; BiometricEnabled = await IsBiometricsEnabledAsync(); // Users without MP and without biometric or pin has no MP to unlock with _hasMasterPassword = await _userVerificationService.HasMasterPasswordAsync(); if (await _stateService.IsAuthenticatedAsync() && !_hasMasterPassword && !BiometricEnabled && !PinEnabled) { await _vaultTimeoutService.LogOutAsync(); return; } _email = await _stateService.GetEmailAsync(); if (string.IsNullOrWhiteSpace(_email)) { await _vaultTimeoutService.LogOutAsync(); _logger.Exception(new NullReferenceException("Email not found in storage")); return; } LoggedInAsText = string.Format(AppResources.LoggedInAsOn, _email, _environmentService.GetCurrentDomain()); if (PinEnabled) { PageTitle = AppResources.VerifyPIN; LockedVerifyText = AppResources.VaultLockedPIN; } else { PageTitle = _hasMasterPassword ? AppResources.VerifyMasterPassword : AppResources.UnlockVault; LockedVerifyText = _hasMasterPassword ? AppResources.VaultLockedMasterPassword : AppResources.VaultLockedIdentity; } if (BiometricEnabled) { BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); if (!_biometricIntegrityValid) { BiometricButtonVisible = false; return; } BiometricButtonVisible = true; BiometricButtonText = AppResources.UseBiometricsToUnlock; if (DeviceInfo.Platform == DevicePlatform.iOS) { var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync(); BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock : AppResources.UseFingerprintToUnlock; } } } public async Task SubmitAsync() { try { ShowPassword = false; var kdfConfig = await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)); if (PinEnabled) { await UnlockWithPinAsync(kdfConfig); } else { await UnlockWithMasterPasswordAsync(kdfConfig); } } catch (LegacyUserException) { await HandleLegacyUserAsync(); } catch (Exception ex) { HandleException(ex); } } private async Task UnlockWithPinAsync(KdfConfig kdfConfig) { if (PinEnabled && string.IsNullOrWhiteSpace(Pin)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.PIN), AppResources.Ok); return; } var failed = true; try { EncString userKeyPin; EncString oldPinProtected; switch (_pinStatus) { case PinLockType.Persistent: { userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync(); var oldEncryptedKey = await _stateService.GetPinProtectedAsync(); oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null; break; } case PinLockType.Transient: userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync(); oldPinProtected = await _stateService.GetPinProtectedKeyAsync(); break; case PinLockType.Disabled: default: throw new Exception("Pin is disabled"); } UserKey userKey; if (oldPinProtected != null) { userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync( _pinStatus == PinLockType.Transient, Pin, _email, kdfConfig, oldPinProtected ); } else { userKey = await _cryptoService.DecryptUserKeyWithPinAsync( Pin, _email, kdfConfig, userKeyPin ); } var protectedPin = await _stateService.GetProtectedPinAsync(); var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey); failed = decryptedPin != Pin; if (!failed) { Pin = string.Empty; await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await SetUserKeyAndContinueAsync(userKey); await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand } } catch (LegacyUserException) { throw; } catch { failed = true; } if (failed) { var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); if (invalidUnlockAttempts >= 5) { _messagingService.Send("logout"); return; } await _platformUtilsService.ShowDialogAsync(AppResources.InvalidPIN, AppResources.AnErrorHasOccurred); } } private async Task UnlockWithMasterPasswordAsync(KdfConfig kdfConfig) { if (!PinEnabled && string.IsNullOrWhiteSpace(MasterPassword)) { await Page.DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, AppResources.MasterPassword), AppResources.Ok); return; } var masterKey = await _cryptoService.MakeMasterKeyAsync(MasterPassword, _email, kdfConfig); if (await _cryptoService.IsLegacyUserAsync(masterKey)) { throw new LegacyUserException(); } var storedKeyHash = await _cryptoService.GetMasterKeyHashAsync(); var passwordValid = false; MasterPasswordPolicyOptions enforcedMasterPasswordOptions = null; if (storedKeyHash != null) { // Offline unlock possible passwordValid = await _cryptoService.CompareAndUpdateKeyHashAsync(MasterPassword, masterKey); } else { // Online unlock required await _deviceActionService.ShowLoadingAsync(AppResources.Loading); var keyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.ServerAuthorization); var request = new PasswordVerificationRequest(); request.MasterPasswordHash = keyHash; try { var response = await _apiService.PostAccountVerifyPasswordAsync(request); enforcedMasterPasswordOptions = response.MasterPasswordPolicy; passwordValid = true; var localKeyHash = await _cryptoService.HashMasterKeyAsync(MasterPassword, masterKey, HashPurpose.LocalAuthorization); await _cryptoService.SetMasterKeyHashAsync(localKeyHash); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(">>> {0}: {1}", e.GetType(), e.StackTrace); } await _deviceActionService.HideLoadingAsync(); } if (passwordValid) { 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(); var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey); await _cryptoService.SetMasterKeyAsync(masterKey); await SetUserKeyAndContinueAsync(userKey); await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand // Re-enable biometrics if (BiometricEnabled & !BiometricIntegrityValid) { await _biometricService.SetupBiometricAsync(); } } else { var invalidUnlockAttempts = await AppHelpers.IncrementInvalidUnlockAttemptsAsync(); if (invalidUnlockAttempts >= 5) { _messagingService.Send("logout"); return; } await _platformUtilsService.ShowDialogAsync(AppResources.InvalidMasterPassword, AppResources.AnErrorHasOccurred); } } /// /// 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, AppResources.LogOut, AppResources.Yes, AppResources.Cancel); if (confirmed) { _messagingService.Send("logout"); } } public void ResetPinPasswordFields() { try { MasterPassword = string.Empty; Pin = string.Empty; ShowPassword = false; } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); } } public void TogglePassword() { ShowPassword = !ShowPassword; var secret = PinEnabled ? Pin : MasterPassword; _secretEntryFocusWeakEventManager.RaiseEvent(string.IsNullOrEmpty(secret) ? 0 : secret.Length, nameof(FocusSecretEntry)); } public async Task PromptBiometricAsync() { try { BiometricIntegrityValid = await _platformUtilsService.IsBiometricIntegrityValidAsync(); BiometricButtonVisible = BiometricIntegrityValid; if (!BiometricEnabled || !BiometricIntegrityValid) { return; } var success = await _platformUtilsService.AuthenticateBiometricAsync(null, PinEnabled ? AppResources.PIN : AppResources.MasterPassword, () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)), !PinEnabled && !HasMasterPassword) ?? false; await _stateService.SetBiometricLockedAsync(!success); if (success) { var userKey = await _cryptoService.GetBiometricUnlockKeyAsync(); await SetUserKeyAndContinueAsync(userKey); } } catch (LegacyUserException) { await HandleLegacyUserAsync(); } } private async Task SetUserKeyAndContinueAsync(UserKey key) { var hasKey = await _cryptoService.HasUserKeyAsync(); if (!hasKey) { await _cryptoService.SetUserKeyAsync(key); } await _deviceTrustCryptoService.TrustDeviceIfNeededAsync(); await DoContinueAsync(); } private async Task DoContinueAsync() { _syncService.FullSyncAsync(false).FireAndForget(); await _stateService.SetBiometricLockedAsync(false); _watchDeviceService.SyncDataToWatchAsync().FireAndForget(); _messagingService.Send("unlocked"); UnlockedAction?.Invoke(); } private async Task IsBiometricsEnabledAsync() { try { return await _vaultTimeoutService.IsBiometricLockSetAsync() && await _biometricService.CanUseBiometricsUnlockAsync(); } catch (LegacyUserException) { await HandleLegacyUserAsync(); } return false; } private async Task HandleLegacyUserAsync() { // Legacy users must migrate on web vault. await _platformUtilsService.ShowDialogAsync(AppResources.EncryptionKeyMigrationRequiredDescriptionLong, AppResources.AnErrorHasOccurred, AppResources.Ok); await _vaultTimeoutService.LogOutAsync(); } } }