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)
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user