581 lines
22 KiB
C#
581 lines
22 KiB
C#
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<int?> _secretEntryFocusWeakEventManager = new WeakEventManager<int?>();
|
|
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<IApiService>("apiService");
|
|
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
|
|
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
|
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
|
_cryptoService = ServiceContainer.Resolve<ICryptoService>("cryptoService");
|
|
_messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
|
|
_environmentService = ServiceContainer.Resolve<IEnvironmentService>("environmentService");
|
|
_stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
|
_biometricService = ServiceContainer.Resolve<IBiometricService>("biometricService");
|
|
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
|
|
_logger = ServiceContainer.Resolve<ILogger>("logger");
|
|
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
|
|
_policyService = ServiceContainer.Resolve<IPolicyService>();
|
|
_passwordGenerationService = ServiceContainer.Resolve<IPasswordGenerationService>();
|
|
_deviceTrustCryptoService = ServiceContainer.Resolve<IDeviceTrustCryptoService>();
|
|
_syncService = ServiceContainer.Resolve<ISyncService>();
|
|
|
|
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<int?> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the master password requires updating to meet the enforced policy requirements
|
|
/// </summary>
|
|
/// <param name="options"></param>
|
|
private async Task<bool> 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<bool> 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();
|
|
}
|
|
|
|
}
|
|
}
|