1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-17 00:33:20 +00:00

[PM-5154] Implement Passkeys on iOS (#3017)

* [PM-5731] feat: implement get assertion params object

* [PM-5731] feat: add first test

* [PM-5731] feat: add rp mismatch test

* [PM-5731] feat: ask for credentials when found

* [PM-5731] feat: find discoverable credentials

* [PM-5731] feat: add tests for successful UV requests

* [PM-5731] feat: add user does not consent test

* [PM-5731] feat: check for UV when reprompt is active

* [PM-5731] fix: tests a bit, needed some additional "arrange" steps

* [PM-5731] feat: add support for counter

* [PM-5731] feat: implement assertion without signature

* [PM-5732] feat: finish authenticator assertion implementation

note: CryptoFunctionService still needs Sign implemenation

* [PM-5731] chore: minor clean up

* [PM-5731] feat: scaffold make credential

* [PM-5731] feat: start implementing attestation

* [PM-5731] feat: implement credential exclusion

* [PM-5731] feat: add new credential confirmaiton

* [PM-5731] feat: implement credential creation

* [PM-5731] feat: add user verification checks

* [PM-5731] feat: add unknown error handling

* [PM-5731] chore: clean up unusued params

* [PM-5731] feat: partial attestation implementation

* [PM-5731] feat: implement key generation

* [PM-5731] feat: return public key in DER format

* [PM-5731] feat: implement signing

* [PM-5731] feat: remove logging

* [PM-5731] chore: use primary constructor

* [PM-5731] chore: add Async to method names

* [PM-5731] feat: add support for silent discoverability

* [PM-5731] feat: add support for specifying user presence requirement

* [PM-5731] feat: ensure unlocked vault

* [PM-5731] chore: clean up and refactor assertion tests

* [PM-5731] chore: clean up and refactor attestation tests

* [PM-5731] chore: add user presence todo comment

* [PM-5731] feat: scaffold fido2 client

* PM-5731 Fix build updating discoverable flag

* [PM-5731] fix: failing test

* [PM-5731] feat: add sameOriginWithAncestor and user id length checks

* [PM-5731] feat: add incomplete rpId verification

* [PM-5731] chore: document uri helpers

* [PM-5731] feat: implement fido2 client createCredential

* Added iOS passkeys integration, warning this branch has lots of logs to ease "debugging" extensions.

* [PM-5731] feat: implement credential assertion in client

* PM-5154 Fixed select passkey flow and started implementing create passkey on iOS

* fix wrong signature format

* PM-5154 [Passkeys iOS] Fix Credential ID handling on bytes and string formats. Fix Discoverable to be lowercase on set so it doesn't break parsing on clients. Added UserDisplayName on Fido2 entities. Extracted the Guid Standard/Raw format helpers to a extensions class.

* Fix incompatible GUID conversions

* PM-5154 [Passkeys iOS] Added custom UI flow for passkey creation

* PM-5154 [Passkeys iOS] Updated UI for passkey creation

* PM-5154 [Passkeys iOS] Refactored and added cipher selection for passkey creation on autofill search.

* PM-5154 [Passkeys iOS] Fixed empty top space on autofill password list

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: mpbw2 <59324545+mpbw2@users.noreply.github.com>
This commit is contained in:
Federico Maccaroni
2024-02-21 14:51:44 -03:00
committed by GitHub
parent 71de3bedf4
commit 16e1b60a4d
37 changed files with 1316 additions and 253 deletions

View File

@@ -1,16 +1,130 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using AuthenticationServices;
using Bit.App.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.iOS.Core.Utilities;
using Foundation;
using Microsoft.Maui.ApplicationModel;
using ObjCRuntime;
using UIKit;
namespace Bit.iOS.Autofill
{
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost
{
private readonly LazyResolve<IFido2AuthenticatorService> _fido2AuthService = new LazyResolve<IFido2AuthenticatorService>();
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
return;
}
try
{
switch (registrationRequest?.Type)
{
case ASCredentialRequestType.PasskeyAssertion:
var passkeyRegistrationRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(registrationRequest.GetHandle());
await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest);
break;
default:
CancelRequest(ASExtensionErrorCode.Failed);
break;
}
}
catch (Exception ex)
{
OnProvidingCredentialException(ex);
}
}
private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null)
{
return;
}
InitAppIfNeeded();
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
_context.PasskeyCredentialRequest = passkeyRegistrationRequest;
_context.IsCreatingPasskey = true;
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
{
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required",
RequireResidentKey = true,
RpEntity = new PublicKeyCredentialRpEntity
{
Id = credIdentity.RelyingPartyIdentifier,
Name = credIdentity.RelyingPartyIdentifier
},
UserEntity = new PublicKeyCredentialUserEntity
{
Id = credIdentity.UserHandle.ToArray(),
Name = credIdentity.UserName,
DisplayName = credIdentity.UserName
}
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync, _context, OnConfirmingNewCredential));
await ASHelpers.ReplaceAllIdentitiesAsync();
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
credIdentity.RelyingPartyIdentifier,
passkeyRegistrationRequest.ClientDataHash,
NSData.FromArray(result.CredentialId),
NSData.FromArray(result.AttestationObject)));
}
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
{
if (supportedAlgorithms?.Any() != true)
{
return new PublicKeyCredentialParameters[]
{
new PublicKeyCredentialParameters
{
Type = Bit.Core.Constants.DefaultFido2CredentialType,
Alg = (int)Fido2AlgorithmIdentifier.ES256
},
new PublicKeyCredentialParameters
{
Type = Bit.Core.Constants.DefaultFido2CredentialType,
Alg = (int)Fido2AlgorithmIdentifier.RS256
}
};
}
return supportedAlgorithms
.Where(alg => (int)alg == (int)Fido2AlgorithmIdentifier.ES256)
.Select(alg => new PublicKeyCredentialParameters
{
Type = Bit.Core.Constants.DefaultFido2CredentialType,
Alg = (int)alg
}).ToArray();
}
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
{
InitAppIfNeeded();
@@ -25,7 +139,7 @@ namespace Bit.iOS.Autofill
await ProvideCredentialAsync(false);
}
public async Task CompleteAssertionRequestAsync(CipherView cipherView)
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
@@ -33,44 +147,127 @@ namespace Bit.iOS.Autofill
return;
}
// // TODO: Generate the credential Signature and Auth data accordingly
// var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
// {
// RpId = cipherView.Login.MainFido2Credential.RpId,
// Counter = cipherView.Login.MainFido2Credential.Counter,
// CredentialId = cipherView.Login.MainFido2Credential.CredentialId
// });
// CompleteAssertionRequest(new ASPasskeyAssertionCredential(
// cipherView.Login.MainFido2Credential.UserHandle,
// cipherView.Login.MainFido2Credential.RpId,
// NSData.FromArray(fido2AssertionResult.Signature),
// _context.PasskeyCredentialRequest?.ClientDataHash,
// NSData.FromArray(fido2AssertionResult.AuthenticatorData),
// cipherView.Login.MainFido2Credential.CredentialId
// ));
}
public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
{
if (_context == null)
if (_context.PasskeyCredentialRequest is null)
{
ServiceContainer.Reset();
CancelRequest(ASExtensionErrorCode.UserCanceled);
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest"));
return;
}
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
try
{
// TODO: Add user verification and remove hardcoding on the user interface "userVerified"
var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
{
RpId = rpId,
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
{
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
{
Id = credentialIdData.ToArray()
}
}
}, new Fido2GetAssertionUserInterface(cipherId, true, EnsureUnlockedVaultAsync, () => Task.FromResult(true)));
var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
: (NSData)userHandleData;
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
: credentialIdData;
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
selectedUserHandleData,
rpId,
NSData.FromArray(fido2AssertionResult.Signature),
_context.PasskeyCredentialRequest.ClientDataHash,
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
selectedCredentialIdData
));
}
catch (InvalidOperationException)
{
return;
}
}
public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
{
try
{
if (assertionCredential is null)
{
ServiceContainer.Reset();
CancelRequest(ASExtensionErrorCode.UserCanceled);
return;
}
ServiceContainer.Reset();
ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null);
});
#pragma warning disable CA1416 // Validate platform compatibility
var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential);
#pragma warning restore CA1416 // Validate platform compatibility
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
{
return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials;
}
private void OnConfirmingNewCredential()
{
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
PerformSegue(SegueConstants.LOGIN_LIST, this);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
}
private async Task EnsureUnlockedVaultAsync()
{
if (_context.IsCreatingPasskey)
{
if (!await IsLocked())
{
return;
}
_context.UnlockVaultTcs?.SetCanceled();
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
PerformSegue(SegueConstants.LOCK, this);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
await _context.UnlockVaultTcs.Task;
return;
}
if (!await IsAuthed() || await IsLocked())
{
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
throw new InvalidOperationException("Not authed or locked");
}
}
}
}