bitwarden-mobile/src/Core/Pages/Settings/SecuritySettingsPageViewMod...

564 lines
25 KiB
C#

using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Pages.Accounts;
using Bit.Core.Resources.Localization;
using Bit.App.Utilities;
using Bit.Core;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
using Bit.Core.Utilities;
using CommunityToolkit.Mvvm.Input;
namespace Bit.App.Pages
{
public class SecuritySettingsPageViewModel : BaseViewModel
{
private const int NEVER_SESSION_TIMEOUT_VALUE = -2;
private const int CUSTOM_VAULT_TIMEOUT_VALUE = -100;
private readonly IStateService _stateService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPlatformUtilsService _platformUtilsService;
private readonly IDeviceActionService _deviceActionService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IBiometricService _biometricsService;
private readonly IUserPinService _userPinService;
private readonly ICryptoService _cryptoService;
private readonly IUserVerificationService _userVerificationService;
private readonly IPolicyService _policyService;
private readonly IMessagingService _messagingService;
private readonly IEnvironmentService _environmentService;
private readonly ILogger _logger;
private bool _inited;
private bool _useThisDeviceToApproveLoginRequests;
private bool _supportsBiometric, _canUnlockWithBiometrics;
private bool _canUnlockWithPin;
private bool _hasMasterPassword;
private int? _maximumVaultTimeoutPolicy;
private string _vaultTimeoutActionPolicy;
private TimeSpan? _customVaultTimeoutTime;
public SecuritySettingsPageViewModel()
{
_stateService = ServiceContainer.Resolve<IStateService>();
_pushNotificationService = ServiceContainer.Resolve<IPushNotificationService>();
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>();
_deviceActionService = ServiceContainer.Resolve<IDeviceActionService>();
_vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
_biometricsService = ServiceContainer.Resolve<IBiometricService>();
_userPinService = ServiceContainer.Resolve<IUserPinService>();
_cryptoService = ServiceContainer.Resolve<ICryptoService>();
_userVerificationService = ServiceContainer.Resolve<IUserVerificationService>();
_policyService = ServiceContainer.Resolve<IPolicyService>();
_messagingService = ServiceContainer.Resolve<IMessagingService>();
_environmentService = ServiceContainer.Resolve<IEnvironmentService>();
_logger = ServiceContainer.Resolve<ILogger>();
VaultTimeoutPickerViewModel = new PickerViewModel<int>(
_deviceActionService,
_logger,
OnVaultTimeoutChangingAsync,
AppResources.SessionTimeout,
() => _inited,
ex => HandleException(ex));
VaultTimeoutPickerViewModel.SetAfterSelectionChanged(_ => MainThread.InvokeOnMainThreadAsync(TriggerUpdateCustomVaultTimeoutPicker));
VaultTimeoutActionPickerViewModel = new PickerViewModel<VaultTimeoutAction>(
_deviceActionService,
_logger,
OnVaultTimeoutActionChangingAsync,
AppResources.SessionTimeoutAction,
() => _inited && !HasVaultTimeoutActionPolicy && IsVaultTimeoutActionLockAllowed,
ex => HandleException(ex));
ToggleUseThisDeviceToApproveLoginRequestsCommand = CreateDefaultAsyncRelayCommand(ToggleUseThisDeviceToApproveLoginRequestsAsync, () => _inited, allowsMultipleExecutions: false);
GoToPendingLogInRequestsCommand = CreateDefaultAsyncRelayCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new LoginPasswordlessRequestsListPage())), allowsMultipleExecutions: false);
ToggleCanUnlockWithBiometricsCommand = CreateDefaultAsyncRelayCommand(ToggleCanUnlockWithBiometricsAsync, () => _inited, allowsMultipleExecutions: false);
ToggleCanUnlockWithPinCommand = CreateDefaultAsyncRelayCommand(ToggleCanUnlockWithPinAsync, () => _inited, allowsMultipleExecutions: false);
ShowAccountFingerprintPhraseCommand = CreateDefaultAsyncRelayCommand(ShowAccountFingerprintPhraseAsync, allowsMultipleExecutions: false);
GoToTwoStepLoginCommand = CreateDefaultAsyncRelayCommand(() => GoToWebVaultSettingsAsync(AppResources.TwoStepLoginDescriptionLong, AppResources.ContinueToWebApp), allowsMultipleExecutions: false);
GoToChangeMasterPasswordCommand = CreateDefaultAsyncRelayCommand(() => GoToWebVaultSettingsAsync(AppResources.ChangeMasterPasswordDescriptionLong, AppResources.ContinueToWebApp), allowsMultipleExecutions: false);
LockCommand = CreateDefaultAsyncRelayCommand(() => _vaultTimeoutService.LockAsync(true, true), allowsMultipleExecutions: false);
LogOutCommand = CreateDefaultAsyncRelayCommand(LogOutAsync, allowsMultipleExecutions: false);
DeleteAccountCommand = CreateDefaultAsyncRelayCommand(() => Page.Navigation.PushModalAsync(new NavigationPage(new DeleteAccountPage())), allowsMultipleExecutions: false);
}
public bool UseThisDeviceToApproveLoginRequests
{
get => _useThisDeviceToApproveLoginRequests;
set
{
if (SetProperty(ref _useThisDeviceToApproveLoginRequests, value))
{
((ICommand)ToggleUseThisDeviceToApproveLoginRequestsCommand).Execute(null);
}
}
}
public string UnlockWithBiometricsTitle
{
get
{
if (!_supportsBiometric)
{
return null;
}
var biometricName = AppResources.Biometrics;
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
biometricName = _deviceActionService.SupportsFaceBiometric()
? AppResources.FaceID
: AppResources.TouchID;
}
return string.Format(AppResources.UnlockWith, biometricName);
}
}
public bool CanUnlockWithBiometrics
{
get => _canUnlockWithBiometrics;
set
{
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
if (SetProperty(ref _canUnlockWithBiometrics, value))
{
((ICommand)ToggleCanUnlockWithBiometricsCommand).Execute(null);
}
}
}
public bool CanUnlockWithPin
{
get => _canUnlockWithPin;
set
{
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
if (SetProperty(ref _canUnlockWithPin, value))
{
((ICommand)ToggleCanUnlockWithPinCommand).Execute(null);
}
}
}
public bool IsVaultTimeoutActionLockAllowed => _hasMasterPassword || _canUnlockWithBiometrics || _canUnlockWithPin;
public string SetUpUnlockMethodLabel => IsVaultTimeoutActionLockAllowed ? null : AppResources.SetUpAnUnlockOptionToChangeYourVaultTimeoutAction;
public TimeSpan? CustomVaultTimeoutTime
{
get => _customVaultTimeoutTime;
set
{
var oldValue = _customVaultTimeoutTime;
if (SetProperty(ref _customVaultTimeoutTime, value, additionalPropertyNames: new string[] { nameof(CustomVaultTimeoutTimeVerbalized) }) && value.HasValue)
{
UpdateVaultTimeoutAsync((int)value.Value.TotalMinutes)
.FireAndForget(ex =>
{
HandleException(ex);
MainThread.BeginInvokeOnMainThread(() => SetProperty(ref _customVaultTimeoutTime, oldValue));
});
}
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
}
}
public string CustomVaultTimeoutTimeVerbalized => CustomVaultTimeoutTime?.Verbalize(A11yExtensions.TimeSpanVerbalizationMode.HoursAndMinutes);
public bool ShowCustomVaultTimeoutPicker => VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE;
public bool ShowVaultTimeoutPolicyInfo => _maximumVaultTimeoutPolicy.HasValue || HasVaultTimeoutActionPolicy;
public string VaultTimeoutPolicyDescription
{
get
{
if (!ShowVaultTimeoutPolicyInfo)
{
return null;
}
static string LocalizeTimeoutAction(string actionPolicy)
{
return actionPolicy == Policy.ACTION_LOCK ? AppResources.Lock : AppResources.LogOut;
};
if (!_maximumVaultTimeoutPolicy.HasValue)
{
return string.Format(AppResources.VaultTimeoutActionPolicyInEffect, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
}
var hours = Math.Floor((float)_maximumVaultTimeoutPolicy / 60);
var minutes = _maximumVaultTimeoutPolicy % 60;
return string.IsNullOrWhiteSpace(_vaultTimeoutActionPolicy)
? string.Format(AppResources.VaultTimeoutPolicyInEffect, hours, minutes)
: string.Format(AppResources.VaultTimeoutPolicyWithActionInEffect, hours, minutes, LocalizeTimeoutAction(_vaultTimeoutActionPolicy));
}
}
public bool ShowChangeMasterPassword { get; private set; }
private int? CurrentVaultTimeout => GetRawVaultTimeoutFrom(VaultTimeoutPickerViewModel.SelectedKey);
private bool IncludeLinksWithSubscriptionInfo => DeviceInfo.Platform != DevicePlatform.iOS;
private bool HasVaultTimeoutActionPolicy => !string.IsNullOrEmpty(_vaultTimeoutActionPolicy);
public PickerViewModel<int> VaultTimeoutPickerViewModel { get; }
public PickerViewModel<VaultTimeoutAction> VaultTimeoutActionPickerViewModel { get; }
public AsyncRelayCommand ToggleUseThisDeviceToApproveLoginRequestsCommand { get; }
public ICommand GoToPendingLogInRequestsCommand { get; }
public AsyncRelayCommand ToggleCanUnlockWithBiometricsCommand { get; }
public AsyncRelayCommand ToggleCanUnlockWithPinCommand { get; }
public ICommand ShowAccountFingerprintPhraseCommand { get; }
public ICommand GoToTwoStepLoginCommand { get; }
public ICommand GoToChangeMasterPasswordCommand { get; }
public ICommand LockCommand { get; }
public ICommand LogOutCommand { get; }
public ICommand DeleteAccountCommand { get; }
public async Task InitAsync()
{
var decryptionOptions = await _stateService.GetAccountDecryptionOptions();
// set default true for backwards compatibility
_hasMasterPassword = decryptionOptions?.HasMasterPassword ?? true;
_useThisDeviceToApproveLoginRequests = await _stateService.GetApprovePasswordlessLoginsAsync();
_supportsBiometric = await _platformUtilsService.SupportsBiometricAsync();
_canUnlockWithBiometrics = await _vaultTimeoutService.IsBiometricLockSetAsync();
_canUnlockWithPin = await _vaultTimeoutService.GetPinLockTypeAsync() != Core.Services.PinLockType.Disabled;
await LoadPoliciesAsync();
await InitVaultTimeoutPickerAsync();
await InitVaultTimeoutActionPickerAsync();
ShowChangeMasterPassword = IncludeLinksWithSubscriptionInfo && await _userVerificationService.HasMasterPasswordAsync();
_inited = true;
MainThread.BeginInvokeOnMainThread(() =>
{
TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests));
TriggerPropertyChanged(nameof(UnlockWithBiometricsTitle));
TriggerPropertyChanged(nameof(CanUnlockWithBiometrics));
TriggerPropertyChanged(nameof(CanUnlockWithPin));
TriggerPropertyChanged(nameof(ShowVaultTimeoutPolicyInfo));
TriggerPropertyChanged(nameof(VaultTimeoutPolicyDescription));
TriggerPropertyChanged(nameof(ShowChangeMasterPassword));
TriggerUpdateCustomVaultTimeoutPicker();
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
ToggleUseThisDeviceToApproveLoginRequestsCommand.NotifyCanExecuteChanged();
ToggleCanUnlockWithBiometricsCommand.NotifyCanExecuteChanged();
ToggleCanUnlockWithPinCommand.NotifyCanExecuteChanged();
VaultTimeoutPickerViewModel.SelectOptionCommand.NotifyCanExecuteChanged();
VaultTimeoutActionPickerViewModel.SelectOptionCommand.NotifyCanExecuteChanged();
});
}
private async Task LoadPoliciesAsync()
{
if (!await _policyService.PolicyAppliesToUser(PolicyType.MaximumVaultTimeout))
{
return;
}
var maximumVaultTimeoutPolicy = await _policyService.FirstOrDefault(PolicyType.MaximumVaultTimeout);
_maximumVaultTimeoutPolicy = maximumVaultTimeoutPolicy?.GetInt(Policy.MINUTES_KEY);
_vaultTimeoutActionPolicy = maximumVaultTimeoutPolicy?.GetString(Policy.ACTION_KEY);
MainThread.BeginInvokeOnMainThread(VaultTimeoutActionPickerViewModel.SelectOptionCommand.NotifyCanExecuteChanged);
}
private async Task InitVaultTimeoutPickerAsync()
{
var options = new Dictionary<int, string>
{
[0] = AppResources.Immediately,
[1] = AppResources.OneMinute,
[5] = AppResources.FiveMinutes,
[15] = AppResources.FifteenMinutes,
[30] = AppResources.ThirtyMinutes,
[60] = AppResources.OneHour,
[240] = AppResources.FourHours,
[-1] = AppResources.OnRestart,
[NEVER_SESSION_TIMEOUT_VALUE] = AppResources.Never
};
if (_maximumVaultTimeoutPolicy.HasValue)
{
options = options.Where(t => t.Key >= 0 && t.Key <= _maximumVaultTimeoutPolicy.Value)
.ToDictionary(v => v.Key, v => v.Value);
}
options.Add(CUSTOM_VAULT_TIMEOUT_VALUE, AppResources.Custom);
var vaultTimeout = await _vaultTimeoutService.GetVaultTimeout() ?? NEVER_SESSION_TIMEOUT_VALUE;
VaultTimeoutPickerViewModel.Init(options, vaultTimeout, CUSTOM_VAULT_TIMEOUT_VALUE, false);
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(vaultTimeout);
}
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
}
private async Task InitVaultTimeoutActionPickerAsync()
{
var options = new Dictionary<VaultTimeoutAction, string>();
if (IsVaultTimeoutActionLockAllowed)
{
options.Add(VaultTimeoutAction.Lock, AppResources.Lock);
}
options.Add(VaultTimeoutAction.Logout, AppResources.LogOut);
var timeoutAction = await _vaultTimeoutService.GetVaultTimeoutAction() ?? VaultTimeoutAction.Lock;
if (!IsVaultTimeoutActionLockAllowed && timeoutAction == VaultTimeoutAction.Lock)
{
timeoutAction = VaultTimeoutAction.Logout;
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
}
VaultTimeoutActionPickerViewModel.Init(options, timeoutAction, IsVaultTimeoutActionLockAllowed ? VaultTimeoutAction.Lock : VaultTimeoutAction.Logout);
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
}
private async Task ToggleUseThisDeviceToApproveLoginRequestsAsync()
{
if (UseThisDeviceToApproveLoginRequests
&&
!await Page.DisplayAlert(AppResources.ApproveLoginRequests, AppResources.UseThisDeviceToApproveLoginRequestsMadeFromOtherDevices, AppResources.Yes, AppResources.No))
{
_useThisDeviceToApproveLoginRequests = !UseThisDeviceToApproveLoginRequests;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(UseThisDeviceToApproveLoginRequests)));
return;
}
await _stateService.SetApprovePasswordlessLoginsAsync(UseThisDeviceToApproveLoginRequests);
if (!UseThisDeviceToApproveLoginRequests || await _pushNotificationService.AreNotificationsSettingsEnabledAsync())
{
return;
}
var openAppSettingsResult = await _platformUtilsService.ShowDialogAsync(
AppResources.ReceivePushNotificationsForNewLoginRequests,
string.Empty,
AppResources.Settings,
AppResources.NoThanks
);
if (openAppSettingsResult)
{
_deviceActionService.OpenAppSettings();
}
}
private async Task ToggleCanUnlockWithBiometricsAsync()
{
if (!_canUnlockWithBiometrics)
{
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
await UpdateVaultTimeoutActionIfNeededAsync();
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
return;
}
if (!_supportsBiometric
||
await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
{
_canUnlockWithBiometrics = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
return;
}
await _biometricsService.SetCanUnlockWithBiometricsAsync(CanUnlockWithBiometrics);
await InitVaultTimeoutActionPickerAsync();
}
public async Task ToggleCanUnlockWithPinAsync()
{
if (!_canUnlockWithPin)
{
await _vaultTimeoutService.ClearAsync();
await UpdateVaultTimeoutActionIfNeededAsync();
return;
}
var newPin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
AppResources.SetPINDescription, null, AppResources.Submit, AppResources.Cancel, true);
if (string.IsNullOrWhiteSpace(newPin))
{
_canUnlockWithPin = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithPin)));
return;
}
var requireMasterPasswordOnRestart = await _userVerificationService.HasMasterPasswordAsync()
&&
await _platformUtilsService.ShowDialogAsync(AppResources.PINRequireMasterPasswordRestart,
AppResources.UnlockWithPIN,
AppResources.Yes,
AppResources.No);
await _userPinService.SetupPinAsync(newPin, requireMasterPasswordOnRestart);
await InitVaultTimeoutActionPickerAsync();
}
private async Task UpdateVaultTimeoutActionIfNeededAsync()
{
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
if (IsVaultTimeoutActionLockAllowed)
{
return;
}
VaultTimeoutActionPickerViewModel.Select(VaultTimeoutAction.Logout);
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, VaultTimeoutAction.Logout);
_deviceActionService.Toast(AppResources.VaultTimeoutActionChangedToLogOut);
}
private async Task<bool> OnVaultTimeoutChangingAsync(int newTimeout)
{
if (newTimeout == NEVER_SESSION_TIMEOUT_VALUE
&&
!await _platformUtilsService.ShowDialogAsync(AppResources.NeverLockWarning, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
{
return false;
}
if (newTimeout == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(0);
}
return await UpdateVaultTimeoutAsync(newTimeout);
}
private async Task<bool> UpdateVaultTimeoutAsync(int newTimeout)
{
var rawTimeout = GetRawVaultTimeoutFrom(newTimeout);
if (rawTimeout > _maximumVaultTimeoutPolicy)
{
await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutToLarge, AppResources.Warning);
VaultTimeoutPickerViewModel.Select(_maximumVaultTimeoutPolicy.Value, false);
if (VaultTimeoutPickerViewModel.SelectedKey == CUSTOM_VAULT_TIMEOUT_VALUE)
{
_customVaultTimeoutTime = TimeSpan.FromMinutes(_maximumVaultTimeoutPolicy.Value);
}
MainThread.BeginInvokeOnMainThread(TriggerUpdateCustomVaultTimeoutPicker);
return false;
}
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(rawTimeout, VaultTimeoutActionPickerViewModel.SelectedKey);
await _cryptoService.RefreshKeysAsync();
return true;
}
private void TriggerUpdateCustomVaultTimeoutPicker()
{
TriggerPropertyChanged(nameof(ShowCustomVaultTimeoutPicker));
TriggerPropertyChanged(nameof(CustomVaultTimeoutTime));
}
private void TriggerVaultTimeoutActionLockAllowedPropertyChanged()
{
MainThread.BeginInvokeOnMainThread(() =>
{
TriggerPropertyChanged(nameof(IsVaultTimeoutActionLockAllowed));
TriggerPropertyChanged(nameof(SetUpUnlockMethodLabel));
VaultTimeoutActionPickerViewModel.SelectOptionCommand.NotifyCanExecuteChanged();
});
}
private int? GetRawVaultTimeoutFrom(int vaultTimeoutPickerKey)
{
if (vaultTimeoutPickerKey == NEVER_SESSION_TIMEOUT_VALUE)
{
return null;
}
if (vaultTimeoutPickerKey == CUSTOM_VAULT_TIMEOUT_VALUE
&&
CustomVaultTimeoutTime.HasValue)
{
return (int)CustomVaultTimeoutTime.Value.TotalMinutes;
}
return vaultTimeoutPickerKey;
}
private async Task<bool> OnVaultTimeoutActionChangingAsync(VaultTimeoutAction timeoutActionKey)
{
if (!string.IsNullOrEmpty(_vaultTimeoutActionPolicy))
{
// do nothing if we have a policy set
return false;
}
if (timeoutActionKey == VaultTimeoutAction.Logout
&&
!await _platformUtilsService.ShowDialogAsync(AppResources.VaultTimeoutLogOutConfirmation, AppResources.Warning, AppResources.Yes, AppResources.Cancel))
{
return false;
}
await _vaultTimeoutService.SetVaultTimeoutOptionsAsync(CurrentVaultTimeout, timeoutActionKey);
_messagingService.Send(AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND);
TriggerVaultTimeoutActionLockAllowedPropertyChanged();
return true;
}
private async Task ShowAccountFingerprintPhraseAsync()
{
List<string> fingerprint;
try
{
fingerprint = await _cryptoService.GetFingerprintAsync(await _stateService.GetActiveUserIdAsync());
}
catch (Exception e) when (e.Message == "No public key available.")
{
return;
}
var phrase = string.Join("-", fingerprint);
var text = $"{AppResources.YourAccountsFingerprint}:\n\n{phrase}";
var learnMore = await _platformUtilsService.ShowDialogAsync(text, AppResources.FingerprintPhrase,
AppResources.LearnMore, AppResources.Close);
if (learnMore)
{
_platformUtilsService.LaunchUri(ExternalLinksConstants.HELP_FINGERPRINT_PHRASE);
}
}
private async Task GoToWebVaultSettingsAsync(string dialogText, string dialogTitle)
{
if (await _platformUtilsService.ShowDialogAsync(dialogText, dialogTitle, AppResources.Continue, AppResources.Cancel))
{
_platformUtilsService.LaunchUri(string.Format(ExternalLinksConstants.WEB_VAULT_SETTINGS_FORMAT, _environmentService.GetWebVaultUrl()));
}
}
public async Task LogOutAsync()
{
if (await _platformUtilsService.ShowDialogAsync(AppResources.LogoutConfirmation, AppResources.LogOut, AppResources.Yes, AppResources.Cancel))
{
_messagingService.Send(AccountsManagerMessageCommands.LOGOUT);
}
}
}
}