mirror of
https://github.com/bitwarden/mobile
synced 2025-12-10 21:33:36 +00:00
[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
This commit is contained in:
committed by
GitHub
parent
6bec0ede05
commit
ebc068d820
@@ -26,7 +26,7 @@ namespace Bit.Core.Abstractions
|
|||||||
bool SupportsDuo();
|
bool SupportsDuo();
|
||||||
Task<bool> SupportsBiometricAsync();
|
Task<bool> SupportsBiometricAsync();
|
||||||
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
||||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
|
Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
|
||||||
long GetActiveTime();
|
long GetActiveTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Core.Abstractions
|
namespace Bit.Core.Abstractions
|
||||||
{
|
{
|
||||||
public interface IUserVerificationMediatorService
|
public interface IUserVerificationMediatorService
|
||||||
{
|
{
|
||||||
Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||||
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
|
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
|
||||||
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
|
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
|
||||||
Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync();
|
Task<bool> ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options);
|
||||||
Task<(bool canPerformUnlockWithPin, bool pinVerified)> VerifyPinCodeAsync();
|
Task<CancellableResult<UVResult>> PerformOSUnlockAsync();
|
||||||
Task<(bool canPerformUnlockWithMasterPassword, bool mpVerified)> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt);
|
Task<CancellableResult<UVResult>> VerifyPinCodeAsync();
|
||||||
|
Task<CancellableResult<UVResult>> 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; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ namespace Bit.App.Pages
|
|||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
|
||||||
!PinEnabled && !HasMasterPassword);
|
!PinEnabled && !HasMasterPassword) ?? false;
|
||||||
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
if (success)
|
if (success)
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ namespace Bit.App.Pages
|
|||||||
|
|
||||||
if (!_supportsBiometric
|
if (!_supportsBiometric
|
||||||
||
|
||
|
||||||
!await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null))
|
await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
|
||||||
{
|
{
|
||||||
_canUnlockWithBiometrics = false;
|
_canUnlockWithBiometrics = false;
|
||||||
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if (!userVerified
|
if (!userVerified
|
||||||
&&
|
&&
|
||||||
await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||||
cipher.Reprompt != CipherRepromptType.None,
|
cipher.Reprompt != CipherRepromptType.None,
|
||||||
makeCredentialParams.UserVerificationPreference,
|
makeCredentialParams.UserVerificationPreference,
|
||||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||||
@@ -158,7 +158,7 @@ namespace Bit.Core.Services
|
|||||||
|
|
||||||
if (!userVerified
|
if (!userVerified
|
||||||
&&
|
&&
|
||||||
await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||||
selectedCipher.Reprompt != CipherRepromptType.None,
|
selectedCipher.Reprompt != CipherRepromptType.None,
|
||||||
assertionParams.UserVerificationPreference,
|
assertionParams.UserVerificationPreference,
|
||||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||||
@@ -458,19 +458,6 @@ namespace Bit.Core.Services
|
|||||||
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> 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 class PublicKey
|
||||||
{
|
{
|
||||||
private readonly ECDsa _dsa;
|
private readonly ECDsa _dsa;
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ namespace Bit.App.Services
|
|||||||
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
return await stateService.IsAccountBiometricIntegrityValidAsync(bioIntegritySrcKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
public async Task<bool?> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
||||||
Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false)
|
Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -262,6 +262,10 @@ namespace Bit.App.Services
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (result.Status == FingerprintAuthenticationResultStatus.Canceled)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
if (result.Status == FingerprintAuthenticationResultStatus.FallbackRequested)
|
||||||
{
|
{
|
||||||
fallback?.Invoke();
|
fallback?.Invoke();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Core.Services.UserVerification
|
namespace Bit.Core.Services.UserVerification
|
||||||
@@ -12,11 +13,11 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
_userVerificationMediatorService = userVerificationMediatorService;
|
_userVerificationMediatorService = userVerificationMediatorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
{
|
{
|
||||||
if (options.HasVaultBeenUnlockedInTransaction)
|
if (options.HasVaultBeenUnlockedInTransaction)
|
||||||
{
|
{
|
||||||
return true;
|
return new CancellableResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.OnNeedUITask != null)
|
if (options.OnNeedUITask != null)
|
||||||
@@ -24,13 +25,17 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
await options.OnNeedUITask();
|
await options.OnNeedUITask();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||||
if (canPerformOSUnlock)
|
if (osUnlockVerification.IsCancelled)
|
||||||
{
|
{
|
||||||
return isOSUnlocked;
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (osUnlockVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return new CancellableResult<bool>(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Core.Services.UserVerification
|
namespace Bit.Core.Services.UserVerification
|
||||||
@@ -16,11 +17,11 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
_platformUtilsService = platformUtilsService;
|
_platformUtilsService = platformUtilsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
{
|
{
|
||||||
if (options.HasVaultBeenUnlockedInTransaction)
|
if (options.HasVaultBeenUnlockedInTransaction)
|
||||||
{
|
{
|
||||||
return true;
|
return new CancellableResult<bool>(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.OnNeedUITask != null)
|
if (options.OnNeedUITask != null)
|
||||||
@@ -28,22 +29,34 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
await options.OnNeedUITask();
|
await options.OnNeedUITask();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
var osUnlockVerification = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||||
if (canPerformOSUnlock)
|
if (osUnlockVerification.IsCancelled)
|
||||||
{
|
{
|
||||||
return isOSUnlocked;
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (osUnlockVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(osUnlockVerification.Result.IsVerified);
|
||||||
}
|
}
|
||||||
|
|
||||||
var (canPerformUnlockWithPin, pinVerified) = await _userVerificationMediatorService.VerifyPinCodeAsync();
|
var pinVerification = await _userVerificationMediatorService.VerifyPinCodeAsync();
|
||||||
if (canPerformUnlockWithPin)
|
if (pinVerification.IsCancelled)
|
||||||
{
|
{
|
||||||
return pinVerified;
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (pinVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(pinVerification.Result.IsVerified);
|
||||||
}
|
}
|
||||||
|
|
||||||
var (canPerformUnlockWithMasterPassword, mpVerified) = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false);
|
var mpVerification = await _userVerificationMediatorService.VerifyMasterPasswordAsync(false);
|
||||||
if (canPerformUnlockWithMasterPassword)
|
if (mpVerification.IsCancelled)
|
||||||
{
|
{
|
||||||
return mpVerified;
|
return new CancellableResult<bool>(false, true);
|
||||||
|
}
|
||||||
|
if (mpVerification.Result.CanPerform)
|
||||||
|
{
|
||||||
|
return new CancellableResult<bool>(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.
|
// 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),
|
string.Format(AppResources.VerificationRequiredByX, options.RpId),
|
||||||
AppResources.Ok);
|
AppResources.Ok);
|
||||||
|
|
||||||
return false;
|
return new CancellableResult<bool>(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Core.Utilities.Fido2;
|
||||||
|
|
||||||
namespace Bit.Core.Services.UserVerification
|
namespace Bit.Core.Services.UserVerification
|
||||||
{
|
{
|
||||||
public interface IUserVerificationServiceStrategy
|
public interface IUserVerificationServiceStrategy
|
||||||
{
|
{
|
||||||
Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
using Bit.Core.Resources.Localization;
|
using Bit.Core.Resources.Localization;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Utilities.Fido2;
|
using Bit.Core.Utilities.Fido2;
|
||||||
using Plugin.Fingerprint;
|
using Plugin.Fingerprint;
|
||||||
|
using static Bit.Core.Abstractions.IUserVerificationMediatorService;
|
||||||
using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability;
|
using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability;
|
||||||
|
|
||||||
namespace Bit.Core.Services.UserVerification
|
namespace Bit.Core.Services.UserVerification
|
||||||
@@ -37,7 +39,7 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this));
|
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
public async Task<CancellableResult<bool>> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||||
{
|
{
|
||||||
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
||||||
{
|
{
|
||||||
@@ -46,13 +48,16 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
await options.OnNeedUITask();
|
await options.OnNeedUITask();
|
||||||
}
|
}
|
||||||
|
|
||||||
var (canPerformMP, mpVerified) = await VerifyMasterPasswordAsync(true);
|
var mpVerification = await VerifyMasterPasswordAsync(true);
|
||||||
return canPerformMP && mpVerified;
|
return new CancellableResult<bool>(
|
||||||
|
!mpVerification.IsCancelled && mpVerification.Result.CanPerform && mpVerification.Result.IsVerified,
|
||||||
|
mpVerification.IsCancelled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy))
|
if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy))
|
||||||
{
|
{
|
||||||
return false;
|
return new CancellableResult<bool>(false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await userVerificationServiceStrategy.VerifyUserForFido2Async(options);
|
return await userVerificationServiceStrategy.VerifyUserForFido2Async(options);
|
||||||
@@ -77,32 +82,53 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync();
|
return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync()
|
public async Task<bool> 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<CancellableResult<UVResult>> PerformOSUnlockAsync()
|
||||||
{
|
{
|
||||||
var availability = await CrossFingerprint.Current.GetAvailabilityAsync();
|
var availability = await CrossFingerprint.Current.GetAvailabilityAsync();
|
||||||
if (availability == FingerprintAvailability.Available)
|
if (availability == FingerprintAvailability.Available)
|
||||||
{
|
{
|
||||||
var isValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null);
|
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);
|
var alternativeAuthAvailability = await CrossFingerprint.Current.GetAvailabilityAsync(true);
|
||||||
if (alternativeAuthAvailability == FingerprintAvailability.Available)
|
if (alternativeAuthAvailability == FingerprintAvailability.Available)
|
||||||
{
|
{
|
||||||
var isNonBioValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null, allowAlternativeAuthentication: true);
|
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<CancellableResult<UVResult>> VerifyPinCodeAsync()
|
||||||
{
|
{
|
||||||
return await VerifyWithAttemptsAsync(async () =>
|
return await VerifyWithAttemptsAsync(async () =>
|
||||||
{
|
{
|
||||||
if (!await _userPinService.IsPinLockEnabledAsync())
|
if (!await _userPinService.IsPinLockEnabledAsync())
|
||||||
{
|
{
|
||||||
return (false, false);
|
return new UVResult(false, false).AsCancellable();
|
||||||
}
|
}
|
||||||
|
|
||||||
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||||
@@ -110,59 +136,76 @@ namespace Bit.Core.Services.UserVerification
|
|||||||
if (pin is null)
|
if (pin is null)
|
||||||
{
|
{
|
||||||
// cancelled by the user
|
// cancelled by the user
|
||||||
return (true, false);
|
return new UVResult(true, false).AsCancellable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var isVerified = await _userPinService.VerifyPinAsync(pin);
|
var isVerified = await _userPinService.VerifyPinAsync(pin);
|
||||||
return (true, isVerified);
|
return new UVResult(true, isVerified).AsCancellable();
|
||||||
}
|
}
|
||||||
catch (SymmetricCryptoKey.ArgumentKeyNullException)
|
catch (SymmetricCryptoKey.ArgumentKeyNullException)
|
||||||
{
|
{
|
||||||
return (true, false);
|
return new UVResult(true, false).AsCancellable();
|
||||||
}
|
}
|
||||||
catch (SymmetricCryptoKey.InvalidKeyOperationException)
|
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<CancellableResult<UVResult>> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt)
|
||||||
{
|
{
|
||||||
return await VerifyWithAttemptsAsync(async () =>
|
return await VerifyWithAttemptsAsync(async () =>
|
||||||
{
|
{
|
||||||
if (!await _userVerificationService.HasMasterPasswordAsync(true))
|
if (!await _userVerificationService.HasMasterPasswordAsync(true))
|
||||||
{
|
{
|
||||||
return (false, false);
|
return new UVResult(false, false).AsCancellable();
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = isMasterPasswordReprompt ? AppResources.PasswordConfirmation : AppResources.MasterPassword;
|
var title = isMasterPasswordReprompt ? AppResources.PasswordConfirmation : AppResources.MasterPassword;
|
||||||
var body = isMasterPasswordReprompt ? AppResources.PasswordConfirmationDesc : string.Empty;
|
var body = isMasterPasswordReprompt ? AppResources.PasswordConfirmationDesc : string.Empty;
|
||||||
|
|
||||||
var (_, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync);
|
var (password, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(title, body, _userVerificationService.VerifyMasterPasswordAsync);
|
||||||
return (true, isValid);
|
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<Task<(bool canPerform, bool isVerified)>> verifyAsync)
|
private async Task<CancellableResult<UVResult>> VerifyWithAttemptsAsync(Func<Task<CancellableResult<UVResult>>> verifyAsync)
|
||||||
{
|
{
|
||||||
byte attempts = 0;
|
byte attempts = 0;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var (canPerform, verified) = await verifyAsync();
|
var verification = await verifyAsync();
|
||||||
if (!canPerform)
|
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);
|
} while (attempts++ < MAX_ATTEMPTS);
|
||||||
|
|
||||||
return (true, false);
|
return new UVResult(true, false).AsCancellable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UVResultExtensions
|
||||||
|
{
|
||||||
|
public static CancellableResult<UVResult> AsCancellable(this UVResult result, bool isCancelled = false)
|
||||||
|
{
|
||||||
|
return new CancellableResult<UVResult>(result, isCancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
src/Core/Utilities/CancellableResult.cs
Normal file
15
src/Core/Utilities/CancellableResult.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Bit.Core.Utilities
|
||||||
|
{
|
||||||
|
public readonly struct CancellableResult<T>
|
||||||
|
{
|
||||||
|
public CancellableResult(T result, bool isCancelled = false)
|
||||||
|
{
|
||||||
|
Result = result;
|
||||||
|
IsCancelled = isCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Result { get; }
|
||||||
|
|
||||||
|
public bool IsCancelled { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -264,7 +264,7 @@ namespace Bit.iOS.Autofill
|
|||||||
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
||||||
var cipher = await encrypted.DecryptAsync();
|
var cipher = await encrypted.DecryptAsync();
|
||||||
|
|
||||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
var cResult = await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||||
new Fido2UserVerificationOptions(
|
new Fido2UserVerificationOptions(
|
||||||
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
||||||
userVerificationPreference,
|
userVerificationPreference,
|
||||||
@@ -285,8 +285,10 @@ namespace Bit.iOS.Autofill
|
|||||||
_platformUtilsService.Value.ShowToast(null, null, AppResources.VerifyingIdentityEllipsis);
|
_platformUtilsService.Value.ShowToast(null, null, AppResources.VerifyingIdentityEllipsis);
|
||||||
|
|
||||||
await _conditionedAwaiterManager.Value.GetAwaiterForPrecondition(AwaiterPrecondition.AutofillIOSExtensionViewDidAppear);
|
await _conditionedAwaiterManager.Value.GetAwaiterForPrecondition(AwaiterPrecondition.AutofillIOSExtensionViewDidAppear);
|
||||||
})
|
}
|
||||||
);
|
)
|
||||||
|
);
|
||||||
|
return !cResult.IsCancelled && cResult.Result;
|
||||||
}
|
}
|
||||||
catch (InvalidOperationNeedsUIException)
|
catch (InvalidOperationNeedsUIException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,7 +87,12 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
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);
|
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||||
@@ -109,13 +114,13 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> VerifyUserAsync()
|
private async Task<CancellableResult<bool>> VerifyUserAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Context?.PasskeyCreationParams is null)
|
if (Context?.PasskeyCreationParams is null)
|
||||||
{
|
{
|
||||||
return false;
|
return new CancellableResult<bool>(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||||
@@ -123,13 +128,14 @@ namespace Bit.iOS.Autofill
|
|||||||
false,
|
false,
|
||||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
||||||
Context.VaultUnlockedDuringThisSession,
|
Context.VaultUnlockedDuringThisSession,
|
||||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier
|
||||||
);
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
return false;
|
return new CancellableResult<bool>(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -332,7 +332,18 @@ namespace Bit.iOS.Autofill
|
|||||||
bool? isUserVerified = null;
|
bool? isUserVerified = null;
|
||||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
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);
|
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||||
@@ -350,27 +361,38 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> VerifyUserAsync()
|
private async Task<CancellableResult<bool>> VerifyUserAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Context?.PasskeyCreationParams is null)
|
if (Context?.PasskeyCreationParams is null)
|
||||||
{
|
{
|
||||||
return false;
|
return new CancellableResult<bool>(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(Fido2UserVerificationOptions);
|
||||||
new Fido2UserVerificationOptions(
|
|
||||||
false,
|
|
||||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
|
||||||
Context.VaultUnlockedDuringThisSession,
|
|
||||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
return false;
|
return new CancellableResult<bool>(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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -424,7 +424,7 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
|
||||||
_pinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
_pinEnabled ? AppResources.PIN : AppResources.MasterPassword,
|
||||||
() => MasterPasswordCell.TextField.BecomeFirstResponder(),
|
() => MasterPasswordCell.TextField.BecomeFirstResponder(),
|
||||||
!_pinEnabled && !_hasMasterPassword);
|
!_pinEnabled && !_hasMasterPassword) ?? false;
|
||||||
|
|
||||||
await _stateService.SetBiometricLockedAsync(!success);
|
await _stateService.SetBiometricLockedAsync(!success);
|
||||||
if (success)
|
if (success)
|
||||||
|
|||||||
Reference in New Issue
Block a user