1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-11-25 12:05:59 +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);
if (needsAutofillReplacement.GetValueOrDefault())
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
else if (message.Command == "showAppExtension")
@ -103,7 +103,7 @@ namespace Bit.iOS
var success = data["successfully"] as bool?;
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
}
@ -112,65 +112,63 @@ namespace Bit.iOS
{
if (_deviceActionService.SystemMajorVersion() >= 12)
{
if (await ASHelpers.IdentitiesCanIncremental())
if (await ASHelpers.IdentitiesSupportIncrementalAsync())
{
var cipherId = message.Data as string;
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
{
var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
if (identity == null)
{
return;
}
await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
new ASPasswordCredentialIdentity[] { identity });
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
return;
}
}
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
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);
if (identity == null)
{
return;
}
await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
new ASPasswordCredentialIdentity[] { identity });
await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
return;
}
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
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")
&& _deviceActionService.SystemMajorVersion() >= 12)
{
await ASHelpers.ReplaceAllIdentities();
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
await ASHelpers.ReplaceAllIdentitiesAsync();
}
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
{
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
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
}
@ -190,30 +188,34 @@ namespace Bit.iOS
public override void OnResignActivation(UIApplication uiApplication)
{
if (UIApplication.SharedApplication.KeyWindow != null)
if (UIApplication.SharedApplication.KeyWindow is null)
{
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
{
Tag = SPLASH_VIEW_TAG
};
var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
{
BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform()
};
var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png");
var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices.
var imageView = new UIImageView(frame)
{
Image = logo,
Center = new CGPoint(view.Center.X, view.Center.Y - 30),
ContentMode = UIViewContentMode.ScaleAspectFit
};
view.AddSubview(backgroundView);
view.AddSubview(imageView);
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
base.OnResignActivation(uiApplication);
return;
}
var view = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
{
Tag = SPLASH_VIEW_TAG
};
var backgroundView = new UIView(UIApplication.SharedApplication.KeyWindow.Frame)
{
BackgroundColor = ThemeManager.GetResourceColor("SplashBackgroundColor").ToPlatform()
};
var logo = new UIImage(!ThemeManager.UsingLightTheme ? "logo_white.png" : "logo.png");
var frame = new CGRect(0, 0, 280, 100); //Setting image width to avoid it being larger and getting cropped on smaller devices. This harcoded size should be good even for very small devices.
var imageView = new UIImageView(frame)
{
Image = logo,
Center = new CGPoint(view.Center.X, view.Center.Y - 30),
ContentMode = UIViewContentMode.ScaleAspectFit
};
view.AddSubview(backgroundView);
view.AddSubview(imageView);
UIApplication.SharedApplication.KeyWindow.AddSubview(view);
UIApplication.SharedApplication.KeyWindow.BringSubviewToFront(view);
UIApplication.SharedApplication.KeyWindow.EndEditing(true);
base.OnResignActivation(uiApplication);
}
@ -304,17 +306,6 @@ namespace Bit.iOS
// Migration services
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();
RegisterPush();
var deviceActionService = ServiceContainer.Resolve<IDeviceActionService>("deviceActionService");

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.View

View File

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using Bit.Core.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.View

View File

