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) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
throw;
} }
} }
@@ -98,8 +97,7 @@ namespace Bit.iOS.Autofill
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
throw;
} }
} }
@@ -113,10 +111,10 @@ namespace Bit.iOS.Autofill
await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
break; break;
case ASPasskeyCredentialRequest passkeyRequest: case ASPasskeyCredentialRequest passkeyRequest:
await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity); await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest);
break; break;
default: default:
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); CancelRequest(ASExtensionErrorCode.Failed);
break; 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) private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity)
{ {
InitAppIfNeeded(); InitAppIfNeeded();
@@ -161,22 +152,6 @@ namespace Bit.iOS.Autofill
await ProvideCredentialAsync(false); 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) public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
{ {
try try
@@ -187,7 +162,7 @@ namespace Bit.iOS.Autofill
PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
break; break;
case ASPasskeyCredentialRequest passkeyRequest: case ASPasskeyCredentialRequest passkeyRequest:
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialIdentity = passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity); await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest);
break; break;
default: default:
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
@@ -240,8 +215,7 @@ namespace Bit.iOS.Autofill
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
throw;
} }
} }
@@ -282,22 +256,19 @@ namespace Bit.iOS.Autofill
}); });
} }
public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) private void OnProvidingCredentialException(Exception ex)
{ {
if (_context == null) //LoggerHelper.LogEvenIfCantBeResolved(ex);
{ UIPasteboard.General.String = ex.ToString();
ServiceContainer.Reset(); CancelRequest(ASExtensionErrorCode.Failed);
CancelRequest(ASExtensionErrorCode.UserCanceled);
return;
}
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
{
ServiceContainer.Reset();
ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null);
});
} }
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) public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{ {
@@ -337,8 +308,7 @@ namespace Bit.iOS.Autofill
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
throw;
} }
} }
@@ -384,18 +354,10 @@ namespace Bit.iOS.Autofill
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
throw;
} }
} }
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) private async Task ProvideCredentialAsync(bool userInteraction = true)
{ {
try try
@@ -417,7 +379,7 @@ namespace Bit.iOS.Autofill
var decCipher = await cipher.DecryptAsync(); var decCipher = await cipher.DecryptAsync();
if (_context.PasskeyCredentialIdentity != null && !decCipher.Login.HasFido2Credentials) if (!CanProvideCredentialOnPasskeyRequest(decCipher))
{ {
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound); CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
return; return;
@@ -447,16 +409,9 @@ namespace Bit.iOS.Autofill
} }
} }
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && _context.IsPasskey) if (_context.IsPasskey)
{ {
CompleteAssertionRequest(new ASPasskeyAssertionCredential( CompleteAssertionRequest(decCipher);
decCipher.Login.MainFido2Credential.UserHandle,
decCipher.Login.MainFido2Credential.RpId,
"qweq",
"adfas",
"adfas",
decCipher.Login.MainFido2Credential.CredentialId
));
return; return;
} }
@@ -475,8 +430,7 @@ namespace Bit.iOS.Autofill
} }
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); OnProvidingCredentialException(ex);
CancelRequest(ASExtensionErrorCode.Failed);
} }
} }

View File

@@ -10,9 +10,21 @@ namespace Bit.iOS.Autofill.Models
public NSExtensionContext ExtContext { get; set; } public NSExtensionContext ExtContext { get; set; }
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; } public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; } public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get; set; } public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
public bool Configuring { 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 public string? RecordIdentifier
{ {
get 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\MaterialIcons_Regular.ttf" />
<BundleResource Include="Resources\bwi-font.ttf" /> <BundleResource Include="Resources\bwi-font.ttf" />
<Compile Include="CredentialProviderViewController.TapGestureHack.cs" /> <Compile Include="CredentialProviderViewController.TapGestureHack.cs" />
<Compile Include="CredentialProviderViewController.Passkeys.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<BundleResource Include="Resources\check.png" /> <BundleResource Include="Resources\check.png" />