1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-28 12:35:40 +01:00

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

This commit is contained in:
Federico Maccaroni 2024-02-15 21:16:54 -03:00
parent 8b5a7b257d
commit 7381d5278a
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
8 changed files with 391 additions and 68 deletions

View File

@ -50,6 +50,8 @@ namespace Bit.Core.Abstractions
/// Whether or not the user must be verified before completing the operation. /// Whether or not the user must be verified before completing the operation.
/// </summary> /// </summary>
public bool UserVerification { get; set; } public bool UserVerification { get; set; }
public string RpId { get; set; }
} }
public struct Fido2ConfirmNewCredentialResult public struct Fido2ConfirmNewCredentialResult

View File

@ -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.Models.Domain;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -28,29 +29,36 @@ namespace Bit.Core.Models.View
public string Counter { get; set; } public string Counter { get; set; }
public DateTime CreationDate { get; set; } public DateTime CreationDate { get; set; }
[JsonIgnore]
public int CounterValue { public int CounterValue {
get => int.TryParse(Counter, out var counter) ? counter : 0; get => int.TryParse(Counter, out var counter) ? counter : 0;
set => Counter = value.ToString(); set => Counter = value.ToString();
} }
[JsonIgnore]
public byte[] UserHandleValue { public byte[] UserHandleValue {
get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle); get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value); set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
} }
[JsonIgnore]
public byte[] KeyBytes { public byte[] KeyBytes {
get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue); get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value); set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
} }
[JsonIgnore]
public bool DiscoverableValue { public bool DiscoverableValue {
get => bool.TryParse(Discoverable, out var discoverable) && discoverable; get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
set => Discoverable = value.ToString(); set => Discoverable = value.ToString();
} }
[JsonIgnore]
public override string SubTitle => UserName; public override string SubTitle => UserName;
public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>(); public override List<KeyValuePair<string, LinkedIdType>> LinkedFieldOptions => new List<KeyValuePair<string, LinkedIdType>>();
[JsonIgnore]
public bool CanLaunch => !string.IsNullOrEmpty(RpId); public bool CanLaunch => !string.IsNullOrEmpty(RpId);
[JsonIgnore]
public string LaunchUri => $"https://{RpId}"; public string LaunchUri => $"https://{RpId}";
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName; public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;

View File

