From 7381d5278a258a634589fdee01f2a1926a303c46 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Thu, 15 Feb 2024 21:16:54 -0300 Subject: [PATCH] PM-5154 Fixed select passkey flow and started implementing create passkey on iOS --- src/Core/Abstractions/IFido2UserInterface.cs | 2 + src/Core/Models/View/Fido2CredentialView.cs | 10 +- .../Services/Fido2AuthenticatorService.cs | 75 ++++- .../Fido2AuthenticatorGetAssertionResult.cs | 5 + .../Fido2/PublicKeyCredentialDescriptor.cs | 1 + ...edentialProviderViewController.Passkeys.cs | 258 +++++++++++++++++- .../CredentialProviderViewController.cs | 98 ++++--- src/iOS.Autofill/Models/Context.cs | 10 +- 8 files changed, 391 insertions(+), 68 deletions(-) diff --git a/src/Core/Abstractions/IFido2UserInterface.cs b/src/Core/Abstractions/IFido2UserInterface.cs index 7bbec6c0d..43b169b19 100644 --- a/src/Core/Abstractions/IFido2UserInterface.cs +++ b/src/Core/Abstractions/IFido2UserInterface.cs @@ -50,6 +50,8 @@ namespace Bit.Core.Abstractions /// Whether or not the user must be verified before completing the operation. /// public bool UserVerification { get; set; } + + public string RpId { get; set; } } public struct Fido2ConfirmNewCredentialResult diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs index 67c05dce5..f76a7486e 100644 --- a/src/Core/Models/View/Fido2CredentialView.cs +++ b/src/Core/Models/View/Fido2CredentialView.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using System.Text.Json.Serialization; +using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Utilities; @@ -28,29 +29,36 @@ namespace Bit.Core.Models.View public string Counter { get; set; } public DateTime CreationDate { get; set; } + [JsonIgnore] public int CounterValue { get => int.TryParse(Counter, out var counter) ? counter : 0; set => Counter = value.ToString(); } + [JsonIgnore] public byte[] UserHandleValue { get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle); set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value); } + [JsonIgnore] public byte[] KeyBytes { get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue); set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value); } + [JsonIgnore] public bool DiscoverableValue { get => bool.TryParse(Discoverable, out var discoverable) && discoverable; set => Discoverable = value.ToString(); } + [JsonIgnore] public override string SubTitle => UserName; public override List> LinkedFieldOptions => new List>(); + [JsonIgnore] public bool CanLaunch => !string.IsNullOrEmpty(RpId); + [JsonIgnore] public string LaunchUri => $"https://{RpId}"; public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName; diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index ddb65bb41..b5774c892 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -60,7 +60,8 @@ namespace Bit.Core.Services var response = await _userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams { CredentialName = makeCredentialParams.RpEntity.Name, UserName = makeCredentialParams.UserEntity.Name, - UserVerification = makeCredentialParams.RequireUserVerification + UserVerification = makeCredentialParams.RequireUserVerification, + RpId = makeCredentialParams.RpEntity.Id }); var cipherId = response.CipherId; @@ -131,11 +132,16 @@ namespace Bit.Core.Services await _syncService.FullSyncAsync(false); if (assertionParams.AllowCredentialDescriptorList?.Length > 0) { + + ClipLogger.Log("[Fido2Authenticator] Finding credentials with credential descriptor list"); + cipherOptions = await FindCredentialsByIdAsync( assertionParams.AllowCredentialDescriptorList, assertionParams.RpId ); - } else { + } else + { + ClipLogger.Log("[Fido2Authenticator] Finding credentials with RP"); cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId); } @@ -154,12 +160,15 @@ namespace Bit.Core.Services // TODO: We might want reconsider allowing user presence to be optional if (assertionParams.AllowCredentialDescriptorList?.Length == 1 && assertionParams.RequireUserPresence == false) { + ClipLogger.Log("[Fido2Authenticator] AllowCredentialDescriptorList + RequireUserPresence false"); selectedCipherId = cipherOptions[0].Id; userVerified = false; userPresence = false; } else { + ClipLogger.Log("[Fido2Authenticator] PickCredentialAsync"); + var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams { CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(), UserVerification = assertionParams.RequireUserVerification @@ -197,10 +206,13 @@ namespace Bit.Core.Services throw new NotAllowedError(); } - try { + try + { var selectedFido2Credential = selectedCipher.Login.MainFido2Credential; var selectedCredentialId = selectedFido2Credential.CredentialId; + ClipLogger.Log($"[Fido2Authenticator] Selected fido2 cred {selectedFido2Credential.CredentialId}"); + if (selectedFido2Credential.CounterValue != 0) { ++selectedFido2Credential.CounterValue; } @@ -211,17 +223,24 @@ namespace Bit.Core.Services var authenticatorData = await GenerateAuthDataAsync( rpId: selectedFido2Credential.RpId, - userPresence: userPresence, - userVerification: userVerified, + userPresence: true, + userVerification: true, counter: selectedFido2Credential.CounterValue ); + + ClipLogger.Log($"authenticatorData base64 from bytes: {Convert.ToBase64String(authenticatorData, Base64FormattingOptions.None)}"); + ClipLogger.Log($"ClientDataHash base64 from bytes: {Convert.ToBase64String(assertionParams.Hash, Base64FormattingOptions.None)}"); + ClipLogger.Log($"selectedFido2Credential.KeyBytes base64 from bytes: {Convert.ToBase64String(selectedFido2Credential.KeyBytes, Base64FormattingOptions.None)}"); + var signature = GenerateSignature( authData: authenticatorData, clientDataHash: assertionParams.Hash, privateKey: selectedFido2Credential.KeyBytes ); + ClipLogger.Log($"signature base64 from bytes: {Convert.ToBase64String(signature, Base64FormattingOptions.None)}"); + return new Fido2AuthenticatorGetAssertionResult { SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential @@ -301,17 +320,35 @@ namespace Bit.Core.Services { try { + if (credential.IdStr != null) + { + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Adding credID: {credential.IdStr}"); + ids.Add(credential.IdStr); + continue; + } + + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Converting Guid byte length: {credential.Id.Length}"); ids.Add(GuidToStandardFormat(credential.Id)); } - catch {} + catch(Exception ex) + { + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> Converting Guid ex {ex}"); + } } + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> {credentials.Length} vs {ids.Count}"); + if (ids.Count == 0) { return new List(); } + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> {ids[0]}"); + var ciphers = await _cipherService.GetAllDecryptedAsync(); + + ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> ciphers count: {ciphers?.Count}"); + return ciphers.FindAll((cipher) => !cipher.IsDeleted && cipher.Type == CipherType.Login && @@ -347,9 +384,9 @@ namespace Bit.Core.Services { return new Fido2CredentialView { CredentialId = Guid.NewGuid().ToString(), - KeyType = "public-key", - KeyAlgorithm = "ECDSA", - KeyCurve = "P-256", + KeyType = Bit.Core.Constants.DefaultFido2CredentialType, + KeyAlgorithm = Bit.Core.Constants.DefaultFido2CredentialAlgorithm, + KeyCurve = Bit.Core.Constants.DefaultFido2CredentialCurve, KeyValue = CoreHelpers.Base64UrlEncode(privateKey), RpId = makeCredentialsParams.RpEntity.Id, UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id), @@ -377,6 +414,9 @@ namespace Bit.Core.Services var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256); authData.AddRange(rpIdHash); + + ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> ad: {isAttestation} - uv: {userVerification} - up: {userPresence}"); + var flags = AuthDataFlags( extensionData: false, attestationData: isAttestation, @@ -385,6 +425,10 @@ namespace Bit.Core.Services ); authData.Add(flags); + ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> flags: {flags}"); + + ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> counter: {counter}"); + authData.AddRange(new List { (byte)(counter >> 24), (byte)(counter >> 16), @@ -407,13 +451,14 @@ namespace Bit.Core.Services attestedCredentialData.AddRange(credentialId); attestedCredentialData.AddRange(publicKey.ExportCose()); + ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> adding attestedCD: {attestedCredentialData}"); authData.AddRange(attestedCredentialData); } return authData.ToArray(); } - private byte AuthDataFlags(bool extensionData, bool attestationData, bool userVerification, bool userPresence) { + private byte AuthDataFlags(bool extensionData, bool attestationData, bool userVerification, bool userPresence, bool backupEligibility = true, bool backupState = true) { byte flags = 0; if (extensionData) { @@ -424,6 +469,16 @@ namespace Bit.Core.Services flags |= 0b01000000; } + if (backupEligibility) + { + flags |= 0b00001000; + } + + if (backupState) + { + flags |= 0b00010000; + } + if (userVerification) { flags |= 0b00000100; } diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs index 70331029b..31e1741ff 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorGetAssertionResult.cs @@ -7,6 +7,11 @@ public byte[] Signature { get; set; } public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; } + + public override string ToString() + { + return $"AD: {AuthenticatorData.Length}; Sig: {Signature.Length}; SC: {SelectedCredential?.Id?.Length}; {SelectedCredential?.UserHandle?.Length}"; + } } public class Fido2AuthenticatorGetAssertionSelectedCredential { diff --git a/src/Core/Utilities/Fido2/PublicKeyCredentialDescriptor.cs b/src/Core/Utilities/Fido2/PublicKeyCredentialDescriptor.cs index 16fa9e698..557695a85 100644 --- a/src/Core/Utilities/Fido2/PublicKeyCredentialDescriptor.cs +++ b/src/Core/Utilities/Fido2/PublicKeyCredentialDescriptor.cs @@ -2,6 +2,7 @@ namespace Bit.Core.Utilities.Fido2 { public class PublicKeyCredentialDescriptor { public byte[] Id { get; set; } + public string IdStr { get; set; } public string[] Transports { get; set; } public string Type { get; set; } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index d3c98c288..5999c68c4 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -1,18 +1,27 @@ using System; +using System.Collections.Generic; +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; +using Vision; namespace Bit.iOS.Autofill { public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface { + private readonly LazyResolve _cipherService = new LazyResolve(); + private IFido2AuthenticatorService _fido2AuthService; private IFido2AuthenticatorService Fido2AuthService { @@ -27,6 +36,136 @@ namespace Bit.iOS.Autofill } } + public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return; + } + + ClipLogger.Log($"PIFPR(IASC)"); + + try + { + switch (registrationRequest?.Type) + { + case ASCredentialRequestType.PasskeyAssertion: + ClipLogger.Log($"PIFPR(IASC) -> Passkey"); + var passkeyRegistrationRequest = Runtime.GetNSObject(registrationRequest.GetHandle()); + await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest); + break; + default: + ClipLogger.Log($"PIFPR(IASC) -> Type not PA"); + 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) + { + ClipLogger.Log($"PIFPR Not iOS 17 or null passkey request/identity"); + return; + } + + InitAppIfNeeded(); + + if (!await IsAuthed()) + { + ClipLogger.Log($"PIFPR Not Authed"); + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + + _context.PasskeyCredentialRequest = passkeyRegistrationRequest; + _context.IsCreatingPasskey = true; + + var credIdentity = Runtime.GetNSObject(passkeyRegistrationRequest.CredentialIdentity.GetHandle()); + + ClipLogger.Log($"PIFPR MakeCredentialAsync"); + ClipLogger.Log($"PIFPR MakeCredentialAsync RpID: {credIdentity.RelyingPartyIdentifier}"); + ClipLogger.Log($"PIFPR MakeCredentialAsync UserName: {credIdentity.UserName}"); + ClipLogger.Log($"PIFPR MakeCredentialAsync UVP: {passkeyRegistrationRequest.UserVerificationPreference}"); + ClipLogger.Log($"PIFPR MakeCredentialAsync SA: {passkeyRegistrationRequest.SupportedAlgorithms?.Select(a => (int)a)}"); + ClipLogger.Log($"PIFPR MakeCredentialAsync UH: {credIdentity.UserHandle.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}"); + + var result = await Fido2AuthService.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 + } + }); + + ClipLogger.Log($"PIFPR Completing"); + ClipLogger.Log($"PIFPR Completing - RpId: {credIdentity.RelyingPartyIdentifier}"); + ClipLogger.Log($"PIFPR Completing - CDH: {passkeyRegistrationRequest.ClientDataHash.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}"); + ClipLogger.Log($"PIFPR Completing - CID: {Convert.ToBase64String(result.CredentialId, Base64FormattingOptions.None)}"); + ClipLogger.Log($"PIFPR Completing - AO: {Convert.ToBase64String(result.AttestationObject, Base64FormattingOptions.None)}"); + + var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential( + credIdentity.RelyingPartyIdentifier, + passkeyRegistrationRequest.ClientDataHash, + NSData.FromArray(result.CredentialId), + NSData.FromArray(result.AttestationObject))); + + ClipLogger.Log($"CompleteRegistrationRequestAsync: {expired}"); + + //else if (await IsLocked()) + //{ + // PerformSegue("lockPasswordSegue", this); + //} + //else + //{ + // PerformSegue("loginListSegue", this); + //} + } + + 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(); @@ -57,27 +196,43 @@ namespace Bit.iOS.Autofill try { + ClipLogger.Log($"ClientDataHash: {_context.PasskeyCredentialRequest.ClientDataHash}"); + ClipLogger.Log($"ClientDataHash BA: {_context.PasskeyCredentialRequest.ClientDataHash.ToByteArray()}"); + ClipLogger.Log($"ClientDataHash base64: {_context.PasskeyCredentialRequest.ClientDataHash.GetBase64EncodedString(NSDataBase64EncodingOptions.None)}"); + ClipLogger.Log($"ClientDataHash base64 from bytes: {Convert.ToBase64String(_context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(), Base64FormattingOptions.None)}"); + var fido2AssertionResult = await Fido2AuthService.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams { RpId = rpId, - ClientDataHash = _context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(), + Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(), RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required", RequireUserPresence = false, AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { - new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialIdData.ToByteArray() } + new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor + { + IdStr = credentialIdData.ToString() + } } }); + + ClipLogger.Log("fido2AssertionResult:" + fido2AssertionResult); + var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null ? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle) : (NSData)userHandleData; + + ClipLogger.Log("selectedUserHandleData:" + selectedUserHandleData); + var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null ? new Guid(fido2AssertionResult.SelectedCredential.Id).ToString() : credentialIdData; - CompleteAssertionRequest(new ASPasskeyAssertionCredential( + ClipLogger.Log("selectedCredentialIdData:" + selectedCredentialIdData); + + await CompleteAssertionRequest(new ASPasskeyAssertionCredential( selectedUserHandleData, rpId, NSData.FromArray(fido2AssertionResult.Signature), @@ -88,24 +243,44 @@ namespace Bit.iOS.Autofill } catch (InvalidOperationException) { + ClipLogger.Log("CompleteAssertionRequestAsync -> InvalidOperationException NoOp"); return; } } - public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) + public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) { - if (_context == null) + try { + ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential"); + if (assertionCredential is null) + { + ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> assertionCredential is null"); ServiceContainer.Reset(); CancelRequest(ASExtensionErrorCode.UserCanceled); return; } - NSRunLoop.Main.BeginInvokeOnMainThread(() => - { + //NSRunLoop.Main.BeginInvokeOnMainThread(() => + //{ ServiceContainer.Reset(); - ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null); - }); +#pragma warning disable CA1416 // Validate platform compatibility + ClipLogger.Log("CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> completing"); + var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential); + //ExtensionContext.CompleteAssertionRequest(assertionCredential, expired => + //{ + // ClipLogger.Log($"ASExtensionContext?.CompleteAssertionRequest: {expired}"); + //}); + + ClipLogger.Log($"CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> Completed {expired}"); +#pragma warning restore CA1416 // Validate platform compatibility + //}); + + } + catch (Exception ex) + { + ClipLogger.Log($"CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential -> failed {ex}"); + } } private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView) @@ -125,13 +300,74 @@ namespace Bit.iOS.Autofill return Task.CompletedTask; } - public Task ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) + public async Task ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) { - return Task.FromResult(new Fido2ConfirmNewCredentialResult()); + // TODO: Show interface so the user can choose whether to create a new passkey or select one to add the passkey to. + var newCipher = new CipherView + { + Name = confirmNewCredentialParams.RpId, + Type = Bit.Core.Enums.CipherType.Login, + Login = new LoginView + { + Uris = new List + { + new LoginUriView + { + Uri = confirmNewCredentialParams.RpId + } + } + }, + Card = new CardView(), + Identity = new IdentityView(), + SecureNote = new SecureNoteView + { + Type = Bit.Core.Enums.SecureNoteType.Generic + }, + Reprompt = Bit.Core.Enums.CipherRepromptType.None + }; + + var encryptedCipher = await _cipherService.Value.EncryptAsync(newCipher); + await _cipherService.Value.SaveWithServerAsync(encryptedCipher); + + return new Fido2ConfirmNewCredentialResult + { + CipherId = encryptedCipher.Id, + UserVerified = true + }; } public async Task EnsureUnlockedVaultAsync() { + if (_context.IsCreatingPasskey) + { + ClipLogger.Log($"EnsureUnlockedVaultAsync creating passkey"); + if (!await IsLocked()) + { + ClipLogger.Log($"EnsureUnlockedVaultAsync not locked"); + return; + } + + _context._unlockVaultTcs?.SetCanceled(); + _context._unlockVaultTcs = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(() => + { + try + { + ClipLogger.Log($"EnsureUnlockedVaultAsync performing lock segue"); + PerformSegue("lockPasswordSegue", this); + } + catch (Exception ex) + { + ClipLogger.Log($"EnsureUnlockedVaultAsync {ex}"); + } + }); + + ClipLogger.Log($"EnsureUnlockedVaultAsync awaiting for unlock"); + await _context._unlockVaultTcs.Task; + return; + } + + ClipLogger.Log($"EnsureUnlockedVaultAsync Passkey selection"); if (!await IsAuthed() || await IsLocked()) { CancelRequest(ASExtensionErrorCode.UserInteractionRequired); diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 9064840d9..bb2e49a72 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -20,6 +20,7 @@ using Foundation; using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; +using ObjCRuntime; using UIKit; using static CoreFoundation.DispatchSource; using static Microsoft.Maui.ApplicationModel.Permissions; @@ -166,44 +167,37 @@ namespace Bit.iOS.Autofill try { - ClipLogger.Log("ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest"); - ClipLogger.Log($"PCWUI(IASC) -> R: {credentialRequest?.GetType().FullName}"); - ClipLogger.Log($"PCWUI(IASC) -> I: {credentialRequest?.CredentialIdentity?.GetType().FullName}"); - var crType = credentialRequest.GetType(); - foreach (var item in crType.GetProperties()) - { - ClipLogger.Log($"PCWUI(IASC) -> R -> {item.Name} -- {item.PropertyType}"); - } + //ClipLogger.Log($"PCWUI(IASC) -> R: {credentialRequest?.GetType().FullName}"); + //ClipLogger.Log($"PCWUI(IASC) -> I: {credentialRequest?.CredentialIdentity?.GetType().FullName}"); - var ciType = credentialRequest.CredentialIdentity.GetType(); - foreach (var item in ciType.GetProperties()) - { - ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}"); - } + //ClipLogger.Log($"PCWUI(IASC) -> R k: {asPasskeyCredentialRequest?.GetType().FullName}"); + //ClipLogger.Log($"PCWUI(IASC) -> I k: {asPasskeyCredentialRequest?.CredentialIdentity?.GetType().FullName}"); - try - { - var cc = (ASPasskeyCredentialRequest)credentialRequest; - ClipLogger.Log($"PCWUI(IASC) -> R -> Force cast {cc}"); - } - catch (Exception ex) - { - ClipLogger.Log($"PCWUI(IASC) -> R -> Force cast bad - {ex}"); - } + //var crType = asPasskeyCredentialRequest.GetType(); + //foreach (var item in crType.GetProperties()) + //{ + // ClipLogger.Log($"PCWUI(IASC) -> R -> {item.Name} -- {item.PropertyType}"); + //} + + //var ciType = asPasskeyCredentialRequest.CredentialIdentity.GetType(); + //foreach (var item in ciType.GetProperties()) + //{ + // ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}"); + //} switch (credentialRequest?.Type) { case ASCredentialRequestType.Password: - ClipLogger.Log($"PCWUI(IASC) -> Type P {credentialRequest.CredentialIdentity}"); - await ProvideCredentialWithoutUserInteractionAsync(credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); + var passwordCredentialIdentity = Runtime.GetNSObject(credentialRequest.CredentialIdentity.GetHandle()); + ClipLogger.Log($"PCWUI(IASC) -> Type P {passwordCredentialIdentity}"); + await ProvideCredentialWithoutUserInteractionAsync(passwordCredentialIdentity); break; case ASCredentialRequestType.PasskeyAssertion: - var bpa = credentialRequest is ASPasskeyCredentialRequest; - ClipLogger.Log($"PCWUI(IASC) -> Type PA {bpa}"); - await ProvideCredentialWithoutUserInteractionAsync(credentialRequest as ASPasskeyCredentialRequest); + var asPasskeyCredentialRequest = Runtime.GetNSObject(credentialRequest.GetHandle()); + await ProvideCredentialWithoutUserInteractionAsync(asPasskeyCredentialRequest); break; default: ClipLogger.Log($"PCWUI(IASC) -> Type not P nor PA"); @@ -254,19 +248,17 @@ namespace Bit.iOS.Autofill try { ClipLogger.Log("PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest"); - ClipLogger.Log($"PITPC(IASCR) -> R: {credentialRequest?.GetType().FullName}"); - ClipLogger.Log($"PITPC(IASCR) -> I: {credentialRequest?.CredentialIdentity?.GetType().FullName}"); switch (credentialRequest?.Type) { case ASCredentialRequestType.Password: + var passwordCredentialIdentity = Runtime.GetNSObject(credentialRequest.CredentialIdentity.GetHandle()); ClipLogger.Log($"PITPC(IASCR) -> Type P {credentialRequest.CredentialIdentity}"); - await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordCredentialIdentity); break; case ASCredentialRequestType.PasskeyAssertion: - var bpa = credentialRequest is ASPasskeyCredentialRequest; - ClipLogger.Log($"PITPC(IASCR) -> Type PA {bpa}"); - await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = credentialRequest as ASPasskeyCredentialRequest); + var asPasskeyCredentialRequest = Runtime.GetNSObject(credentialRequest.GetHandle()); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = asPasskeyCredentialRequest); break; default: ClipLogger.Log($"PITPC(IASCR) -> Type not P nor PA"); @@ -474,6 +466,14 @@ namespace Bit.iOS.Autofill try { ClipLogger.Log("OnLockDismissedAsync"); + + if (_context.IsCreatingPasskey) + { + ClipLogger.Log("OnLockDismissedAsync -> IsCreatingPasskey"); + _context._unlockVaultTcs.SetResult(true); + return; + } + if (_context.PasswordCredentialIdentity != null || _context.IsPasskey) { ClipLogger.Log("OnLockDismissedAsync -> ProvideCredentialAsync"); @@ -509,6 +509,28 @@ namespace Bit.iOS.Autofill try { ClipLogger.Log("ProvideCredentialAsync"); + + if (_context.IsPasskey && UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + if (_context.PasskeyCredentialIdentity is null) + { + ClipLogger.Log("ProvideCredentialAsync -> IsPasskey failed"); + CancelRequest(ASExtensionErrorCode.Failed); + } + + ClipLogger.Log("ProvideCredentialAsync -> IsPasskey"); + ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - RP: {_context.PasskeyCredentialIdentity.RelyingPartyIdentifier}"); + ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - UH: {_context.PasskeyCredentialIdentity.UserHandle}"); + ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - CID: {_context.PasskeyCredentialIdentity.CredentialId}"); + ClipLogger.Log($"ProvideCredentialAsync -> IsPasskey - RI: {_context.RecordIdentifier}"); + + await CompleteAssertionRequestAsync(_context.PasskeyCredentialIdentity.RelyingPartyIdentifier, + _context.PasskeyCredentialIdentity.UserHandle, + _context.PasskeyCredentialIdentity.CredentialId, + _context.RecordIdentifier); + return; + } + if (!ServiceContainer.TryResolve(out var cipherService) || _context.RecordIdentifier == null) @@ -518,16 +540,6 @@ namespace Bit.iOS.Autofill return; } - if (_context.IsPasskey) - { - ClipLogger.Log("ProvideCredentialAsync -> IsPasskey"); - await CompleteAssertionRequestAsync(_context.PasskeyCredentialIdentity.RelyingPartyIdentifier, - _context.PasskeyCredentialIdentity.UserHandle, - _context.PasskeyCredentialIdentity.CredentialId, - _context.RecordIdentifier); - return; - } - ClipLogger.Log("ProvideCredentialAsync -> IsPassword"); var cipher = await cipherService.GetAsync(_context.RecordIdentifier); if (cipher?.Login is null || cipher.Type != CipherType.Login) diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index a7b6212eb..6a1459bb3 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -1,6 +1,8 @@ -using AuthenticationServices; +using System.Threading.Tasks; +using AuthenticationServices; using Bit.iOS.Core.Models; using Foundation; +using ObjCRuntime; using UIKit; namespace Bit.iOS.Autofill.Models @@ -12,14 +14,16 @@ namespace Bit.iOS.Autofill.Models public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; } public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; } public bool Configuring { get; set; } + public bool IsCreatingPasskey { get; set; } + public TaskCompletionSource _unlockVaultTcs { get; set; } public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get { - if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + if (PasskeyCredentialRequest != null && UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { - return PasskeyCredentialRequest?.CredentialIdentity as ASPasskeyCredentialIdentity; + return Runtime.GetNSObject(PasskeyCredentialRequest.CredentialIdentity.GetHandle()); } return null; }