diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs new file mode 100644 index 000000000..bcb40035a --- /dev/null +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using AuthenticationServices; +using Bit.App.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Utilities; +using Foundation; +using UIKit; + +namespace Bit.iOS.Autofill +{ + public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost + { + 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); + } + + public void CompleteAssertionRequest(CipherView cipherView) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request before iOS 17")); + return; + } + + // TODO: Generate the credential Signature and Auth data accordingly + CompleteAssertionRequest(new ASPasskeyAssertionCredential( + cipherView.Login.MainFido2Credential.UserHandle, + cipherView.Login.MainFido2Credential.RpId, + "TODO: Generate Signature", + _context.PasskeyCredentialRequest?.ClientDataHash, + "TODO: Generate Authenticator Data", + cipherView.Login.MainFido2Credential.CredentialId + )); + } + + public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) + { + if (_context == null) + { + ServiceContainer.Reset(); + CancelRequest(ASExtensionErrorCode.UserCanceled); + return; + } + + NSRunLoop.Main.BeginInvokeOnMainThread(() => + { + ServiceContainer.Reset(); + ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null); + }); + } + + private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView) + { + return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials; + } + } +} + diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index c15b62561..a315feb78 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -56,8 +56,7 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + OnProvidingCredentialException(ex); } } @@ -98,8 +97,7 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + OnProvidingCredentialException(ex); } } @@ -113,10 +111,10 @@ namespace Bit.iOS.Autofill await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); break; case ASPasskeyCredentialRequest passkeyRequest: - await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity); + await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest); break; default: - ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); + CancelRequest(ASExtensionErrorCode.Failed); break; } } @@ -138,13 +136,6 @@ namespace Bit.iOS.Autofill } } - private void OnProvidingCredentialException(Exception ex) - { - //LoggerHelper.LogEvenIfCantBeResolved(ex); - UIPasteboard.General.String = ex.ToString(); - ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); - } - private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity) { InitAppIfNeeded(); @@ -161,22 +152,6 @@ namespace Bit.iOS.Autofill await ProvideCredentialAsync(false); } - private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialIdentity passkeyIdentity) - { - InitAppIfNeeded(); - await _stateService.Value.SetPasswordRepromptAutofillAsync(false); - await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); - if (!await IsAuthed() || await IsLocked()) - { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); - ExtensionContext.CancelRequest(err); - return; - } - _context.PasskeyCredentialIdentity = passkeyIdentity; - await ProvideCredentialAsync(false); - } - public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest) { try @@ -187,7 +162,7 @@ namespace Bit.iOS.Autofill PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); break; case ASPasskeyCredentialRequest passkeyRequest: - await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialIdentity = passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest); break; default: ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); @@ -240,8 +215,7 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + OnProvidingCredentialException(ex); } } @@ -282,22 +256,19 @@ namespace Bit.iOS.Autofill }); } - public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) + private void OnProvidingCredentialException(Exception ex) { - if (_context == null) - { - ServiceContainer.Reset(); - CancelRequest(ASExtensionErrorCode.UserCanceled); - return; - } - - NSRunLoop.Main.BeginInvokeOnMainThread(() => - { - ServiceContainer.Reset(); - ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null); - }); + //LoggerHelper.LogEvenIfCantBeResolved(ex); + UIPasteboard.General.String = ex.ToString(); + CancelRequest(ASExtensionErrorCode.Failed); } + private void CancelRequest(ASExtensionErrorCode code) + { + //var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null); + var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code); + ExtensionContext?.CancelRequest(err); + } public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) { @@ -337,8 +308,7 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + OnProvidingCredentialException(ex); } } @@ -384,18 +354,10 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + OnProvidingCredentialException(ex); } } - private void CancelRequest(ASExtensionErrorCode code) - { - //var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null); - var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code); - ExtensionContext?.CancelRequest(err); - } - private async Task ProvideCredentialAsync(bool userInteraction = true) { try @@ -417,7 +379,7 @@ namespace Bit.iOS.Autofill var decCipher = await cipher.DecryptAsync(); - if (_context.PasskeyCredentialIdentity != null && !decCipher.Login.HasFido2Credentials) + if (!CanProvideCredentialOnPasskeyRequest(decCipher)) { CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound); return; @@ -447,16 +409,9 @@ namespace Bit.iOS.Autofill } } - if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && _context.IsPasskey) + if (_context.IsPasskey) { - CompleteAssertionRequest(new ASPasskeyAssertionCredential( - decCipher.Login.MainFido2Credential.UserHandle, - decCipher.Login.MainFido2Credential.RpId, - "qweq", - "adfas", - "adfas", - decCipher.Login.MainFido2Credential.CredentialId - )); + CompleteAssertionRequest(decCipher); return; } @@ -475,8 +430,7 @@ namespace Bit.iOS.Autofill } catch (Exception ex) { - LoggerHelper.LogEvenIfCantBeResolved(ex); - CancelRequest(ASExtensionErrorCode.Failed); + OnProvidingCredentialException(ex); } } diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index 005a235ee..a7b6212eb 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -10,9 +10,21 @@ namespace Bit.iOS.Autofill.Models public NSExtensionContext ExtContext { get; set; } public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; } public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; } - public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get; set; } + public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; } public bool Configuring { get; set; } + public ASPasskeyCredentialIdentity PasskeyCredentialIdentity + { + get + { + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return PasskeyCredentialRequest?.CredentialIdentity as ASPasskeyCredentialIdentity; + } + return null; + } + } + public string? RecordIdentifier { get @@ -31,6 +43,6 @@ namespace Bit.iOS.Autofill.Models } } - public bool IsPasskey => PasskeyCredentialIdentity != null; + public bool IsPasskey => PasskeyCredentialRequest != null; } } diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj index 5a988c2b9..99080d832 100644 --- a/src/iOS.Autofill/iOS.Autofill.csproj +++ b/src/iOS.Autofill/iOS.Autofill.csproj @@ -84,6 +84,7 @@ +