From 18beb4e5f48347504791ea8f6ce5681373b81e9b Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Fri, 9 Feb 2024 18:15:25 -0300 Subject: [PATCH] Added iOS passkeys integration, warning this branch has lots of logs to ease "debugging" extensions. --- Directory.Build.props | 2 +- .../IFido2AuthenticatorService.cs | 1 + .../Services/Fido2AuthenticatorService.cs | 45 ++- src/Core/Services/Logging/ClipLogger.cs | 61 ++++ src/Core/Services/Logging/LoggerHelper.cs | 19 +- ...edentialProviderViewController.Passkeys.cs | 101 +++++- .../CredentialProviderViewController.cs | 324 +++++++++++++----- src/iOS.Core/Utilities/NSDataExtensions.cs | 15 + src/iOS.Core/Utilities/iOSCoreHelpers.cs | 8 +- 9 files changed, 461 insertions(+), 115 deletions(-) create mode 100644 src/Core/Services/Logging/ClipLogger.cs create mode 100644 src/iOS.Core/Utilities/NSDataExtensions.cs diff --git a/Directory.Build.props b/Directory.Build.props index 5a27c8b90..379578e7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 8.0.4-nightly.* + 8.0.7-nightly.* Automatic:AppStore iPhone Distribution True diff --git a/src/Core/Abstractions/IFido2AuthenticatorService.cs b/src/Core/Abstractions/IFido2AuthenticatorService.cs index a9cc46f30..836d1532c 100644 --- a/src/Core/Abstractions/IFido2AuthenticatorService.cs +++ b/src/Core/Abstractions/IFido2AuthenticatorService.cs @@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions { public interface IFido2AuthenticatorService { + void Init(IFido2UserInterface userInterface); Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams); Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams); // TODO: Should this return a List? Or maybe IEnumerable? diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index eeacf646d..0f1537e9e 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -8,10 +8,27 @@ using System.Security.Cryptography; namespace Bit.Core.Services { - public class Fido2AuthenticatorService(ICipherService _cipherService, ISyncService _syncService, ICryptoFunctionService _cryptoFunctionService, IFido2UserInterface _userInterface) : IFido2AuthenticatorService + public class Fido2AuthenticatorService: IFido2AuthenticatorService { // AAGUID: d548826e-79b4-db40-a3d8-11116f7e8349 - public static readonly byte[] AAGUID = [ 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49 ]; + public static readonly byte[] AAGUID = new byte[] { 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49 }; + + private readonly ICipherService _cipherService; + private readonly ISyncService _syncService; + private readonly ICryptoFunctionService _cryptoFunctionService; + private IFido2UserInterface _userInterface; + + public Fido2AuthenticatorService(ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService) + { + _cipherService = cipherService; + _syncService = syncService; + _cryptoFunctionService = cryptoFunctionService; + } + + public void Init(IFido2UserInterface userInterface) + { + _userInterface = userInterface; + } public async Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams) { @@ -21,6 +38,7 @@ namespace Bit.Core.Services // _logService.Warning( // $"[Fido2Authenticator] No compatible algorithms found, RP requested: {requestedAlgorithms}" // ); + ClipLogger.Log("[Fido2Authenticator] No compatible algorithms found, RP requested: {requestedAlgorithms}"); throw new NotSupportedError(); } @@ -34,6 +52,7 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting due to excluded credential found in vault." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting due to excluded credential found in vault"); await _userInterface.InformExcludedCredential(existingCipherIds); throw new NotAllowedError(); } @@ -51,6 +70,7 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting because user confirmation was not recieved." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because user confirmation was not recieved"); throw new NotAllowedError(); } @@ -65,10 +85,11 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting because user verification was unsuccessful." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because user verification was unsuccessful"); throw new NotAllowedError(); } - cipher.Login.Fido2Credentials = [fido2Credential]; + cipher.Login.Fido2Credentials = new List { fido2Credential }; var reencrypted = await _cipherService.EncryptAsync(cipher); await _cipherService.SaveWithServerAsync(reencrypted); credentialId = fido2Credential.CredentialId; @@ -92,10 +113,11 @@ namespace Bit.Core.Services }; } catch (NotAllowedError) { throw; - } catch (Exception) { + } catch (Exception e) { // _logService.Error( // $"[Fido2Authenticator] Unknown error occured during attestation: {e.Message}" // ); + ClipLogger.Log("[Fido2Authenticator] Unknown error occured during attestation: {e.Message}"); throw new UnknownError(); } @@ -121,6 +143,7 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting because no matching credentials were found in the vault." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because no matching credentials were found in the vault"); throw new NotAllowedError(); } @@ -151,6 +174,7 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting because the selected credential could not be found." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because the selected credential could not be found"); throw new NotAllowedError(); } @@ -160,6 +184,7 @@ namespace Bit.Core.Services // "[Fido2Authenticator] Aborting because user presence was required but not detected." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because user presence was required but not detected"); throw new NotAllowedError(); } @@ -167,6 +192,7 @@ namespace Bit.Core.Services // _logService.Info( // "[Fido2Authenticator] Aborting because user verification was unsuccessful." // ); + ClipLogger.Log("[Fido2Authenticator] Aborting because user verification was unsuccessful"); throw new NotAllowedError(); } @@ -206,10 +232,11 @@ namespace Bit.Core.Services AuthenticatorData = authenticatorData, Signature = signature }; - } catch (Exception) { + } catch (Exception e) { // _logService.Error( // $"[Fido2Authenticator] Unknown error occured during assertion: {e.Message}" // ); + ClipLogger.Log($"[Fido2Authenticator] Unknown error occured during assertion: {e.Message}"); throw new UnknownError(); } @@ -235,7 +262,7 @@ namespace Bit.Core.Services PublicKeyCredentialDescriptor[] credentials ) { if (credentials == null || credentials.Length == 0) { - return []; + return Array.Empty(); } var ids = new List(); @@ -249,7 +276,7 @@ namespace Bit.Core.Services } if (ids.Count == 0) { - return []; + return Array.Empty(); } var ciphers = await _cipherService.GetAllDecryptedAsync(); @@ -358,12 +385,12 @@ namespace Bit.Core.Services ); authData.Add(flags); - authData.AddRange([ + authData.AddRange(new List { (byte)(counter >> 24), (byte)(counter >> 16), (byte)(counter >> 8), (byte)counter - ]); + }); if (isAttestation) { diff --git a/src/Core/Services/Logging/ClipLogger.cs b/src/Core/Services/Logging/ClipLogger.cs new file mode 100644 index 000000000..5363d5eb1 --- /dev/null +++ b/src/Core/Services/Logging/ClipLogger.cs @@ -0,0 +1,61 @@ +using System.Runtime.CompilerServices; +using System.Text; +using Bit.Core.Abstractions; + +#if IOS +using UIKit; +#endif + +namespace Bit.Core.Services +{ + public class ClipLogger : ILogger + { + private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder(); + + static ILogger _instance; + public static ILogger Instance + { + get + { + if (_instance is null) + { + _instance = new ClipLogger(); + } + return _instance; + } + } + + protected ClipLogger() + { + } + + public static void Log(string breadcrumb) + { + _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}"); +#if IOS + UIPasteboard.General.String = _currentBreadcrumbs.ToString(); +#endif + } + + public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) + { + var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; + var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; + var properties = new Dictionary + { + ["File"] = filePathAndLineNumber, + ["Method"] = memberName + }; + + Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}"); + } + + public void Exception(Exception ex) => Log(ex?.ToString()); + + public Task InitAsync() => Task.CompletedTask; + + public Task IsEnabled() => Task.FromResult(true); + + public Task SetEnabled(bool value) => Task.CompletedTask; + } +} diff --git a/src/Core/Services/Logging/LoggerHelper.cs b/src/Core/Services/Logging/LoggerHelper.cs index 9cdf225f6..b8b27d8c2 100644 --- a/src/Core/Services/Logging/LoggerHelper.cs +++ b/src/Core/Services/Logging/LoggerHelper.cs @@ -1,5 +1,5 @@ -using System; -using Bit.Core.Abstractions; +using Bit.Core.Abstractions; +using Bit.Core.Services; using Bit.Core.Utilities; namespace Bit.Core.Services @@ -22,10 +22,23 @@ namespace Bit.Core.Services #if !FDROID // just in case the caller throws the exception in a moment where the logger can't be resolved // we need to track the error as well - Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); + //Microsoft.AppCenter.Crashes.Crashes.TrackError(ex); + ClipLogger.Log(ex?.ToString()); #endif } } + + public static void LogBreadcrumb(string breadcrumb) + { + if (ServiceContainer.Resolve("logger", true) is ILogger logger) + { + logger.Error(breadcrumb); + } + else + { + ClipLogger.Log(breadcrumb); + } + } } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index 4cbac99f1..d3c98c288 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -2,15 +2,31 @@ using System.Threading.Tasks; using AuthenticationServices; using Bit.App.Abstractions; +using Bit.Core.Abstractions; using Bit.Core.Models.View; using Bit.Core.Utilities; +using Bit.iOS.Core.Utilities; using Foundation; using UIKit; namespace Bit.iOS.Autofill { - public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost + public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface { + private IFido2AuthenticatorService _fido2AuthService; + private IFido2AuthenticatorService Fido2AuthService + { + get + { + if (_fido2AuthService is null) + { + _fido2AuthService = ServiceContainer.Resolve(); + _fido2AuthService.Init(this); + } + return _fido2AuthService; + } + } + private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest) { InitAppIfNeeded(); @@ -25,7 +41,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,22 +49,47 @@ 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 - // }); + if (_context.PasskeyCredentialRequest is null) + { + OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest")); + return; + } - // 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 - // )); + try + { + var fido2AssertionResult = await Fido2AuthService.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams + { + RpId = rpId, + ClientDataHash = _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() } + } + }); + + var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null + ? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle) + : (NSData)userHandleData; + + var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null + ? new Guid(fido2AssertionResult.SelectedCredential.Id).ToString() + : credentialIdData; + + CompleteAssertionRequest(new ASPasskeyAssertionCredential( + selectedUserHandleData, + rpId, + NSData.FromArray(fido2AssertionResult.Signature), + _context.PasskeyCredentialRequest.ClientDataHash, + NSData.FromArray(fido2AssertionResult.AuthenticatorData), + selectedCredentialIdData + )); + } + catch (InvalidOperationException) + { + return; + } } public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) @@ -71,6 +112,32 @@ namespace Bit.iOS.Autofill { return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials; } + + // IFido2UserInterface + + public Task PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams) + { + return Task.FromResult(new Fido2PickCredentialResult()); + } + + public Task InformExcludedCredential(string[] existingCipherIds) + { + return Task.CompletedTask; + } + + public Task ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) + { + return Task.FromResult(new Fido2ConfirmNewCredentialResult()); + } + + public async Task EnsureUnlockedVaultAsync() + { + if (!await IsAuthed() || await IsLocked()) + { + CancelRequest(ASExtensionErrorCode.UserInteractionRequired); + throw new InvalidOperationException("Not authed or locked"); + } + } } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index fc7d461fc..9e0edd664 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -20,6 +20,8 @@ using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; using UIKit; +using static CoreFoundation.DispatchSource; +using static Microsoft.Maui.ApplicationModel.Permissions; namespace Bit.iOS.Autofill { @@ -31,7 +33,6 @@ namespace Bit.iOS.Autofill private IAccountsManager _accountsManager; private readonly LazyResolve _stateService = new LazyResolve(); - private readonly LazyResolve _fido2AuthService = new LazyResolve(); public CredentialProviderViewController(IntPtr handle) : base(handle) @@ -45,8 +46,12 @@ namespace Bit.iOS.Autofill { try { + ClipLogger.Log("ViewDidLoad"); + InitAppIfNeeded(); + ClipLogger.Log("Inited"); + base.ViewDidLoad(); Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png"); @@ -56,9 +61,11 @@ namespace Bit.iOS.Autofill ExtContext = ExtensionContext }; + ClipLogger.Log("ViewDidLoad completed"); } catch (Exception ex) { + ClipLogger.Log($"ViewDidLoad ex: {ex}"); OnProvidingCredentialException(ex); } } @@ -67,6 +74,7 @@ namespace Bit.iOS.Autofill { try { + ClipLogger.Log("PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers"); InitAppIfNeeded(); _context.ServiceIdentifiers = serviceIdentifiers; if (serviceIdentifiers.Length > 0) @@ -104,22 +112,116 @@ namespace Bit.iOS.Autofill } } - public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest) + [Export("prepareCredentialListForServiceIdentifiers:requestParameters:")] + public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters) { try { - switch (credentialRequest) + ClipLogger.Log("PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters"); + InitAppIfNeeded(); + _context.ServiceIdentifiers = serviceIdentifiers; + if (serviceIdentifiers.Length > 0) { - case ASPasswordCredentialRequest passwordRequest: - await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + var uri = serviceIdentifiers[0].Identifier; + if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) + { + uri = string.Concat("https://", uri); + } + _context.UrlString = uri; + } + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + } + else if (await IsLocked()) + { + PerformSegue("lockPasswordSegue", this); + } + else + { + if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue("loginSearchSegue", this); + } + else + { + PerformSegue("loginListSegue", this); + } + } + } + catch (Exception ex) + { + OnProvidingCredentialException(ex); + } + } + + [Export("provideCredentialWithoutUserInteractionForRequest:")] + public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return; + } + + 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}"); + } + + var ciType = credentialRequest.CredentialIdentity.GetType(); + foreach (var item in ciType.GetProperties()) + { + ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}"); + } + + 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}"); + } + + + switch (credentialRequest?.Type) + { + case ASCredentialRequestType.Password: + ClipLogger.Log($"PCWUI(IASC) -> Type P {credentialRequest.CredentialIdentity}"); + await ProvideCredentialWithoutUserInteractionAsync(credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); break; - case ASPasskeyCredentialRequest passkeyRequest: - await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest); + case ASCredentialRequestType.PasskeyAssertion: + var bpa = credentialRequest is ASPasskeyCredentialRequest; + ClipLogger.Log($"PCWUI(IASC) -> Type PA {bpa}"); + await ProvideCredentialWithoutUserInteractionAsync(credentialRequest as ASPasskeyCredentialRequest); break; default: + ClipLogger.Log($"PCWUI(IASC) -> Type not P nor PA"); CancelRequest(ASExtensionErrorCode.Failed); break; } + + //switch (credentialRequest) + //{ + // case ASPasswordCredentialRequest passwordRequest: + // await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + // break; + // case ASPasskeyCredentialRequest passkeyRequest: + // await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest); + // break; + // default: + // CancelRequest(ASExtensionErrorCode.Failed); + // break; + //} } catch (Exception ex) { @@ -127,86 +229,89 @@ namespace Bit.iOS.Autofill } } - public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) - { - try - { - await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity); - } - catch (Exception ex) - { - OnProvidingCredentialException(ex); - } - } - - private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity) - { - 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.PasswordCredentialIdentity = credentialIdentity; - await ProvideCredentialAsync(false); - } + //public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) + //{ + // try + // { + // ClipLogger.Log("ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity"); + // await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity); + // } + // catch (Exception ex) + // { + // OnProvidingCredentialException(ex); + // } + //} + [Export("prepareInterfaceToProvideCredentialForRequest:")] public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest) { - try + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { - switch (credentialRequest) - { - case ASPasswordCredentialRequest passwordRequest: - PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); - break; - case ASPasskeyCredentialRequest passkeyRequest: - await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest); - break; - default: - ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); - break; - } - } - catch (Exception ex) - { - OnProvidingCredentialException(ex); - } - } - - public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) - { - try - { - await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity); - } - catch (Exception ex) - { - OnProvidingCredentialException(ex); - } - } - - private async Task PrepareInterfaceToProvideCredentialAsync(Action updateContext) - { - InitAppIfNeeded(); - if (!await IsAuthed()) - { - await _accountsManager.NavigateOnAccountChangeAsync(false); return; } - updateContext(_context); - await CheckLockAsync(async () => await ProvideCredentialAsync()); + + 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: + ClipLogger.Log($"PITPC(IASCR) -> Type P {credentialRequest.CredentialIdentity}"); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); + break; + case ASCredentialRequestType.PasskeyAssertion: + var bpa = credentialRequest is ASPasskeyCredentialRequest; + ClipLogger.Log($"PITPC(IASCR) -> Type PA {bpa}"); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = credentialRequest as ASPasskeyCredentialRequest); + break; + default: + ClipLogger.Log($"PITPC(IASCR) -> Type not P nor PA"); + CancelRequest(ASExtensionErrorCode.Failed); + break; + } + + + //switch (credentialRequest) + //{ + // case ASPasswordCredentialRequest passwordRequest: + // //PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + // await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + // break; + // case ASPasskeyCredentialRequest passkeyRequest: + // await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest); + // break; + // default: + // CancelRequest(ASExtensionErrorCode.Failed); + // break; + //} + } + catch (Exception ex) + { + OnProvidingCredentialException(ex); + } } + //public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) + //{ + // try + // { + // ClipLogger.Log("PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity"); + // await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity); + // } + // catch (Exception ex) + // { + // OnProvidingCredentialException(ex); + // } + //} public override async void PrepareInterfaceForExtensionConfiguration() { try { + ClipLogger.Log("PrepareInterfaceForExtensionConfiguration"); InitAppIfNeeded(); _context.Configuring = true; if (!await IsAuthed()) @@ -221,10 +326,41 @@ namespace Bit.iOS.Autofill OnProvidingCredentialException(ex); } } + + private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity) + { + ClipLogger.Log("ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity"); + 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.PasswordCredentialIdentity = credentialIdentity; + await ProvideCredentialAsync(false); + } + + private async Task PrepareInterfaceToProvideCredentialAsync(Action updateContext) + { + ClipLogger.Log("PrepareInterfaceToProvideCredentialAsync(Action updateContext"); + InitAppIfNeeded(); + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + updateContext(_context); + await CheckLockAsync(async () => await ProvideCredentialAsync()); + } public void CompleteRequest(string id = null, string username = null, string password = null, string totp = null) { + ClipLogger.Log("CompleteRequest"); if ((_context?.Configuring ?? true) && string.IsNullOrWhiteSpace(password)) { ServiceContainer.Reset(); @@ -261,13 +397,13 @@ namespace Bit.iOS.Autofill private void OnProvidingCredentialException(Exception ex) { - //LoggerHelper.LogEvenIfCantBeResolved(ex); - UIPasteboard.General.String = ex.ToString(); + LoggerHelper.LogEvenIfCantBeResolved(ex); CancelRequest(ASExtensionErrorCode.Failed); } private void CancelRequest(ASExtensionErrorCode code) { + ClipLogger.Log("CancelRequest" + code); //var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null); var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code); ExtensionContext?.CancelRequest(err); @@ -277,6 +413,7 @@ namespace Bit.iOS.Autofill { try { + ClipLogger.Log("Preparing for Segue"); if (segue.DestinationViewController is UINavigationController navController) { if (navController.TopViewController is LoginListViewController listLoginController) @@ -315,13 +452,15 @@ namespace Bit.iOS.Autofill } } - public async void DismissLockAndContinue() + public void DismissLockAndContinue() { + ClipLogger.Log("DismissLockAndContinue"); DismissViewController(false, async () => await OnLockDismissedAsync()); } private void NavigateToPage(ContentPage page) { + ClipLogger.Log("NavigateToPage" + page.GetType().FullName); var navigationPage = new NavigationPage(page); var uiController = navigationPage.ToUIViewController(MauiContextSingleton.Instance.MauiContext); uiController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; @@ -333,23 +472,28 @@ namespace Bit.iOS.Autofill { try { - if (_context.PasswordCredentialIdentity != null) + ClipLogger.Log("OnLockDismissedAsync"); + if (_context.PasswordCredentialIdentity != null || _context.IsPasskey) { + ClipLogger.Log("OnLockDismissedAsync -> ProvideCredentialAsync"); await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync()); return; } if (_context.Configuring) { + ClipLogger.Log("OnLockDismissedAsync -> Configuring"); await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("setupSegue", this)); return; } if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) { + ClipLogger.Log("OnLockDismissedAsync -> loginSearchSegue"); await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("loginSearchSegue", this)); } else { + ClipLogger.Log("OnLockDismissedAsync -> loginListSegue"); await MainThread.InvokeOnMainThreadAsync(() => PerformSegue("loginListSegue", this)); } } @@ -363,14 +507,27 @@ namespace Bit.iOS.Autofill { try { + ClipLogger.Log("ProvideCredentialAsync"); if (!ServiceContainer.TryResolve(out var cipherService) || _context.RecordIdentifier == null) { + ClipLogger.Log("ProvideCredentialAsync -> CredentialIdentityNotFound"); CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound); 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) { @@ -410,12 +567,6 @@ namespace Bit.iOS.Autofill } } - if (_context.IsPasskey) - { - await CompleteAssertionRequestAsync(decCipher); - return; - } - string totpCode = null; if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true) { @@ -437,6 +588,7 @@ namespace Bit.iOS.Autofill private async Task CheckLockAsync(Action notLockedAction) { + ClipLogger.Log("CheckLockAsync"); if (await IsLocked() || await _stateService.Value.GetPasswordRepromptAutofillAsync()) { DispatchQueue.MainQueue.DispatchAsync(() => PerformSegue("lockPasswordSegue", this)); @@ -460,6 +612,7 @@ namespace Bit.iOS.Autofill private void LogoutIfAuthed() { + ClipLogger.Log("LogoutIfAuthed"); NSRunLoop.Main.BeginInvokeOnMainThread(async () => { try @@ -482,12 +635,14 @@ namespace Bit.iOS.Autofill private void InitApp() { + ClipLogger.Log("InitApp"); iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSAutoFillClearCiphersCacheKey, _nfcSession, out _nfcDelegate, out _accountsManager); } private void InitAppIfNeeded() { + ClipLogger.Log("InitAppIfNeeded"); if (ServiceContainer.RegisteredServices == null || ServiceContainer.RegisteredServices.Count == 0) { InitApp(); @@ -685,6 +840,7 @@ namespace Bit.iOS.Autofill public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null) { + ClipLogger.Log("Navigate" + navTarget); switch (navTarget) { case NavigationTarget.HomeLogin: diff --git a/src/iOS.Core/Utilities/NSDataExtensions.cs b/src/iOS.Core/Utilities/NSDataExtensions.cs new file mode 100644 index 000000000..3e58ae3c7 --- /dev/null +++ b/src/iOS.Core/Utilities/NSDataExtensions.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using Foundation; + +namespace Bit.iOS.Core.Utilities +{ + public static class NSDataExtensions + { + public static byte[] ToByteArray(this NSData data) + { + var bytes = new byte[data.Length]; + Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length)); + return bytes; + } + } +} diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 8e55434f1..ff5b3f277 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -123,7 +123,7 @@ namespace Bit.iOS.Core.Utilities else { #if DEBUG - logger = DebugLogger.Instance; + logger = ClipLogger.Instance; #else logger = Logger.Instance; #endif @@ -138,6 +138,7 @@ namespace Bit.iOS.Core.Utilities logger!.Exception(nreAppGroupContainer); throw nreAppGroupContainer; } + var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); @@ -189,6 +190,11 @@ namespace Bit.iOS.Core.Utilities public static void RegisterFinallyBeforeBootstrap() { + ServiceContainer.Register(new Fido2AuthenticatorService( + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), ServiceContainer.Resolve(), ServiceContainer.Resolve(),