1
0
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:
Federico Maccaroni
2024-01-03 18:22:03 -03:00
parent 275ae76761
commit e3877cc589
4 changed files with 106 additions and 70 deletions

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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" />