@ -60,7 +60,8 @@ namespace Bit.Core.Services
var response = await _userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams { var response = await _userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams {
CredentialName = makeCredentialParams.RpEntity.Name, CredentialName = makeCredentialParams.RpEntity.Name,
UserName = makeCredentialParams.UserEntity.Name, UserName = makeCredentialParams.UserEntity.Name,
UserVerification = makeCredentialParams.RequireUserVerification UserVerification = makeCredentialParams.RequireUserVerification,
RpId = makeCredentialParams.RpEntity.Id
}); });
var cipherId = response.CipherId; var cipherId = response.CipherId;
@ -131,11 +132,16 @@ namespace Bit.Core.Services
await _syncService.FullSyncAsync(false); await _syncService.FullSyncAsync(false);
if (assertionParams.AllowCredentialDescriptorList?.Length > 0) { if (assertionParams.AllowCredentialDescriptorList?.Length > 0) {
ClipLogger.Log("[Fido2Authenticator] Finding credentials with credential descriptor list");
cipherOptions = await FindCredentialsByIdAsync( cipherOptions = await FindCredentialsByIdAsync(
assertionParams.AllowCredentialDescriptorList, assertionParams.AllowCredentialDescriptorList,
assertionParams.RpId assertionParams.RpId
); );
} else { } else
{
ClipLogger.Log("[Fido2Authenticator] Finding credentials with RP");
cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId); cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId);
} }
@ -154,12 +160,15 @@ namespace Bit.Core.Services
// TODO: We might want reconsider allowing user presence to be optional // TODO: We might want reconsider allowing user presence to be optional
if (assertionParams.AllowCredentialDescriptorList?.Length == 1 && assertionParams.RequireUserPresence == false) if (assertionParams.AllowCredentialDescriptorList?.Length == 1 && assertionParams.RequireUserPresence == false)
{ {
ClipLogger.Log("[Fido2Authenticator] AllowCredentialDescriptorList + RequireUserPresence false");
selectedCipherId = cipherOptions[0].Id; selectedCipherId = cipherOptions[0].Id;
userVerified = false; userVerified = false;
userPresence = false; userPresence = false;
} }
else else
{ {
ClipLogger.Log("[Fido2Authenticator] PickCredentialAsync");
var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams { var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams {
CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(), CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(),
UserVerification = assertionParams.RequireUserVerification UserVerification = assertionParams.RequireUserVerification
@ -197,10 +206,13 @@ namespace Bit.Core.Services
throw new NotAllowedError(); throw new NotAllowedError();
} }
try { try
{
var selectedFido2Credential = selectedCipher.Login.MainFido2Credential; var selectedFido2Credential = selectedCipher.Login.MainFido2Credential;
var selectedCredentialId = selectedFido2Credential.CredentialId; var selectedCredentialId = selectedFido2Credential.CredentialId;
ClipLogger.Log($"[Fido2Authenticator] Selected fido2 cred {selectedFido2Credential.CredentialId}");
if (selectedFido2Credential.CounterValue != 0) { if (selectedFido2Credential.CounterValue != 0) {
++selectedFido2Credential.CounterValue; ++selectedFido2Credential.CounterValue;
} }
@ -211,17 +223,24 @@ namespace Bit.Core.Services
var authenticatorData = await GenerateAuthDataAsync( var authenticatorData = await GenerateAuthDataAsync(
rpId: selectedFido2Credential.RpId, rpId: selectedFido2Credential.RpId,
userPresence: userPresence, userPresence: true,
userVerification: userVerified, userVerification: true,
counter: selectedFido2Credential.CounterValue 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( var signature = GenerateSignature(
authData: authenticatorData, authData: authenticatorData,
clientDataHash: assertionParams.Hash, clientDataHash: assertionParams.Hash,
privateKey: selectedFido2Credential.KeyBytes privateKey: selectedFido2Credential.KeyBytes
); );
ClipLogger.Log($"signature base64 from bytes: {Convert.ToBase64String(signature, Base64FormattingOptions.None)}");
return new Fido2AuthenticatorGetAssertionResult return new Fido2AuthenticatorGetAssertionResult
{ {
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
@ -301,17 +320,35 @@ namespace Bit.Core.Services
{ {
try 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)); 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) if (ids.Count == 0)
{ {
return new List<CipherView>(); return new List<CipherView>();
} }
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> {ids[0]}");
var ciphers = await _cipherService.GetAllDecryptedAsync(); var ciphers = await _cipherService.GetAllDecryptedAsync();
ClipLogger.Log($"[Fido2Authenticator] FindCredentialsByIdAsync -> ciphers count: {ciphers?.Count}");
return ciphers.FindAll((cipher) => return ciphers.FindAll((cipher) =>
!cipher.IsDeleted && !cipher.IsDeleted &&
cipher.Type == CipherType.Login && cipher.Type == CipherType.Login &&
@ -347,9 +384,9 @@ namespace Bit.Core.Services
{ {
return new Fido2CredentialView { return new Fido2CredentialView {
CredentialId = Guid.NewGuid().ToString(), CredentialId = Guid.NewGuid().ToString(),
KeyType = "public-key", KeyType = Bit.Core.Constants.DefaultFido2CredentialType,
KeyAlgorithm = "ECDSA", KeyAlgorithm = Bit.Core.Constants.DefaultFido2CredentialAlgorithm,
KeyCurve = "P-256", KeyCurve = Bit.Core.Constants.DefaultFido2CredentialCurve,
KeyValue = CoreHelpers.Base64UrlEncode(privateKey), KeyValue = CoreHelpers.Base64UrlEncode(privateKey),
RpId = makeCredentialsParams.RpEntity.Id, RpId = makeCredentialsParams.RpEntity.Id,
UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id), UserHandle = CoreHelpers.Base64UrlEncode(makeCredentialsParams.UserEntity.Id),
@ -377,6 +414,9 @@ namespace Bit.Core.Services
var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256); var rpIdHash = await _cryptoFunctionService.HashAsync(rpId, CryptoHashAlgorithm.Sha256);
authData.AddRange(rpIdHash); authData.AddRange(rpIdHash);
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> ad: {isAttestation} - uv: {userVerification} - up: {userPresence}");
var flags = AuthDataFlags( var flags = AuthDataFlags(
extensionData: false, extensionData: false,
attestationData: isAttestation, attestationData: isAttestation,
@ -385,6 +425,10 @@ namespace Bit.Core.Services
); );
authData.Add(flags); authData.Add(flags);
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> flags: {flags}");
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> counter: {counter}");
authData.AddRange(new List<byte> { authData.AddRange(new List<byte> {
(byte)(counter >> 24), (byte)(counter >> 24),
(byte)(counter >> 16), (byte)(counter >> 16),
@ -407,13 +451,14 @@ namespace Bit.Core.Services
attestedCredentialData.AddRange(credentialId); attestedCredentialData.AddRange(credentialId);
attestedCredentialData.AddRange(publicKey.ExportCose()); attestedCredentialData.AddRange(publicKey.ExportCose());
ClipLogger.Log($"[Fido2Authenticator] GenerateAuthDataAsync -> adding attestedCD: {attestedCredentialData}");
authData.AddRange(attestedCredentialData); authData.AddRange(attestedCredentialData);
} }
return authData.ToArray(); 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; byte flags = 0;
if (extensionData) { if (extensionData) {
@ -424,6 +469,16 @@ namespace Bit.Core.Services
flags |= 0b01000000; flags |= 0b01000000;
} }
if (backupEligibility)
{
flags |= 0b00001000;
}
if (backupState)
{
flags |= 0b00010000;
}
if (userVerification) { if (userVerification) {
flags |= 0b00000100; flags |= 0b00000100;
} }

View File

@ -7,6 +7,11 @@
public byte[] Signature { get; set; } public byte[] Signature { get; set; }
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { 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 { public class Fido2AuthenticatorGetAssertionSelectedCredential {

View File

@ -2,6 +2,7 @@ namespace Bit.Core.Utilities.Fido2
{ {
public class PublicKeyCredentialDescriptor { public class PublicKeyCredentialDescriptor {
public byte[] Id { get; set; } public byte[] Id { get; set; }
public string IdStr { get; set; }
public string[] Transports { get; set; } public string[] Transports { get; set; }
public string Type { get; set; } public string Type { get; set; }
} }

View File

@ -1,18 +1,27 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AuthenticationServices; using AuthenticationServices;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Models.View; using Bit.Core.Models.View;
using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Utilities;
using Foundation; using Foundation;
using Microsoft.Maui.ApplicationModel;
using ObjCRuntime;
using UIKit; using UIKit;
using Vision;
namespace Bit.iOS.Autofill namespace Bit.iOS.Autofill
{ {
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost, IFido2UserInterface
{ {
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
private IFido2AuthenticatorService _fido2AuthService; private IFido2AuthenticatorService _fido2AuthService;
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<ASPasskeyCredentialRequest>(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<ASPasskeyCredentialIdentity>(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) private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
{ {
InitAppIfNeeded(); InitAppIfNeeded();
@ -57,27 +196,43 @@ namespace Bit.iOS.Autofill
try 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 var fido2AssertionResult = await Fido2AuthService.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
{ {
RpId = rpId, RpId = rpId,
ClientDataHash = _context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(), Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToByteArray(),
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required", RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
RequireUserPresence = false, RequireUserPresence = false,
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] 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 var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle) ? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
: (NSData)userHandleData; : (NSData)userHandleData;
ClipLogger.Log("selectedUserHandleData:" + selectedUserHandleData);
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
? new Guid(fido2AssertionResult.SelectedCredential.Id).ToString() ? new Guid(fido2AssertionResult.SelectedCredential.Id).ToString()
: credentialIdData; : credentialIdData;
CompleteAssertionRequest(new ASPasskeyAssertionCredential( ClipLogger.Log("selectedCredentialIdData:" + selectedCredentialIdData);
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
selectedUserHandleData, selectedUserHandleData,
rpId, rpId,
NSData.FromArray(fido2AssertionResult.Signature), NSData.FromArray(fido2AssertionResult.Signature),
@ -88,24 +243,44 @@ namespace Bit.iOS.Autofill
} }
catch (InvalidOperationException) catch (InvalidOperationException)
{ {
ClipLogger.Log("CompleteAssertionRequestAsync -> InvalidOperationException NoOp");
return; 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(); ServiceContainer.Reset();
CancelRequest(ASExtensionErrorCode.UserCanceled); CancelRequest(ASExtensionErrorCode.UserCanceled);
return; return;
} }
NSRunLoop.Main.BeginInvokeOnMainThread(() => //NSRunLoop.Main.BeginInvokeOnMainThread(() =>
{ //{
ServiceContainer.Reset(); 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) private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
@ -125,13 +300,74 @@ namespace Bit.iOS.Autofill
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<Fido2ConfirmNewCredentialResult> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) public async Task<Fido2ConfirmNewCredentialResult> 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<LoginUriView>
{
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() 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<bool>();
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()) if (!await IsAuthed() || await IsLocked())
{ {
CancelRequest(ASExtensionErrorCode.UserInteractionRequired); CancelRequest(ASExtensionErrorCode.UserInteractionRequired);

View File

@ -20,6 +20,7 @@ using Foundation;
using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls; using Microsoft.Maui.Controls;
using Microsoft.Maui.Platform; using Microsoft.Maui.Platform;
using ObjCRuntime;
using UIKit; using UIKit;
using static CoreFoundation.DispatchSource; using static CoreFoundation.DispatchSource;
using static Microsoft.Maui.ApplicationModel.Permissions; using static Microsoft.Maui.ApplicationModel.Permissions;
@ -166,44 +167,37 @@ namespace Bit.iOS.Autofill
try try
{ {
ClipLogger.Log("ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest"); 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(); //ClipLogger.Log($"PCWUI(IASC) -> R: {credentialRequest?.GetType().FullName}");
foreach (var item in crType.GetProperties()) //ClipLogger.Log($"PCWUI(IASC) -> I: {credentialRequest?.CredentialIdentity?.GetType().FullName}");
{
ClipLogger.Log($"PCWUI(IASC) -> R -> {item.Name} -- {item.PropertyType}");
}
var ciType = credentialRequest.CredentialIdentity.GetType(); //ClipLogger.Log($"PCWUI(IASC) -> R k: {asPasskeyCredentialRequest?.GetType().FullName}");
foreach (var item in ciType.GetProperties()) //ClipLogger.Log($"PCWUI(IASC) -> I k: {asPasskeyCredentialRequest?.CredentialIdentity?.GetType().FullName}");
{
ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}");
}
try //var crType = asPasskeyCredentialRequest.GetType();
{ //foreach (var item in crType.GetProperties())
var cc = (ASPasskeyCredentialRequest)credentialRequest; //{
ClipLogger.Log($"PCWUI(IASC) -> R -> Force cast {cc}"); // ClipLogger.Log($"PCWUI(IASC) -> R -> {item.Name} -- {item.PropertyType}");
} //}
catch (Exception ex)
{ //var ciType = asPasskeyCredentialRequest.CredentialIdentity.GetType();
ClipLogger.Log($"PCWUI(IASC) -> R -> Force cast bad - {ex}"); //foreach (var item in ciType.GetProperties())
} //{
// ClipLogger.Log($"PCWUI(IASC) -> I -> {item.Name} -- {item.PropertyType}");
//}
switch (credentialRequest?.Type) switch (credentialRequest?.Type)
{ {
case ASCredentialRequestType.Password: case ASCredentialRequestType.Password:
ClipLogger.Log($"PCWUI(IASC) -> Type P {credentialRequest.CredentialIdentity}"); var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
await ProvideCredentialWithoutUserInteractionAsync(credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); ClipLogger.Log($"PCWUI(IASC) -> Type P {passwordCredentialIdentity}");
await ProvideCredentialWithoutUserInteractionAsync(passwordCredentialIdentity);
break; break;
case ASCredentialRequestType.PasskeyAssertion: case ASCredentialRequestType.PasskeyAssertion:
var bpa = credentialRequest is ASPasskeyCredentialRequest; var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
ClipLogger.Log($"PCWUI(IASC) -> Type PA {bpa}"); await ProvideCredentialWithoutUserInteractionAsync(asPasskeyCredentialRequest);
await ProvideCredentialWithoutUserInteractionAsync(credentialRequest as ASPasskeyCredentialRequest);
break; break;
default: default:
ClipLogger.Log($"PCWUI(IASC) -> Type not P nor PA"); ClipLogger.Log($"PCWUI(IASC) -> Type not P nor PA");
@ -254,19 +248,17 @@ namespace Bit.iOS.Autofill
try try
{ {
ClipLogger.Log("PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest"); 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) switch (credentialRequest?.Type)
{ {
case ASCredentialRequestType.Password: case ASCredentialRequestType.Password:
var passwordCredentialIdentity = Runtime.GetNSObject<ASPasswordCredentialIdentity>(credentialRequest.CredentialIdentity.GetHandle());
ClipLogger.Log($"PITPC(IASCR) -> Type P {credentialRequest.CredentialIdentity}"); ClipLogger.Log($"PITPC(IASCR) -> Type P {credentialRequest.CredentialIdentity}");
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialRequest.CredentialIdentity as ASPasswordCredentialIdentity); await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordCredentialIdentity);
break; break;
case ASCredentialRequestType.PasskeyAssertion: case ASCredentialRequestType.PasskeyAssertion:
var bpa = credentialRequest is ASPasskeyCredentialRequest; var asPasskeyCredentialRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(credentialRequest.GetHandle());
ClipLogger.Log($"PITPC(IASCR) -> Type PA {bpa}"); await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = asPasskeyCredentialRequest);
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = credentialRequest as ASPasskeyCredentialRequest);
break; break;
default: default:
ClipLogger.Log($"PITPC(IASCR) -> Type not P nor PA"); ClipLogger.Log($"PITPC(IASCR) -> Type not P nor PA");
@ -474,6 +466,14 @@ namespace Bit.iOS.Autofill
try try
{ {
ClipLogger.Log("OnLockDismissedAsync"); ClipLogger.Log("OnLockDismissedAsync");
if (_context.IsCreatingPasskey)
{
ClipLogger.Log("OnLockDismissedAsync -> IsCreatingPasskey");
_context._unlockVaultTcs.SetResult(true);
return;
}
if (_context.PasswordCredentialIdentity != null || _context.IsPasskey) if (_context.PasswordCredentialIdentity != null || _context.IsPasskey)
{ {
ClipLogger.Log("OnLockDismissedAsync -> ProvideCredentialAsync"); ClipLogger.Log("OnLockDismissedAsync -> ProvideCredentialAsync");
@ -509,6 +509,28 @@ namespace Bit.iOS.Autofill
try try
{ {
ClipLogger.Log("ProvideCredentialAsync"); 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<ICipherService>(out var cipherService) if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|| ||
_context.RecordIdentifier == null) _context.RecordIdentifier == null)
@ -518,16 +540,6 @@ namespace Bit.iOS.Autofill
return; 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"); ClipLogger.Log("ProvideCredentialAsync -> IsPassword");
var cipher = await cipherService.GetAsync(_context.RecordIdentifier); var cipher = await cipherService.GetAsync(_context.RecordIdentifier);
if (cipher?.Login is null || cipher.Type != CipherType.Login) if (cipher?.Login is null || cipher.Type != CipherType.Login)

View File

@ -1,6 +1,8 @@
using AuthenticationServices; using System.Threading.Tasks;
using AuthenticationServices;
using Bit.iOS.Core.Models; using Bit.iOS.Core.Models;
using Foundation; using Foundation;
using ObjCRuntime;
using UIKit; using UIKit;
namespace Bit.iOS.Autofill.Models namespace Bit.iOS.Autofill.Models
@ -12,14 +14,16 @@ namespace Bit.iOS.Autofill.Models
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; } public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; } public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
public bool Configuring { get; set; } public bool Configuring { get; set; }
public bool IsCreatingPasskey { get; set; }
public TaskCompletionSource<bool> _unlockVaultTcs { get; set; }
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
{ {
get get
{ {
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) if (PasskeyCredentialRequest != null && UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{ {
return PasskeyCredentialRequest?.CredentialIdentity as ASPasskeyCredentialIdentity; return Runtime.GetNSObject<ASPasskeyCredentialIdentity>(PasskeyCredentialRequest.CredentialIdentity.GetHandle());
} }
return null; return null;
} }