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 @@
+