From da051b8fc15a550f994852677b9f80164663d24b Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Mon, 19 Feb 2024 16:44:15 +0100 Subject: [PATCH] Add example of delegated UI class --- .../Fido2/Fido2DelegatedUserInterface.cs | 65 +++++++++++++++++++ ...edentialProviderViewController.Passkeys.cs | 22 +++++-- .../CredentialProviderViewController.cs | 7 ++ 3 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs diff --git a/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs b/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs new file mode 100644 index 000000000..25be4ae20 --- /dev/null +++ b/src/Core/Utilities/Fido2/Fido2DelegatedUserInterface.cs @@ -0,0 +1,65 @@ +using Bit.Core.Abstractions; + +namespace Bit.Core.Utilities.Fido2 +{ + /// + /// This implementation is used when all interactions are delegated to the operating system. + /// Most often these decisions have already been made by the time the Authenticator is called. + /// + /// This is only supported for assertion operations. Attestation requires the user to interact + /// with the app directly. + /// + public class Fido2DelegatedUserInterface : IFido2UserInterface + { + private string _cipherId = null; + private bool _userVerified = false; + private Func _ensureUnlockedVaultAsyncCallback; + + /// + /// Indicates that the user has already picked a credential from a list of existing credentials. + /// Picking a credential also assumes user presence. + /// + public Fido2DelegatedUserInterface UserPickedCredential(string cipherId) + { + _cipherId = cipherId; + return this; + } + + /// + /// Indicates that the user was verified by the OS, e.g. by a fingerprint or face scan. + /// + public Fido2DelegatedUserInterface UserIsVerified() + { + _userVerified = true; + return this; + } + + public Fido2DelegatedUserInterface WithEnsureUnlockedVaultAsyncCallback(Func callback) + { + _ensureUnlockedVaultAsyncCallback = callback; + return this; + } + + public Task PickCredentialAsync(Fido2PickCredentialParams parameters) + { + return Task.FromResult(new Fido2PickCredentialResult + { + CipherId = _cipherId, + UserVerified = _userVerified + }); + } + + public Task EnsureUnlockedVaultAsync() + { + if (_ensureUnlockedVaultAsyncCallback != null) + { + return _ensureUnlockedVaultAsyncCallback(); + } + + throw new Exception("No callback provided to ensure the vault is unlocked"); + } + + public Task InformExcludedCredential(string[] existingCipherIds) => throw new NotImplementedException(); + public Task ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams parameters) => throw new NotImplementedException(); + } +} diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index b51827c07..d0bb228c0 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -21,6 +21,8 @@ namespace Bit.iOS.Autofill { private readonly LazyResolve _cipherService = new LazyResolve(); + private readonly Fido2DelegatedUserInterface _userInterface = new Fido2DelegatedUserInterface(); + private IFido2AuthenticatorService _fido2AuthService; private IFido2AuthenticatorService Fido2AuthService { @@ -29,7 +31,7 @@ namespace Bit.iOS.Autofill if (_fido2AuthService is null) { _fido2AuthService = ServiceContainer.Resolve(); - _fido2AuthService.Init(this); + _fido2AuthService.Init(_userInterface); } return _fido2AuthService; } @@ -159,18 +161,20 @@ namespace Bit.iOS.Autofill }).ToArray(); } + public class UserInteractionRequiredException : Exception {} + private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest) { InitAppIfNeeded(); await _stateService.Value.SetPasswordRepromptAutofillAsync(false); await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); - if (!await IsAuthed() || await IsLocked()) - { - CancelRequest(ASExtensionErrorCode.UserInteractionRequired); - return; - } _context.PasskeyCredentialRequest = passkeyCredentialRequest; - await ProvideCredentialAsync(false); + + try { + await ProvideCredentialAsync(false); + } catch (UserInteractionRequiredException) { + CancelRequest(ASExtensionErrorCode.UserInteractionRequired); + } } public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId) @@ -209,6 +213,10 @@ namespace Bit.iOS.Autofill } }); + _userInterface.UserPickedCredential(cipherId); + // if (os.PerformedFaceID) { + _userInterface.UserIsVerified(); + //} ClipLogger.Log("fido2AssertionResult:" + fido2AssertionResult); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index c5e98f167..4a20e405d 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -506,6 +506,13 @@ namespace Bit.iOS.Autofill private async Task ProvideCredentialAsync(bool userInteraction = true) { + _userInterface.WithEnsureUnlockedVaultAsyncCallback(async () => { + if (!userInteraction && (!await IsAuthed() || await IsLocked())) { + throw new UserInteractionRequiredException(); + } + + await EnsureUnlockedVaultAsync(); + }); try { ClipLogger.Log("ProvideCredentialAsync");