mirror of
https://github.com/bitwarden/mobile
synced 2026-01-09 03:53:15 +00:00
[PM-6466] Implement passkeys User Verification (#3044)
* PM-6441 Implement passkeys User Verification * PM-6441 Reorganized UserVerificationMediatorService so everything is not in the same file * PM-6441 Fix Unit tests * PM-6441 Refactor UserVerification on Fido2Authenticator and Client services to be of an enum type so we can see which specific preference the RP sent and to be passed into the user verification mediator service to perform the correct flow depending on that. Also updated Unit tests. * PM-6441 Changed user verification logic a bit so if preference is Preferred and the app has the ability to verify the user then enforce required UV and fix issue on on Discouraged to take into account MP reprompt
This commit is contained in:
committed by
GitHub
parent
e41abf5003
commit
4292542155
@@ -5,9 +5,11 @@ using AuthenticationServices;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Foundation;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
@@ -19,7 +21,10 @@ namespace Bit.iOS.Autofill
|
||||
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost
|
||||
{
|
||||
private readonly LazyResolve<IFido2AuthenticatorService> _fido2AuthService = new LazyResolve<IFido2AuthenticatorService>();
|
||||
|
||||
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
private readonly LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
|
||||
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
@@ -27,6 +32,8 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
try
|
||||
{
|
||||
switch (registrationRequest?.Type)
|
||||
@@ -68,33 +75,55 @@ namespace Bit.iOS.Autofill
|
||||
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
||||
|
||||
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
||||
|
||||
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||
|
||||
try
|
||||
{
|
||||
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
||||
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
||||
RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required",
|
||||
RequireResidentKey = true,
|
||||
RpEntity = new PublicKeyCredentialRpEntity
|
||||
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
Id = credIdentity.RelyingPartyIdentifier,
|
||||
Name = credIdentity.RelyingPartyIdentifier
|
||||
},
|
||||
UserEntity = new PublicKeyCredentialUserEntity
|
||||
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
||||
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(passkeyRegistrationRequest.UserVerificationPreference),
|
||||
RequireResidentKey = true,
|
||||
RpEntity = new PublicKeyCredentialRpEntity
|
||||
{
|
||||
Id = credIdentity.RelyingPartyIdentifier,
|
||||
Name = credIdentity.RelyingPartyIdentifier
|
||||
},
|
||||
UserEntity = new PublicKeyCredentialUserEntity
|
||||
{
|
||||
Id = credIdentity.UserHandle.ToArray(),
|
||||
Name = credIdentity.UserName,
|
||||
DisplayName = credIdentity.UserName
|
||||
}
|
||||
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync,
|
||||
() => _context.VaultUnlockedDuringThisSession,
|
||||
_context,
|
||||
OnConfirmingNewCredential,
|
||||
VerifyUserAsync));
|
||||
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
|
||||
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
||||
credIdentity.RelyingPartyIdentifier,
|
||||
passkeyRegistrationRequest.ClientDataHash,
|
||||
NSData.FromArray(result.CredentialId),
|
||||
NSData.FromArray(result.AttestationObject)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
Id = credIdentity.UserHandle.ToArray(),
|
||||
Name = credIdentity.UserName,
|
||||
DisplayName = credIdentity.UserName
|
||||
await _platformUtilsService.Value.ShowDialogAsync(
|
||||
string.Format(AppResources.ThereWasAProblemCreatingAPasskeyForXTryAgainLater, credIdentity?.RelyingPartyIdentifier),
|
||||
AppResources.ErrorCreatingPasskey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync, _context, OnConfirmingNewCredential));
|
||||
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
|
||||
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
||||
credIdentity.RelyingPartyIdentifier,
|
||||
passkeyRegistrationRequest.ClientDataHash,
|
||||
NSData.FromArray(result.CredentialId),
|
||||
NSData.FromArray(result.AttestationObject)));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
|
||||
@@ -155,12 +184,11 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Add user verification and remove hardcoding on the user interface "userVerified"
|
||||
var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
RpId = rpId,
|
||||
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
|
||||
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(_context.PasskeyCredentialRequest.UserVerificationPreference),
|
||||
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
|
||||
@@ -168,29 +196,81 @@ namespace Bit.iOS.Autofill
|
||||
Id = credentialIdData.ToArray()
|
||||
}
|
||||
}
|
||||
}, new Fido2GetAssertionUserInterface(cipherId, true, EnsureUnlockedVaultAsync, () => Task.FromResult(true)));
|
||||
}, new Fido2GetAssertionUserInterface(cipherId, false,
|
||||
EnsureUnlockedVaultAsync,
|
||||
() => _context?.VaultUnlockedDuringThisSession ?? false,
|
||||
VerifyUserAsync));
|
||||
|
||||
var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
|
||||
: (NSData)userHandleData;
|
||||
|
||||
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||
: credentialIdData;
|
||||
if (fido2AssertionResult.SelectedCredential is null)
|
||||
{
|
||||
throw new NullReferenceException("SelectedCredential must have a value");
|
||||
}
|
||||
|
||||
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||
selectedUserHandleData,
|
||||
NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle),
|
||||
rpId,
|
||||
NSData.FromArray(fido2AssertionResult.Signature),
|
||||
_context.PasskeyCredentialRequest.ClientDataHash,
|
||||
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
||||
selectedCredentialIdData
|
||||
NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||
));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
catch (InvalidOperationNeedsUIException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_context?.IsExecutingWithoutUserInteraction == false)
|
||||
{
|
||||
await _platformUtilsService.Value.ShowDialogAsync(
|
||||
string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, rpId),
|
||||
AppResources.ErrorReadingPasskey);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
|
||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
||||
userVerificationPreference,
|
||||
_context.VaultUnlockedDuringThisSession,
|
||||
_context.PasskeyCredentialIdentity?.RelyingPartyIdentifier,
|
||||
() =>
|
||||
{
|
||||
if (_context.IsExecutingWithoutUserInteraction)
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
throw new InvalidOperationNeedsUIException();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
catch (InvalidOperationNeedsUIException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
||||
@@ -266,7 +346,7 @@ namespace Bit.iOS.Autofill
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
throw new InvalidOperationException("Not authed or locked");
|
||||
throw new InvalidOperationNeedsUIException("Not authed or locked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
|
||||
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
|
||||
|
||||
_context = new Context
|
||||
{
|
||||
ExtContext = ExtensionContext
|
||||
@@ -109,6 +110,7 @@ namespace Bit.iOS.Autofill
|
||||
try
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
{
|
||||
@@ -153,6 +155,9 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
_context.IsExecutingWithoutUserInteraction = true;
|
||||
|
||||
try
|
||||
{
|
||||
switch (credentialRequest?.Type)
|
||||
@@ -196,6 +201,8 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
try
|
||||
{
|
||||
switch (credentialRequest?.Type)
|
||||
@@ -237,6 +244,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
@@ -326,7 +334,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
if (_context?.IsPasskey == true)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.TrySetCanceled();
|
||||
_context.PickCredentialForFido2CreationTcs?.TrySetCanceled();
|
||||
_context.UnlockVaultTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
@@ -395,6 +403,8 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.VaultUnlockedDuringThisSession = true;
|
||||
|
||||
if (_context.IsCreatingPasskey)
|
||||
{
|
||||
_context.UnlockVaultTcs.SetResult(true);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
@@ -8,30 +9,49 @@ namespace Bit.iOS.Autofill
|
||||
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialUserInterface
|
||||
{
|
||||
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
|
||||
private readonly Context _context;
|
||||
private readonly Action _onConfirmingNewCredential;
|
||||
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
|
||||
|
||||
public Fido2MakeCredentialUserInterface(Func<Task> ensureUnlockedVaultCallback, Context context, Action onConfirmingNewCredential)
|
||||
public Fido2MakeCredentialUserInterface(Func<Task> ensureUnlockedVaultCallback,
|
||||
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||
Context context,
|
||||
Action onConfirmingNewCredential,
|
||||
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
|
||||
{
|
||||
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
|
||||
_context = context;
|
||||
_onConfirmingNewCredential = onConfirmingNewCredential;
|
||||
_verifyUserCallback = verifyUserCallback;
|
||||
}
|
||||
|
||||
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.SetCanceled();
|
||||
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>();
|
||||
_context.PickCredentialForFido2CreationTcs?.SetCanceled();
|
||||
_context.PickCredentialForFido2CreationTcs = new TaskCompletionSource<(string, bool?)>();
|
||||
_context.PasskeyCreationParams = confirmNewCredentialParams;
|
||||
|
||||
_onConfirmingNewCredential();
|
||||
|
||||
return await _context.ConfirmNewCredentialTcs.Task;
|
||||
var (cipherId, isUserVerified) = await _context.PickCredentialForFido2CreationTcs.Task;
|
||||
|
||||
var verified = isUserVerified ?? await _verifyUserCallback(cipherId, confirmNewCredentialParams.UserVerificationPreference);
|
||||
|
||||
return (cipherId, verified);
|
||||
}
|
||||
|
||||
// iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently.
|
||||
public Task InformExcludedCredentialAsync(string[] existingCipherIds) => Task.CompletedTask;
|
||||
|
||||
public Task EnsureUnlockedVaultAsync() => _ensureUnlockedVaultCallback();
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
await _ensureUnlockedVaultCallback();
|
||||
|
||||
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
@@ -16,6 +18,7 @@ namespace Bit.iOS.Autofill
|
||||
public partial class LoginAddViewController : Core.Controllers.LoginAddViewController
|
||||
{
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
|
||||
public LoginAddViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
@@ -32,11 +35,13 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private new Context Context => (Context)base.Context;
|
||||
|
||||
private bool? _isUserVerified;
|
||||
|
||||
public override Action<string> Success => cipherId =>
|
||||
{
|
||||
if (IsCreatingPasskey)
|
||||
{
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true));
|
||||
Context.PickCredentialForFido2CreationTcs.TrySetResult((cipherId, _isUserVerified));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,10 +81,15 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
_isUserVerified = await VerifyUserAsync();
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
try
|
||||
{
|
||||
@@ -99,6 +109,30 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context?.PasskeyCreationParams is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
false,
|
||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
||||
Context.VaultUnlockedDuringThisSession,
|
||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async partial void SaveBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
await SaveAsync();
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.ListItems;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
@@ -17,7 +18,6 @@ using CoreFoundation;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
@@ -44,6 +44,7 @@ namespace Bit.iOS.Autofill
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
|
||||
bool _alreadyLoadItemsOnce = false;
|
||||
|
||||
@@ -163,24 +164,29 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context.PasskeyCreationParams is null)
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params."));
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
bool? isUserVerified = null;
|
||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
isUserVerified = await VerifyUserAsync();
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
try
|
||||
{
|
||||
PresentViewController(loadingAlert, true, null);
|
||||
|
||||
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCreationParams.Value);
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true));
|
||||
Context.PickCredentialForFido2CreationTcs.TrySetResult((cipherId, isUserVerified));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -189,6 +195,30 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context?.PasskeyCreationParams is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
false,
|
||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
||||
Context.VaultUnlockedDuringThisSession,
|
||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
{
|
||||
if (segue.DestinationViewController is UINavigationController navController)
|
||||
|
||||
@@ -652,7 +652,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="12574"/>
|
||||
<segue reference="12959"/>
|
||||
<segue reference="3731"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
|
||||
@@ -15,11 +15,21 @@ namespace Bit.iOS.Autofill.Models
|
||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||
public bool Configuring { get; set; }
|
||||
public bool IsExecutingWithoutUserInteraction { get; set; }
|
||||
|
||||
public bool IsCreatingPasskey { get; set; }
|
||||
public Fido2ConfirmNewCredentialParams? PasskeyCreationParams { get; set; }
|
||||
/// <summary>
|
||||
/// This is used to defer the completion until the vault is unlocked.
|
||||
/// </summary>
|
||||
public TaskCompletionSource<bool> UnlockVaultTcs { get; set; }
|
||||
public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; }
|
||||
/// <summary>
|
||||
/// This is used to defer the completion until a vault item is chosen to add the passkey to.
|
||||
/// Param: cipher ID to add the passkey to.
|
||||
/// Param: isUserVerified if the user was verified. If null then the verification hasn't been done.
|
||||
/// </summary>
|
||||
public TaskCompletionSource<(string cipherId, bool? isUserVerified)> PickCredentialForFido2CreationTcs { get; set; }
|
||||
public bool VaultUnlockedDuringThisSession { get; set; }
|
||||
|
||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||
{
|
||||
|
||||
@@ -78,9 +78,7 @@ namespace Bit.iOS.Autofill.Utilities
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check user verification
|
||||
|
||||
Context.ConfirmNewCredentialTcs.SetResult((item.Id, true));
|
||||
Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.iOS.Autofill.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom exception to be thrown when we need UI from the extension.
|
||||
/// This is likely to be thrown when initiating on "...WithoutUserInteraction(...)" and doing some logic that
|
||||
/// requires user interaction.
|
||||
/// </summary>
|
||||
public class InvalidOperationNeedsUIException : InvalidOperationException
|
||||
{
|
||||
public InvalidOperationNeedsUIException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidOperationNeedsUIException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidOperationNeedsUIException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,7 @@
|
||||
<Compile Include="Utilities\BaseLoginListTableSource.cs" />
|
||||
<Compile Include="ILoginListViewController.cs" />
|
||||
<Compile Include="Fido2MakeCredentialUserInterface.cs" />
|
||||
<Compile Include="Utilities\InvalidOperationNeedsUIException.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\check.png" />
|
||||
|
||||
Reference in New Issue
Block a user