From ebc068d820b69a984b3db8ff7c377eb6a2b0a1f4 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 21 Mar 2024 13:28:14 -0300 Subject: [PATCH] [PM-6848] Improved User verification on passkeys creation (#3099) * PM-6848 Updated cancellation flow on passkey user verification and improved UV enforcement on creation * PM-6848 Added null checks to help diagnosing if NRE is presented --- .../Abstractions/IPlatformUtilsService.cs | 2 +- .../IUserVerificationMediatorService.cs | 24 ++++- src/Core/Pages/Accounts/LockPageViewModel.cs | 2 +- .../Settings/SecuritySettingsPageViewModel.cs | 2 +- .../Services/Fido2AuthenticatorService.cs | 17 +--- .../Services/MobilePlatformUtilsService.cs | 6 +- ...serVerificationPreferredServiceStrategy.cs | 17 ++-- ...UserVerificationRequiredServiceStrategy.cs | 37 +++++--- .../IUserVerificationServiceStrategy.cs | 5 +- .../UserVerificationMediatorService.cs | 93 ++++++++++++++----- src/Core/Utilities/CancellableResult.cs | 15 +++ ...edentialProviderViewController.Passkeys.cs | 8 +- src/iOS.Autofill/LoginAddViewController.cs | 18 ++-- src/iOS.Autofill/LoginListViewController.cs | 44 ++++++--- .../BaseLockPasswordViewController.cs | 2 +- 15 files changed, 202 insertions(+), 90 deletions(-) create mode 100644 src/Core/Utilities/CancellableResult.cs diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs index b6e9ac5e6..2d09952f6 100644 --- a/src/Core/Abstractions/IPlatformUtilsService.cs +++ b/src/Core/Abstractions/IPlatformUtilsService.cs @@ -26,7 +26,7 @@ namespace Bit.Core.Abstractions bool SupportsDuo(); Task SupportsBiometricAsync(); Task IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null); - Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false); + Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false); long GetActiveTime(); } } diff --git a/src/Core/Abstractions/IUserVerificationMediatorService.cs b/src/Core/Abstractions/IUserVerificationMediatorService.cs index 873d297bd..2382873da 100644 --- a/src/Core/Abstractions/IUserVerificationMediatorService.cs +++ b/src/Core/Abstractions/IUserVerificationMediatorService.cs @@ -1,14 +1,28 @@ -using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities; +using Bit.Core.Utilities.Fido2; namespace Bit.Core.Abstractions { public interface IUserVerificationMediatorService { - Task VerifyUserForFido2Async(Fido2UserVerificationOptions options); + Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options); Task CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options); Task ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options); - Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync(); - Task<(bool canPerformUnlockWithPin, bool pinVerified)> VerifyPinCodeAsync(); - Task<(bool canPerformUnlockWithMasterPassword, bool mpVerified)> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt); + Task ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options); + Task> PerformOSUnlockAsync(); + Task> VerifyPinCodeAsync(); + Task> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt); + + public struct UVResult + { + public UVResult(bool canPerform, bool isVerified) + { + CanPerform = canPerform; + IsVerified = isVerified; + } + + public bool CanPerform { get; set; } + public bool IsVerified { get; set; } + } } } diff --git a/src/Core/Pages/Accounts/LockPageViewModel.cs b/src/Core/Pages/Accounts/LockPageViewModel.cs index b3bd61015..64d35c4cb 100644 --- a/src/Core/Pages/Accounts/LockPageViewModel.cs +++ b/src/Core/Pages/Accounts/LockPageViewModel.cs @@ -515,7 +515,7 @@ namespace Bit.App.Pages var success = await _platformUtilsService.AuthenticateBiometricAsync(null, PinEnabled ? AppResources.PIN : AppResources.MasterPassword, () => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)), - !PinEnabled && !HasMasterPassword); + !PinEnabled && !HasMasterPassword) ?? false; await _stateService.SetBiometricLockedAsync(!success); if (success) diff --git a/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs b/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs index bc247295b..81c0fb60f 100644 --- a/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs +++ b/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs @@ -370,7 +370,7 @@ namespace Bit.App.Pages if (!_supportsBiometric || - !await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null)) + await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true) { _canUnlockWithBiometrics = false; MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics))); diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 00b42e8cc..cbde94e58 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -74,7 +74,7 @@ namespace Bit.Core.Services if (!userVerified && - await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions( + await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions( cipher.Reprompt != CipherRepromptType.None, makeCredentialParams.UserVerificationPreference, userInterface.HasVaultBeenUnlockedInThisTransaction))) @@ -158,7 +158,7 @@ namespace Bit.Core.Services if (!userVerified && - await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions( + await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions( selectedCipher.Reprompt != CipherRepromptType.None, assertionParams.UserVerificationPreference, userInterface.HasVaultBeenUnlockedInThisTransaction))) @@ -458,19 +458,6 @@ namespace Bit.Core.Services return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence); } - private async Task ShouldEnforceRequiredUserVerificationAsync(Fido2UserVerificationOptions options) - { - switch (options.UserVerificationPreference) - { - case Fido2UserVerificationPreference.Required: - return true; - case Fido2UserVerificationPreference.Discouraged: - return await _userVerificationMediatorService.ShouldPerformMasterPasswordRepromptAsync(options); - default: - return await _userVerificationMediatorService.CanPerformUserVerificationPreferredAsync(options); - } - } - private class PublicKey { private readonly ECDsa _dsa; diff --git a/src/Core/Services/MobilePlatformUtilsService.cs b/src/Core/Services/MobilePlatformUtilsService.cs index ce81f625b..fd80a5ff9 100644 --- a/src/Core/Services/MobilePlatformUtilsService.cs +++ b/src/Core/Services/MobilePlatformUtilsService.cs @@ -238,7 +238,7 @@ namespace Bit.App.Services return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey); } - public async Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, + public async Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false) { try @@ -262,6 +262,10 @@ namespace Bit.App.Services { return true; } + if (result.Status == FingerprintAuthenticationResultStatus.Canceled) + { + return null; + } if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested) { fallback?.Invoke(); diff --git a/src/Core/Services/UserVerification/Fido2UserVerificationPreferredServiceStrategy.cs b/src/Core/Services/UserVerification/Fido2UserVerificationPreferredServiceStrategy.cs index 7f42aafa6..4f4457df6 100644 --- a/src/Core/Services/UserVerification/Fido2UserVerificationPreferredServiceStrategy.cs +++ b/src/Core/Services/UserVerification/Fido2UserVerificationPreferredServiceStrategy.cs @@ -1,4 +1,5 @@ using Bit.Core.Abstractions; +using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; namespace Bit.Core.Services.UserVerification @@ -12,11 +13,11 @@ namespace Bit.Core.Services.UserVerification _userVerificationMediatorService = userVerificationMediatorService; } - public async Task VerifyUserForFido2Async(Fido2UserVerificationOptions options) + public async Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options) { if (options.HasVaultBeenUnlockedInTransaction) { - return true; + return new CancellableResult(true); } if (options.OnNeedUITask != null) @@ -24,13 +25,17 @@ namespace Bit.Core.Services.UserVerification await options.OnNeedUITask(); } - var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync(); - if (canPerformOSUnlock) + var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync(); + if (osUnlockVerification.IsCancelled) { - return isOSUnlocked; + return new CancellableResult(false, true); + } + if (osUnlockVerification.Result.CanPerform) + { + return new CancellableResult(osUnlockVerification.Result.IsVerified); } - return false; + return new CancellableResult(false); } } } diff --git a/src/Core/Services/UserVerification/Fido2UserVerificationRequiredServiceStrategy.cs b/src/Core/Services/UserVerification/Fido2UserVerificationRequiredServiceStrategy.cs index 4aca3fe73..fac385354 100644 --- a/src/Core/Services/UserVerification/Fido2UserVerificationRequiredServiceStrategy.cs +++ b/src/Core/Services/UserVerification/Fido2UserVerificationRequiredServiceStrategy.cs @@ -1,5 +1,6 @@ using Bit.Core.Abstractions; using Bit.Core.Resources.Localization; +using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; namespace Bit.Core.Services.UserVerification @@ -16,11 +17,11 @@ namespace Bit.Core.Services.UserVerification _platformUtilsService = platformUtilsService; } - public async Task VerifyUserForFido2Async(Fido2UserVerificationOptions options) + public async Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options) { if (options.HasVaultBeenUnlockedInTransaction) { - return true; + return new CancellableResult(true); } if (options.OnNeedUITask != null) @@ -28,22 +29,34 @@ namespace Bit.Core.Services.UserVerification await options.OnNeedUITask(); } - var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync(); - if (canPerformOSUnlock) + var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync(); + if (osUnlockVerification.IsCancelled) { - return isOSUnlocked; + return new CancellableResult(false, true); + } + if (osUnlockVerification.Result.CanPerform) + { + return new CancellableResult(osUnlockVerification.Result.IsVerified); } - var (canPerformUnlockWithPin, pinVerified) = await _userVerificationMediatorService.VerifyPinCodeAsync(); - if (canPerformUnlockWithPin) + var pinVerification = await _userVerificationMediatorService.VerifyPinCodeAsync(); + if (pinVerification.IsCancelled) { - return pinVerified; + return new CancellableResult(false, true); + } + if (pinVerification.Result.CanPerform) + { + return new CancellableResult(pinVerification.Result.IsVerified); } - var (canPerformUnlockWithMasterPassword, mpVerified) = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false); - if (canPerformUnlockWithMasterPassword) + var mpVerification = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false); + if (mpVerification.IsCancelled) { - return mpVerified; + return new CancellableResult(false, true); + } + if (mpVerification.Result.CanPerform) + { + return new CancellableResult(mpVerification.Result.IsVerified); } // TODO: Setup PIN code. For the sake of simplicity, we're not implementing this step now and just telling the user to do it in the main app. @@ -52,7 +65,7 @@ namespace Bit.Core.Services.UserVerification string.Format(AppResources.VerificationRequiredByX, options.RpId), AppResources.Ok); - return false; + return new CancellableResult(false); } } } diff --git a/src/Core/Services/UserVerification/IUserVerificationServiceStrategy.cs b/src/Core/Services/UserVerification/IUserVerificationServiceStrategy.cs index f32a295c5..c5cdf5a19 100644 --- a/src/Core/Services/UserVerification/IUserVerificationServiceStrategy.cs +++ b/src/Core/Services/UserVerification/IUserVerificationServiceStrategy.cs @@ -1,9 +1,10 @@ -using Bit.Core.Utilities.Fido2; +using Bit.Core.Utilities; +using Bit.Core.Utilities.Fido2; namespace Bit.Core.Services.UserVerification { public interface IUserVerificationServiceStrategy { - Task VerifyUserForFido2Async(Fido2UserVerificationOptions options); + Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options); } } diff --git a/src/Core/Services/UserVerification/UserVerificationMediatorService.cs b/src/Core/Services/UserVerification/UserVerificationMediatorService.cs index 5864969d1..5b52382fd 100644 --- a/src/Core/Services/UserVerification/UserVerificationMediatorService.cs +++ b/src/Core/Services/UserVerification/UserVerificationMediatorService.cs @@ -2,8 +2,10 @@ using Bit.Core.Abstractions; using Bit.Core.Models.Domain; using Bit.Core.Resources.Localization; +using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; using Plugin.Fingerprint; +using static Bit.Core.Abstractions.IUserVerificationMediatorService; using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability; namespace Bit.Core.Services.UserVerification @@ -37,7 +39,7 @@ namespace Bit.Core.Services.UserVerification _fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this)); } - public async Task VerifyUserForFido2Async(Fido2UserVerificationOptions options) + public async Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options) { if (await ShouldPerformMasterPasswordRepromptAsync(options)) { @@ -46,13 +48,16 @@ namespace Bit.Core.Services.UserVerification await options.OnNeedUITask(); } - var (canPerformMP, mpVerified) = await VerifyMasterPasswordAsync(true); - return canPerformMP && mpVerified; + var mpVerification = await VerifyMasterPasswordAsync(true); + return new CancellableResult( + !mpVerification.IsCancelled && mpVerification.Result.CanPerform && mpVerification.Result.IsVerified, + mpVerification.IsCancelled + ); } if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy)) { - return false; + return new CancellableResult(false, false); } return await userVerificationServiceStrategy.VerifyUserForFido2Async(options); @@ -77,32 +82,53 @@ namespace Bit.Core.Services.UserVerification return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync(); } - public async Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync() + public async Task ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options) + { + switch (options.UserVerificationPreference) + { + case Fido2UserVerificationPreference.Required: + return true; + case Fido2UserVerificationPreference.Discouraged: + return await ShouldPerformMasterPasswordRepromptAsync(options); + default: + return await CanPerformUserVerificationPreferredAsync(options); + } + } + + public async Task> PerformOSUnlockAsync() { var availability = await CrossFingerprint.Current.GetAvailabilityAsync(); if (availability == FingerprintAvailability.Available) { var isValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null); - return (true, isValid); + if (!isValid.HasValue) + { + return new UVResult(false, false).AsCancellable(true); + } + return new UVResult(true, isValid.Value).AsCancellable(); } var alternativeAuthAvailability = await CrossFingerprint.Current.GetAvailabilityAsync(true); if (alternativeAuthAvailability == FingerprintAvailability.Available) { var isNonBioValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null, allowAlternativeAuthentication: true); - return (true, isNonBioValid); + if (!isNonBioValid.HasValue) + { + return new UVResult(false, false).AsCancellable(true); + } + return new UVResult(true, isNonBioValid.Value).AsCancellable(); } - return (false, false); + return new UVResult(false, false).AsCancellable(); } - public async Task<(bool canPerformUnlockWithPin, bool pinVerified)> VerifyPinCodeAsync() + public async Task> VerifyPinCodeAsync() { return await VerifyWithAttemptsAsync(async () => { if (!await _userPinService.IsPinLockEnabledAsync()) { - return (false, false); + return new UVResult(false, false).AsCancellable(); } var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN, @@ -110,59 +136,76 @@ namespace Bit.Core.Services.UserVerification if (pin is null) { // cancelled by the user - return (true, false); + return new UVResult(true, false).AsCancellable(true); } try { var isVerified = await _userPinService.VerifyPinAsync(pin); - return (true, isVerified); + return new UVResult(true, isVerified).AsCancellable(); } catch (SymmetricCryptoKey.ArgumentKeyNullException) { - return (true, false); + return new UVResult(true, false).AsCancellable(); } catch (SymmetricCryptoKey.InvalidKeyOperationException) { - return (true, false); + return new UVResult(true, false).AsCancellable(); } }); } - public async Task<(bool canPerformUnlockWithMasterPassword, bool mpVerified)> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt) + public async Task> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt) { return await VerifyWithAttemptsAsync(async () => { if (!await _userVerificationService.HasMasterPasswordAsync(true)) { - return (false, false); + return new UVResult(false, false).AsCancellable(); } var title = isMasterPasswordReprompt ? AppResources.PasswordConfirmation : AppResources.MasterPassword; var body = isMasterPasswordReprompt ? AppResources.PasswordConfirmationDesc : string.Empty; - var (_, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync); - return (true, isValid); + var (password, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync); + if (password is null) + { + return new UVResult(true, false).AsCancellable(true); + } + + return new UVResult(true, isValid).AsCancellable(); }); } - private async Task<(bool canPerform, bool isVerified)> VerifyWithAttemptsAsync(Func> verifyAsync) + private async Task> VerifyWithAttemptsAsync(Func>> verifyAsync) { byte attempts = 0; do { - var (canPerform, verified) = await verifyAsync(); - if (!canPerform) + var verification = await verifyAsync(); + if (verification.IsCancelled) { - return (false, false); + return new UVResult(false, false).AsCancellable(true); } - if (verified) + if (!verification.Result.CanPerform) { - return (true, true); + return new UVResult(false, false).AsCancellable(); + } + if (verification.Result.IsVerified) + { + return new UVResult(true, true).AsCancellable(); } } while (attempts++ < MAX_ATTEMPTS); - return (true, false); + return new UVResult(true, false).AsCancellable(); + } + } + + public static class UVResultExtensions + { + public static CancellableResult AsCancellable(this UVResult result, bool isCancelled = false) + { + return new CancellableResult(result, isCancelled); } } } diff --git a/src/Core/Utilities/CancellableResult.cs b/src/Core/Utilities/CancellableResult.cs new file mode 100644 index 000000000..74f159b83 --- /dev/null +++ b/src/Core/Utilities/CancellableResult.cs @@ -0,0 +1,15 @@ +namespace Bit.Core.Utilities +{ + public readonly struct CancellableResult + { + public CancellableResult(T result, bool isCancelled = false) + { + Result = result; + IsCancelled = isCancelled; + } + + public T Result { get; } + + public bool IsCancelled { get; } + } +} diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index ecbe0a50b..46761267c 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -264,7 +264,7 @@ namespace Bit.iOS.Autofill var encrypted = await _cipherService.Value.GetAsync(selectedCipherId); var cipher = await encrypted.DecryptAsync(); - return await _userVerificationMediatorService.Value.VerifyUserForFido2Async( + var cResult = await _userVerificationMediatorService.Value.VerifyUserForFido2Async( new Fido2UserVerificationOptions( cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password, userVerificationPreference, @@ -285,8 +285,10 @@ namespace Bit.iOS.Autofill _platformUtilsService.Value.ShowToast(null, null, AppResources.VerifyingIdentityEllipsis); await _conditionedAwaiterManager.Value.GetAwaiterForPrecondition(AwaiterPrecondition.AutofillIOSExtensionViewDidAppear); - }) - ); + } + ) + ); + return !cResult.IsCancelled && cResult.Result; } catch (InvalidOperationNeedsUIException) { diff --git a/src/iOS.Autofill/LoginAddViewController.cs b/src/iOS.Autofill/LoginAddViewController.cs index 214c434ec..33399c2ef 100644 --- a/src/iOS.Autofill/LoginAddViewController.cs +++ b/src/iOS.Autofill/LoginAddViewController.cs @@ -87,7 +87,12 @@ namespace Bit.iOS.Autofill if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged) { - _isUserVerified = await VerifyUserAsync(); + var verification = await VerifyUserAsync(); + if (verification.IsCancelled) + { + return; + } + _isUserVerified = verification.Result; } var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); @@ -109,13 +114,13 @@ namespace Bit.iOS.Autofill } } - private async Task VerifyUserAsync() + private async Task> VerifyUserAsync() { try { if (Context?.PasskeyCreationParams is null) { - return false; + return new CancellableResult(false); } return await _userVerificationMediatorService.Value.VerifyUserForFido2Async( @@ -123,13 +128,14 @@ namespace Bit.iOS.Autofill false, Context.PasskeyCreationParams.Value.UserVerificationPreference, Context.VaultUnlockedDuringThisSession, - Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier) - ); + Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier + ) + ); } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); - return false; + return new CancellableResult(false); } } diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index cb69902a0..37867dc11 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -332,7 +332,18 @@ namespace Bit.iOS.Autofill bool? isUserVerified = null; if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged) { - isUserVerified = await VerifyUserAsync(); + var verification = await VerifyUserAsync(); + if (verification.IsCancelled) + { + return; + } + isUserVerified = verification.Result; + + if (!isUserVerified.Value && await _userVerificationMediatorService.Value.ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions)) + { + await _platformUtilsService.Value.ShowDialogAsync(AppResources.ErrorCreatingPasskey, AppResources.SavePasskey); + return; + } } var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); @@ -350,27 +361,38 @@ namespace Bit.iOS.Autofill } } - private async Task VerifyUserAsync() + private async Task> VerifyUserAsync() { try { if (Context?.PasskeyCreationParams is null) { - return false; + return new CancellableResult(false); } - return await _userVerificationMediatorService.Value.VerifyUserForFido2Async( - new Fido2UserVerificationOptions( - false, - Context.PasskeyCreationParams.Value.UserVerificationPreference, - Context.VaultUnlockedDuringThisSession, - Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier) - ); + return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(Fido2UserVerificationOptions); } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); - return false; + return new CancellableResult(false); + } + } + + private Fido2UserVerificationOptions Fido2UserVerificationOptions + { + get + { + ArgumentNullException.ThrowIfNull(Context); + ArgumentNullException.ThrowIfNull(Context.PasskeyCreationParams); + + return new Fido2UserVerificationOptions + ( + false, + Context.PasskeyCreationParams.Value.UserVerificationPreference, + Context.VaultUnlockedDuringThisSession, + Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier + ); } } diff --git a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs index 31477f403..e8cffc7da 100644 --- a/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs +++ b/src/iOS.Core/Controllers/BaseLockPasswordViewController.cs @@ -424,7 +424,7 @@ namespace Bit.iOS.Core.Controllers var success = await _platformUtilsService.AuthenticateBiometricAsync(null, _pinEnabled ? AppResources.PIN : AppResources.MasterPassword, () => MasterPasswordCell.TextField.BecomeFirstResponder(), - !_pinEnabled && !_hasMasterPassword); + !_pinEnabled && !_hasMasterPassword) ?? false; await _stateService.SetBiometricLockedAsync(!success); if (success)