@ -1,5 +1,4 @@
using System;
using System.Text;
using System.Threading.Tasks;
using AuthenticationServices;
using Bit.App.Abstractions;
@ -104,27 +103,100 @@ 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)
{
try
{
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.CredentialIdentity = credentialIdentity;
await ProvideCredentialAsync(false);
await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
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();
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
if (!await IsAuthed() || await IsLocked())
{
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
ExtensionContext.CancelRequest(err);
return;
}
_context.PasswordCredentialIdentity = credentialIdentity;
await ProvideCredentialAsync(false);
}
private async Task 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)
{
OnProvidingCredentialException(ex);
}
}
@ -132,22 +204,27 @@ namespace Bit.iOS.Autofill
{
try
{
InitAppIfNeeded();
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
_context.CredentialIdentity = credentialIdentity;
await CheckLockAsync(async () => await ProvideCredentialAsync());
await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
OnProvidingCredentialException(ex);
}
}
private async Task PrepareInterfaceToProvideCredentialAsync(Action<Context> updateContext)
{
InitAppIfNeeded();
if (!await IsAuthed())
{
await _accountsManager.NavigateOnAccountChangeAsync(false);
return;
}
updateContext(_context);
await CheckLockAsync(async () => await ProvideCredentialAsync());
}
public override async void PrepareInterfaceForExtensionConfiguration()
{
try
@ -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)
{
try
@ -268,7 +362,7 @@ namespace Bit.iOS.Autofill
{
try
{
if (_context.CredentialIdentity != null)
if (_context.PasswordCredentialIdentity != null)
{
await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync());
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)
{
try
{
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService", true);
Bit.Core.Models.Domain.Cipher cipher = null;
var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null;
if (!cancel)
if (!ServiceContainer.TryResolve<ICipherService>(out var cipherService)
||
_context.RecordIdentifier == null)
{
cipher = await cipherService.GetAsync(_context.CredentialIdentity.RecordIdentifier);
cancel = cipher == null || cipher.Type != Bit.Core.Enums.CipherType.Login || cipher.Login == null;
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
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"),
Convert.ToInt32(ASExtensionErrorCode.CredentialIdentityNotFound), null);
ExtensionContext?.CancelRequest(err);
CancelRequest(ASExtensionErrorCode.CredentialIdentityNotFound);
return;
}
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
// already verified the password.
if (!userInteraction)
{
await _stateService.Value.SetPasswordRepromptAutofillAsync(true);
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null);
ExtensionContext?.CancelRequest(err);
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
return;
}
else if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
if (!await _stateService.Value.GetPasswordVerifiedAutofillAsync())
{
// Add a timeout to resolve keyboard not always showing up.
await Task.Delay(250);
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService");
var passwordRepromptService = ServiceContainer.Resolve<IPasswordRepromptService>();
if (!await passwordRepromptService.PromptAndCheckPasswordIfNeededAsync())
{
var err = new NSError(new NSString("ASExtensionErrorDomain"),
Convert.ToInt32(ASExtensionErrorCode.UserCanceled), null);
ExtensionContext?.CancelRequest(err);
CancelRequest(ASExtensionErrorCode.UserCanceled);
return;
}
}
}
string totpCode = null;
var disableTotpCopy = await _stateService.Value.GetDisableAutoTotpCopyAsync();
if (!disableTotpCopy.GetValueOrDefault(false))
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && _context.IsPasskey)
{
var canAccessPremiumAsync = await _stateService.Value.CanAccessPremiumAsync();
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp) &&
(canAccessPremiumAsync || cipher.OrganizationUseTotp))
CompleteAssertionRequest(new ASPasskeyAssertionCredential(
decCipher.Login.MainFido2Credential.UserHandle,
decCipher.Login.MainFido2Credential.RpId,
"qweq",
"adfas",
"adfas",
decCipher.Login.MainFido2Credential.CredentialId
));
return;
}
string totpCode = null;
if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true)
{
if (!string.IsNullOrWhiteSpace(decCipher.Login.Totp)
&&
(cipher.OrganizationUseTotp || await _stateService.Value.CanAccessPremiumAsync()))
{
var totpService = ServiceContainer.Resolve<ITotpService>("totpService");
totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp);
totpCode = await ServiceContainer.Resolve<ITotpService>().GetCodeAsync(decCipher.Login.Totp);
}
}
@ -360,7 +476,7 @@ namespace Bit.iOS.Autofill
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
throw;
CancelRequest(ASExtensionErrorCode.Failed);
}
}

View File

@ -93,8 +93,15 @@
<string>com.apple.authentication-services-credential-provider-ui</string>
<key>NSExtensionAttributes</key>
<dict>
<key>ASCredentialProviderExtensionShowsConfigurationUI</key>
<true/>
<key>ASCredentialProviderExtensionCapabilities</key>
<dict>
<key>ProvidesPasskeys</key>
<true/>
<key>ProvidesPasswords</key>
<true/>
<key>ShowsConfigurationUI</key>
<true/>
</dict>
</dict>
</dict>
</dict>

