mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-29 12:45:20 +01:00
PM-5154 Start implementing Passkeys Autofill in iOS
This commit is contained in:
parent
a1e4f0aaa2
commit
275ae76761
@ -89,7 +89,7 @@ namespace Bit.iOS
|
|||||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||||
if (needsAutofillReplacement.GetValueOrDefault())
|
if (needsAutofillReplacement.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "showAppExtension")
|
else if (message.Command == "showAppExtension")
|
||||||
@ -103,7 +103,7 @@ namespace Bit.iOS
|
|||||||
var success = data["successfully"] as bool?;
|
var success = data["successfully"] as bool?;
|
||||||
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,65 +112,63 @@ namespace Bit.iOS
|
|||||||
{
|
{
|
||||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
if (_deviceActionService.SystemMajorVersion() >= 12)
|
||||||
{
|
{
|
||||||
if (await ASHelpers.IdentitiesCanIncremental())
|
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||||
{
|
{
|
||||||
var cipherId = message.Data as string;
|
var cipherId = message.Data as string;
|
||||||
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
|
||||||
{
|
{
|
||||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
|
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
if (await ASHelpers.IdentitiesCanIncremental())
|
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||||
{
|
{
|
||||||
var identity = ASHelpers.ToCredentialIdentity(
|
var identity = ASHelpers.ToPasswordCredentialIdentity(
|
||||||
message.Data as Bit.Core.Models.View.CipherView);
|
message.Data as Bit.Core.Models.View.CipherView);
|
||||||
if (identity == null)
|
if (identity == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (message.Command == "logout")
|
else if (message.Command == "logout")
|
||||||
{
|
{
|
||||||
if (_deviceActionService.SystemMajorVersion() >= 12)
|
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
|
||||||
&& _deviceActionService.SystemMajorVersion() >= 12)
|
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
|
||||||
{
|
{
|
||||||
var timeoutAction = await _stateService.GetVaultTimeoutActionAsync();
|
var timeoutAction = await _stateService.GetVaultTimeoutActionAsync();
|
||||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
if (timeoutAction == VaultTimeoutAction.Logout && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,8 +188,12 @@ namespace Bit.iOS
|
|||||||
|
|
||||||
public override void OnResignActivation(UIApplication uiApplication)
|
public override void OnResignActivation(UIApplication uiApplication)
|
||||||
{
|
{
|
||||||
if (UIApplication.SharedApplication.KeyWindow != null)
|
if (UIApplication.SharedApplication.KeyWindow is null)
|
||||||
{
|
{
|
||||||
|
base.OnResignActivation(uiApplication);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
|
||||||
{
|
{
|
||||||
Tag = SPLASH_VIEW_TAG
|
Tag = SPLASH_VIEW_TAG
|
||||||
@ -213,7 +215,7 @@ namespace Bit.iOS
|
|||||||
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
|
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
|
||||||
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
|
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
|
||||||
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
|
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
|
||||||
}
|
|
||||||
base.OnResignActivation(uiApplication);
|
base.OnResignActivation(uiApplication);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,17 +306,6 @@ namespace Bit.iOS
|
|||||||
// Migration services
|
// Migration services
|
||||||
ServiceContainer.Register<INativeLogService>("nativeLogService", new ConsoleLogService());
|
ServiceContainer.Register<INativeLogService>("nativeLogService", new ConsoleLogService());
|
||||||
|
|
||||||
// Note: This might cause a race condition. Investigate more.
|
|
||||||
//Task.Run(() =>
|
|
||||||
//{
|
|
||||||
// FFImageLoading.Forms.Platform.CachedImageRenderer.Init();
|
|
||||||
// FFImageLoading.ImageService.Instance.Initialize(new FFImageLoading.Config.Configuration
|
|
||||||
// {
|
|
||||||
// FadeAnimationEnabled = false,
|
|
||||||
// FadeAnimationForCachedImages = false
|
|
||||||
// });
|
|
||||||
//});
|
|
||||||
|
|
||||||
iOSCoreHelpers.RegisterLocalServices();
|
iOSCoreHelpers.RegisterLocalServices();
|
||||||
RegisterPush();
|
RegisterPush();
|
||||||
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using Bit.Core.Enums;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Models.Domain;
|
using Bit.Core.Models.Domain;
|
||||||
|
|
||||||
namespace Bit.Core.Models.View
|
namespace Bit.Core.Models.View
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using AuthenticationServices;
|
using AuthenticationServices;
|
||||||
using Bit.App.Abstractions;
|
using Bit.App.Abstractions;
|
||||||
@ -104,9 +103,49 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (credentialRequest)
|
||||||
|
{
|
||||||
|
case ASPasswordCredentialRequest passwordRequest:
|
||||||
|
await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||||
|
break;
|
||||||
|
case ASPasskeyCredentialRequest passkeyRequest:
|
||||||
|
await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProvidingCredentialException(Exception ex)
|
||||||
|
{
|
||||||
|
//LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
|
UIPasteboard.General.String = ex.ToString();
|
||||||
|
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity)
|
||||||
{
|
{
|
||||||
InitAppIfNeeded();
|
InitAppIfNeeded();
|
||||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
||||||
@ -118,19 +157,62 @@ namespace Bit.iOS.Autofill
|
|||||||
ExtensionContext.CancelRequest(err);
|
ExtensionContext.CancelRequest(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_context.CredentialIdentity = credentialIdentity;
|
_context.PasswordCredentialIdentity = credentialIdentity;
|
||||||
await ProvideCredentialAsync(false);
|
await ProvideCredentialAsync(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialIdentity passkeyIdentity)
|
||||||
|
{
|
||||||
|
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.PasskeyCredentialIdentity = passkeyIdentity;
|
||||||
|
await ProvideCredentialAsync(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (credentialRequest)
|
||||||
|
{
|
||||||
|
case ASPasswordCredentialRequest passwordRequest:
|
||||||
|
PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity);
|
||||||
|
break;
|
||||||
|
case ASPasskeyCredentialRequest passkeyRequest:
|
||||||
|
await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialIdentity = passkeyRequest.CredentialIdentity as ASPasskeyCredentialIdentity);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
OnProvidingCredentialException(ex);
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnProvidingCredentialException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
|
||||||
{
|
{
|
||||||
InitAppIfNeeded();
|
InitAppIfNeeded();
|
||||||
if (!await IsAuthed())
|
if (!await IsAuthed())
|
||||||
@ -138,15 +220,10 @@ namespace Bit.iOS.Autofill
|
|||||||
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_context.CredentialIdentity = credentialIdentity;
|
updateContext(_context);
|
||||||
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
await CheckLockAsync(async () => await ProvideCredentialAsync());
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void PrepareInterfaceForExtensionConfiguration()
|
public override async void PrepareInterfaceForExtensionConfiguration()
|
||||||
{
|
{
|
||||||
@ -205,6 +282,23 @@ namespace Bit.iOS.Autofill
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
||||||
|
{
|
||||||
|
if (_context == null)
|
||||||
|
{
|
||||||
|
ServiceContainer.Reset();
|
||||||
|
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSRunLoop.Main.BeginInvokeOnMainThread(() =>
|
||||||
|
{
|
||||||
|
ServiceContainer.Reset();
|
||||||
|
ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -268,7 +362,7 @@ namespace Bit.iOS.Autofill
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_context.CredentialIdentity != null)
|
if (_context.PasswordCredentialIdentity != null)
|
||||||
{
|
{
|
||||||
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
|
||||||
return;
|
return;
|
||||||
@ -295,63 +389,85 @@ namespace Bit.iOS.Autofill
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CancelRequest(ASExtensionErrorCode code)
|
||||||
|
{
|
||||||
|
//var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null);
|
||||||
|
var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code);
|
||||||
|
ExtensionContext?.CancelRequest(err);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ProvideCredentialAsync(bool userInteraction = true)
|
private async Task ProvideCredentialAsync(bool userInteraction = true)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
|
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
|
||||||
Bit.Core.Models.Domain.Cipher cipher = null;
|
||
|
||||||
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
|
_context.RecordIdentifier == null)
|
||||||
if (!cancel)
|
|
||||||
{
|
{
|
||||||
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
|
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||||
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
|
return;
|
||||||
}
|
}
|
||||||
if (cancel)
|
|
||||||
|
var cipher = await cipherService.GetAsync(_context.RecordIdentifier);
|
||||||
|
if (cipher?.Login is null || cipher.Type != CipherType.Login)
|
||||||
{
|
{
|
||||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||||
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
|
|
||||||
ExtensionContext?.CancelRequest(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var decCipher = await cipher.DecryptAsync();
|
var decCipher = await cipher.DecryptAsync();
|
||||||
if (decCipher.Reprompt != Bit.Core.Enums.CipherRepromptType.None)
|
|
||||||
|
if (_context.PasskeyCredentialIdentity != null && !decCipher.Login.HasFido2Credentials)
|
||||||
|
{
|
||||||
|
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decCipher.Reprompt != CipherRepromptType.None)
|
||||||
{
|
{
|
||||||
// Prompt for password using either the lock screen or dialog unless
|
// Prompt for password using either the lock screen or dialog unless
|
||||||
// already verified the password.
|
// already verified the password.
|
||||||
if (!userInteraction)
|
if (!userInteraction)
|
||||||
{
|
{
|
||||||
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
|
||||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||||
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
|
|
||||||
ExtensionContext?.CancelRequest(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
|
||||||
|
if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
|
||||||
{
|
{
|
||||||
// Add a timeout to resolve keyboard not always showing up.
|
// Add a timeout to resolve keyboard not always showing up.
|
||||||
await Task.Delay(250);
|
await Task.Delay(250);
|
||||||
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
|
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
|
||||||
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync())
|
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync())
|
||||||
{
|
{
|
||||||
var err = new NSError(new NSString("ASExtensionErrorDomain"),
|
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
||||||
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
|
|
||||||
ExtensionContext?.CancelRequest(err);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && _context.IsPasskey)
|
||||||
|
{
|
||||||
|
CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||||
|
decCipher.Login.MainFido2Credential.UserHandle,
|
||||||
|
decCipher.Login.MainFido2Credential.RpId,
|
||||||
|
"qweq",
|
||||||
|
"adfas",
|
||||||
|
"adfas",
|
||||||
|
decCipher.Login.MainFido2Credential.CredentialId
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string totpCode = null;
|
string totpCode = null;
|
||||||
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
|
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
|
||||||
if (!disableTotpCopy.GetValueOrDefault(false))
|
|
||||||
{
|
{
|
||||||
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
|
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp)
|
||||||
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
|
&&
|
||||||
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
|
(cipher.OrganizationUseTotp || await _stateService.Value.CanAccessPremiumAsync()))
|
||||||
{
|
{
|
||||||
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
|
totpCode = await ServiceContainer.Resolve<ITotpService>().GetCodeAsync(decCipher.Login.Totp);
|
||||||
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -360,7 +476,7 @@ namespace Bit.iOS.Autofill
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||||
throw;
|
CancelRequest(ASExtensionErrorCode.Failed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,8 +93,15 @@
|
|||||||
<string>com.apple.authentication-services-credential-provider-ui</string>
|
<string>com.apple.authentication-services-credential-provider-ui</string>
|
||||||
<key>NSExtensionAttributes</key>
|
<key>NSExtensionAttributes</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
|
<key>ASCredentialProviderExtensionCapabilities</key>
|
||||||
|
<dict>
|
||||||
|
<key>ProvidesPasskeys</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>ProvidesPasswords</key>
|
||||||
|
<true/>
|
||||||
|
<key>ShowsConfigurationUI</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -57,7 +57,7 @@ namespace Bit.iOS.Autofill
|
|||||||
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
Core.Constants.AutofillNeedsIdentityReplacementKey);
|
||||||
if (needsAutofillReplacement.GetValueOrDefault())
|
if (needsAutofillReplacement.GetValueOrDefault())
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using AuthenticationServices;
|
using AuthenticationServices;
|
||||||
using Bit.iOS.Core.Models;
|
using Bit.iOS.Core.Models;
|
||||||
using Foundation;
|
using Foundation;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Autofill.Models
|
namespace Bit.iOS.Autofill.Models
|
||||||
{
|
{
|
||||||
@ -8,7 +9,28 @@ namespace Bit.iOS.Autofill.Models
|
|||||||
{
|
{
|
||||||
public NSExtensionContext ExtContext { get; set; }
|
public NSExtensionContext ExtContext { get; set; }
|
||||||
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
public ASCredentialServiceIdentifier[] ServiceIdentifiers { get; set; }
|
||||||
public ASPasswordCredentialIdentity CredentialIdentity { get; set; }
|
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||||
|
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get; set; }
|
||||||
public bool Configuring { get; set; }
|
public bool Configuring { get; set; }
|
||||||
|
|
||||||
|
public string? RecordIdentifier
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PasswordCredentialIdentity?.RecordIdentifier is string id)
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return PasskeyCredentialIdentity?.RecordIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsPasskey => PasskeyCredentialIdentity != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ namespace Bit.iOS.Autofill
|
|||||||
|
|
||||||
BackButton.Title = AppResources.Back;
|
BackButton.Title = AppResources.Back;
|
||||||
base.ViewDidLoad();
|
base.ViewDidLoad();
|
||||||
var task = ASHelpers.ReplaceAllIdentities();
|
var task = ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void BackButton_Activated(UIBarButtonItem sender)
|
partial void BackButton_Activated(UIBarButtonItem sender)
|
||||||
|
@ -188,18 +188,17 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
await _cipherService.SaveWithServerAsync(cipherDomain);
|
await _cipherService.SaveWithServerAsync(cipherDomain);
|
||||||
await loadingAlert.DismissViewControllerAsync(true);
|
await loadingAlert.DismissViewControllerAsync(true);
|
||||||
await _storageService.SaveAsync(Bit.Core.Constants.ClearCiphersCacheKey, true);
|
await _storageService.SaveAsync(Bit.Core.Constants.ClearCiphersCacheKey, true);
|
||||||
if (await ASHelpers.IdentitiesCanIncremental())
|
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
|
||||||
{
|
{
|
||||||
var identity = await ASHelpers.GetCipherIdentityAsync(cipherDomain.Id);
|
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherDomain.Id);
|
||||||
if (identity != null)
|
if (identity != null)
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(
|
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
|
||||||
new ASPasswordCredentialIdentity[] { identity });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
Success(cipherDomain.Id);
|
Success(cipherDomain.Id);
|
||||||
}
|
}
|
||||||
@ -229,7 +228,7 @@ namespace Bit.iOS.Core.Controllers
|
|||||||
var appOptions = new AppOptions { IosExtension = true };
|
var appOptions = new AppOptions { IosExtension = true };
|
||||||
var app = new App.App(appOptions);
|
var app = new App.App(appOptions);
|
||||||
|
|
||||||
var generatorPage = new GeneratorPage(false, selectAction: async (username) =>
|
var generatorPage = new GeneratorPage(false, selectAction: (username) =>
|
||||||
{
|
{
|
||||||
UsernameCell.TextField.Text = username;
|
UsernameCell.TextField.Text = username;
|
||||||
DismissViewController(false, null);
|
DismissViewController(false, null);
|
||||||
|
@ -375,7 +375,7 @@ namespace Bit.iOS.Core.Services
|
|||||||
|
|
||||||
public async Task OnAccountSwitchCompleteAsync()
|
public async Task OnAccountSwitchCompleteAsync()
|
||||||
{
|
{
|
||||||
await ASHelpers.ReplaceAllIdentities();
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetScreenCaptureAllowedAsync()
|
public Task SetScreenCaptureAllowedAsync()
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
using AuthenticationServices;
|
||||||
|
using Foundation;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
|
namespace Bit.iOS.Core.Utilities
|
||||||
|
{
|
||||||
|
public static class ASCredentialIdentityStoreExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Saves password credential identities to the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||||
|
/// Note: This is added to provide the proper method depending on the OS version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identities">Password identities to save</param>
|
||||||
|
public static Task<Tuple<bool, NSError>> SaveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentityEntriesAsync(identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes password credential identities of the shared store of <see cref="ASCredentialIdentityStore"/>
|
||||||
|
/// Note: This is added to provide the proper method depending on the OS version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identities">Password identities to remove</param>
|
||||||
|
public static Task<Tuple<bool, NSError>> RemoveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities)
|
||||||
|
{
|
||||||
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
|
{
|
||||||
|
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentityEntriesAsync(identities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentitiesAsync(identities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,93 +1,124 @@
|
|||||||
using System.Collections.Generic;
|
using AuthenticationServices;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AuthenticationServices;
|
|
||||||
using Bit.Core.Abstractions;
|
using Bit.Core.Abstractions;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.View;
|
using Bit.Core.Models.View;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using UIKit;
|
||||||
|
|
||||||
namespace Bit.iOS.Core.Utilities
|
namespace Bit.iOS.Core.Utilities
|
||||||
{
|
{
|
||||||
public static class ASHelpers
|
public static class ASHelpers
|
||||||
{
|
{
|
||||||
public static async Task ReplaceAllIdentities()
|
public static async Task ReplaceAllIdentitiesAsync()
|
||||||
{
|
{
|
||||||
if (await AutofillEnabled())
|
if (!await IsAutofillEnabledAsync())
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
|
|
||||||
|
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
|
||||||
if (await vaultTimeoutService.IsLockedAsync())
|
if (await vaultTimeoutService.IsLockedAsync())
|
||||||
{
|
{
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
|
||||||
var identities = new List<ASPasswordCredentialIdentity>();
|
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||||
var ciphers = await cipherService.GetAllDecryptedAsync();
|
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||||
foreach (var cipher in ciphers.Where(x => !x.IsDeleted))
|
|
||||||
{
|
{
|
||||||
var identity = ToCredentialIdentity(cipher);
|
await ReplaceAllIdentitiesIOS17Async(cipherService, storageService);
|
||||||
if (identity != null)
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
identities.Add(identity);
|
await ReplaceAllIdentitiesIOS12Async(cipherService, storageService);
|
||||||
}
|
|
||||||
}
|
|
||||||
if (identities.Any())
|
|
||||||
{
|
|
||||||
await ASCredentialIdentityStore.SharedStore?.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
|
||||||
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> IdentitiesCanIncremental()
|
private static async Task ReplaceAllIdentitiesIOS12Async(ICipherService cipherService, IStorageService storageService)
|
||||||
{
|
{
|
||||||
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
|
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||||
|
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||||
|
.Select(ToPasswordCredentialIdentity)
|
||||||
|
.Where(i => i != null)
|
||||||
|
.Cast<ASPasswordCredentialIdentity>()
|
||||||
|
.ToList();
|
||||||
|
if (!identities.Any())
|
||||||
|
{
|
||||||
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA1422 // Validate platform compatibility
|
||||||
|
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentitiesAsync(identities.ToArray());
|
||||||
|
#pragma warning restore CA1422 // Validate platform compatibility
|
||||||
|
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ReplaceAllIdentitiesIOS17Async(ICipherService cipherService, IStorageService storageService)
|
||||||
|
{
|
||||||
|
var ciphers = await cipherService.GetAllDecryptedAsync();
|
||||||
|
var identities = ciphers.Where(c => !c.IsDeleted)
|
||||||
|
.Select(ToCredentialIdentity)
|
||||||
|
.Where(i => i != null)
|
||||||
|
.Cast<IASCredentialIdentity>()
|
||||||
|
.ToList();
|
||||||
|
if (!identities.Any())
|
||||||
|
{
|
||||||
|
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ASCredentialIdentityStore.SharedStore.ReplaceCredentialIdentityEntriesAsync(identities.ToArray());
|
||||||
|
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> IdentitiesSupportIncrementalAsync()
|
||||||
|
{
|
||||||
|
var stateService = ServiceContainer.Resolve<IStateService>();
|
||||||
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
|
||||||
if (timeoutAction == VaultTimeoutAction.Logout)
|
if (timeoutAction == VaultTimeoutAction.Logout)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||||
return state != null && state.Enabled && state.SupportsIncrementalUpdates;
|
return state != null && state.Enabled && state.SupportsIncrementalUpdates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<bool> AutofillEnabled()
|
public static async Task<bool> IsAutofillEnabledAsync()
|
||||||
{
|
{
|
||||||
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
|
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
|
||||||
return state != null && state.Enabled;
|
return state != null && state.Enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<ASPasswordCredentialIdentity> GetCipherIdentityAsync(string cipherId)
|
public static async Task<ASPasswordCredentialIdentity?> GetCipherPasswordIdentityAsync(string cipherId)
|
||||||
{
|
{
|
||||||
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
|
var cipherService = ServiceContainer.Resolve<ICipherService>();
|
||||||
var cipher = await cipherService.GetAsync(cipherId);
|
var cipher = await cipherService.GetAsync(cipherId);
|
||||||
if (cipher == null)
|
if (cipher == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var cipherView = await cipher.DecryptAsync();
|
var cipherView = await cipher.DecryptAsync();
|
||||||
return ToCredentialIdentity(cipherView);
|
return ToPasswordCredentialIdentity(cipherView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ASPasswordCredentialIdentity ToCredentialIdentity(CipherView cipher)
|
public static ASPasswordCredentialIdentity? ToPasswordCredentialIdentity(CipherView cipher)
|
||||||
{
|
{
|
||||||
if (!cipher?.Login?.Uris?.Any() ?? true)
|
if (cipher?.Login?.Uris?.Any() != true)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != Bit.Core.Enums.UriMatchType.Never)?.Uri;
|
var uri = cipher.Login.Uris.FirstOrDefault(u => u.Match != UriMatchType.Never)?.Uri;
|
||||||
if (string.IsNullOrWhiteSpace(uri))
|
if (string.IsNullOrWhiteSpace(uri))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -100,5 +131,24 @@ namespace Bit.iOS.Core.Utilities
|
|||||||
var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url);
|
var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url);
|
||||||
return new ASPasswordCredentialIdentity(serviceId, username, cipher.Id);
|
return new ASPasswordCredentialIdentity(serviceId, username, cipher.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IASCredentialIdentity? ToCredentialIdentity(CipherView cipher)
|
||||||
|
{
|
||||||
|
if (!cipher.HasFido2Credential)
|
||||||
|
{
|
||||||
|
return ToPasswordCredentialIdentity(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cipher.Login.MainFido2Credential.IsDiscoverable)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId,
|
||||||
|
cipher.Login.MainFido2Credential.UserName,
|
||||||
|
cipher.Login.MainFido2Credential.CredentialId,
|
||||||
|
cipher.Login.MainFido2Credential.UserHandle,
|
||||||
|
cipher.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user