mirror of
https://github.com/bitwarden/mobile
synced 2025-12-05 23:53:33 +00:00
PM-5154 Continue Passkeys Autofill in iOS
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
<BundleResource Include="Resources\MaterialIcons_Regular.ttf" />
|
||||
<BundleResource Include="Resources\bwi-font.ttf" />
|
||||
<Compile Include="CredentialProviderViewController.TapGestureHack.cs" />
|
||||
<Compile Include="CredentialProviderViewController.Passkeys.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\check.png" />
|
||||
|
||||
Reference in New Issue
Block a user