1
0
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:
Federico Maccaroni 2024-01-03 18:21:02 -03:00
parent a1e4f0aaa2
commit 275ae76761
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
12 changed files with 396 additions and 177 deletions

View File

@ -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");

View File

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

View File

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

View File

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

View File

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

View File

@ -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();

View File

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

View File

@ -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)

View File

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

View File

@ -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()

View File

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

View File

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