View File

@ -57,7 +57,7 @@ namespace Bit.iOS.Autofill
Core.Constants.AutofillNeedsIdentityReplacementKey);
if (needsAutofillReplacement.GetValueOrDefault())
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();

View File

@ -1,6 +1,7 @@
using AuthenticationServices;
using Bit.iOS.Core.Models;
using Foundation;
using UIKit;
namespace Bit.iOS.Autofill.Models
{
@ -8,7 +9,28 @@ namespace Bit.iOS.Autofill.Models
{
public NSExtensionContext ExtContext { 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 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;
base.ViewDidLoad();
var task = ASHelpers.ReplaceAllIdentities();
var task = ASHelpers.ReplaceAllIdentitiesAsync();
}
partial void BackButton_Activated(UIBarButtonItem sender)

View File

@ -188,18 +188,17 @@ namespace Bit.iOS.Core.Controllers
await _cipherService.SaveWithServerAsync(cipherDomain);
await loadingAlert.DismissViewControllerAsync(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)
{
await ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(
new ASPasswordCredentialIdentity[] { identity });
await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
}
}
else
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
Success(cipherDomain.Id);
}
@ -229,7 +228,7 @@ namespace Bit.iOS.Core.Controllers
var appOptions = new AppOptions { IosExtension = true };
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;
DismissViewController(false, null);

View File

@ -375,7 +375,7 @@ namespace Bit.iOS.Core.Services
public async Task OnAccountSwitchCompleteAsync()
{
await ASHelpers.ReplaceAllIdentities();
await ASHelpers.ReplaceAllIdentitiesAsync();
}
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 System.Linq;
using System.Threading.Tasks;
using AuthenticationServices;
using AuthenticationServices;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Models.View;
using Bit.Core.Utilities;
using UIKit;
namespace Bit.iOS.Core.Utilities
{
public static class ASHelpers
{
public static async Task ReplaceAllIdentities()
public static async Task ReplaceAllIdentitiesAsync()
{
if (await AutofillEnabled())
if (!await IsAutofillEnabledAsync())
{
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
var stateService = ServiceContainer.Resolve<IStateService>("stateService");
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
if (timeoutAction == VaultTimeoutAction.Logout)
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
return;
}
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>("vaultTimeoutService");
if (await vaultTimeoutService.IsLockedAsync())
{
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
return;
}
var cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
var identities = new List<ASPasswordCredentialIdentity>();
var ciphers = await cipherService.GetAllDecryptedAsync();
foreach (var cipher in ciphers.Where(x => !x.IsDeleted))
{
var identity = ToCredentialIdentity(cipher);
if (identity != null)
{
identities.Add(identity);
}
}
if (identities.Any())
{
await ASCredentialIdentityStore.SharedStore?.ReplaceCredentialIdentitiesAsync(identities.ToArray());
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, false);
return;
}
await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
return;
}
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
var stateService = ServiceContainer.Resolve<IStateService>();
var timeoutAction = await stateService.GetVaultTimeoutActionAsync();
if (timeoutAction == VaultTimeoutAction.Logout)
{
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
return;
}
var vaultTimeoutService = ServiceContainer.Resolve<IVaultTimeoutService>();
if (await vaultTimeoutService.IsLockedAsync())
{
await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true);
return;
}
var cipherService = ServiceContainer.Resolve<ICipherService>();
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
await ReplaceAllIdentitiesIOS17Async(cipherService, storageService);
}
else
{
await ReplaceAllIdentitiesIOS12Async(cipherService, storageService);
}
}
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();
if (timeoutAction == VaultTimeoutAction.Logout)
{
return false;
}
var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync();
var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync();
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;
}
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);
if (cipher == null)
{
return null;
}
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;
}
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))
{
return null;
@ -100,5 +131,24 @@ namespace Bit.iOS.Core.Utilities
var serviceId = new ASCredentialServiceIdentifier(uri, ASCredentialServiceIdentifierType.Url);
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);
}
}
}