1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-21 11:25:56 +01:00

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

This commit is contained in:
Federico Maccaroni 2024-02-09 18:15:25 -03:00
parent e9d1792dd7
commit 18beb4e5f4
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
9 changed files with 461 additions and 115 deletions

View File

@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<MauiVersion>8.0.4-nightly.*</MauiVersion>
<MauiVersion>8.0.7-nightly.*</MauiVersion>
<ReleaseCodesignProvision>Automatic:AppStore</ReleaseCodesignProvision>
<ReleaseCodesignKey>iPhone Distribution</ReleaseCodesignKey>
<IncludeBitwardeniOSExtensions>True</IncludeBitwardeniOSExtensions>

View File

@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
{
public interface IFido2AuthenticatorService
{
void Init(IFido2UserInterface userInterface);
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
// TODO: Should this return a List? Or maybe IEnumerable?

View File

@ -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<Fido2AuthenticatorMakeCredentialResult> 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<Fido2CredentialView> { 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<string>();
}
var ids = new List<string>();
@ -249,7 +276,7 @@ namespace Bit.Core.Services
}
if (ids.Count == 0) {
return [];
return Array.Empty<string>();
}
var ciphers = await _cipherService.GetAllDecryptedAsync();
@ -358,12 +385,12 @@ namespace Bit.Core.Services
);
authData.Add(flags);
authData.AddRange([
authData.AddRange(new List<byte> {
(byte)(counter >> 24),
(byte)(counter >> 16),
(byte)(counter >> 8),
(byte)counter
]);
});
if (isAttestation)
{

View File

@ -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<string, string> 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<string, string>
{
["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<bool> IsEnabled() => Task.FromResult(true);
public Task SetEnabled(bool value) => Task.CompletedTask;
}
}

View File

@ -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<ILogger>("logger", true) is ILogger logger)
{
logger.Error(breadcrumb);
}
else
{
ClipLogger.Log(breadcrumb);
}
}
}
}

View File

@ -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<IFido2AuthenticatorService>();
_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<Fido2PickCredentialResult> PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams)
{
return Task.FromResult(new Fido2PickCredentialResult());
}
public Task InformExcludedCredential(string[] existingCipherIds)
{
return Task.CompletedTask;
}
public Task<Fido2ConfirmNewCredentialResult> 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");
}
}
}
}

View File

@ -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<IStateService> _stateService = new LazyResolve<IStateService>();
private readonly LazyResolve<IFido2AuthenticationService> _fido2AuthService = new LazyResolve<IFido2AuthenticationService>();
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<Context> 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<Context> updateContext)
{
ClipLogger.Log("PrepareInterfaceToProvideCredentialAsync(Action<Context> 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<ICipherService>(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:

View File

@ -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;
}
}
}

View File

@ -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<IFido2AuthenticatorService>(new Fido2AuthenticatorService(
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<ISyncService>(),
ServiceContainer.Resolve<ICryptoFunctionService>()));
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<IStateService>(),