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.Resources.Localization; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; using Bit.iOS.Autofill.Utilities; 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 _fido2AuthService = new LazyResolve(); private readonly LazyResolve _platformUtilsService = new LazyResolve(); private readonly LazyResolve _userVerificationMediatorService = new LazyResolve(); private readonly LazyResolve _cipherService = new LazyResolve(); public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest) { if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { return; } _context.VaultUnlockedDuringThisSession = false; try { switch (registrationRequest?.Type) { case ASCredentialRequestType.PasskeyAssertion: var passkeyRegistrationRequest = Runtime.GetNSObject(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(passkeyRegistrationRequest.CredentialIdentity.GetHandle()); _context.UrlString = credIdentity?.RelyingPartyIdentifier; try { var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams { Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(), CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms), UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(passkeyRegistrationRequest.UserVerificationPreference), 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.VaultUnlockedDuringThisSession, _context, OnConfirmingNewCredential, VerifyUserAsync)); await ASHelpers.ReplaceAllIdentitiesAsync(); var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential( credIdentity.RelyingPartyIdentifier, passkeyRegistrationRequest.ClientDataHash, NSData.FromArray(result.CredentialId), NSData.FromArray(result.AttestationObject))); } catch { try { await _platformUtilsService.Value.ShowDialogAsync( string.Format(AppResources.ThereWasAProblemCreatingAPasskeyForXTryAgainLater, credIdentity?.RelyingPartyIdentifier), AppResources.ErrorCreatingPasskey); } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); } throw; } } 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(); 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 async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId) { if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request before iOS 17")); return; } if (_context.PasskeyCredentialRequest is null) { OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest")); return; } try { var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams { RpId = rpId, Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(), UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(_context.PasskeyCredentialRequest.UserVerificationPreference), AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialIdData.ToArray() } } }, new Fido2GetAssertionUserInterface(cipherId, false, EnsureUnlockedVaultAsync, () => _context?.VaultUnlockedDuringThisSession ?? false, VerifyUserAsync)); if (fido2AssertionResult.SelectedCredential is null) { throw new NullReferenceException("SelectedCredential must have a value"); } await CompleteAssertionRequest(new ASPasskeyAssertionCredential( NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle), rpId, NSData.FromArray(fido2AssertionResult.Signature), _context.PasskeyCredentialRequest.ClientDataHash, NSData.FromArray(fido2AssertionResult.AuthenticatorData), NSData.FromArray(fido2AssertionResult.SelectedCredential.Id) )); } catch (InvalidOperationNeedsUIException) { return; } catch { try { if (_context?.IsExecutingWithoutUserInteraction == false) { await _platformUtilsService.Value.ShowDialogAsync( string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, rpId), AppResources.ErrorReadingPasskey); } } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); } throw; } } private async Task VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference) { try { var encrypted = await _cipherService.Value.GetAsync(selectedCipherId); var cipher = await encrypted.DecryptAsync(); return await _userVerificationMediatorService.Value.VerifyUserForFido2Async( new Fido2UserVerificationOptions( cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password, userVerificationPreference, _context.VaultUnlockedDuringThisSession, _context.PasskeyCredentialIdentity?.RelyingPartyIdentifier, () => { if (_context.IsExecutingWithoutUserInteraction) { CancelRequest(ASExtensionErrorCode.UserInteractionRequired); throw new InvalidOperationNeedsUIException(); } }) ); } catch (InvalidOperationNeedsUIException) { throw; } catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); return false; } } public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) { try { if (assertionCredential is null) { ServiceContainer.Reset(); CancelRequest(ASExtensionErrorCode.UserCanceled); return; } ServiceContainer.Reset(); #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(); 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 InvalidOperationNeedsUIException("Not authed or locked"); } } } }