diff --git a/src/App/Platforms/iOS/AppDelegate.cs b/src/App/Platforms/iOS/AppDelegate.cs index 98d55b3ba..48ddf1ba2 100644 --- a/src/App/Platforms/iOS/AppDelegate.cs +++ b/src/App/Platforms/iOS/AppDelegate.cs @@ -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("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("deviceActionService"); diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs index df8a47eb4..0d0830731 100644 --- a/src/Core/Models/View/CipherView.cs +++ b/src/Core/Models/View/CipherView.cs @@ -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 diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs index 049d82047..92d3ff85b 100644 --- a/src/Core/Models/View/Fido2CredentialView.cs +++ b/src/Core/Models/View/Fido2CredentialView.cs @@ -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 diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index cafbc335c..c15b62561 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -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 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("cipherService", true); - Bit.Core.Models.Domain.Cipher cipher = null; - var cancel = cipherService == null || _context.CredentialIdentity?.RecordIdentifier == null; - if (!cancel) + if (!ServiceContainer.TryResolve(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("passwordRepromptService"); + var passwordRepromptService = ServiceContainer.Resolve(); 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("totpService"); - totpCode = await totpService.GetCodeAsync(decCipher.Login.Totp); + totpCode = await ServiceContainer.Resolve().GetCodeAsync(decCipher.Login.Totp); } } @@ -360,7 +476,7 @@ namespace Bit.iOS.Autofill catch (Exception ex) { LoggerHelper.LogEvenIfCantBeResolved(ex); - throw; + CancelRequest(ASExtensionErrorCode.Failed); } } diff --git a/src/iOS.Autofill/Info.plist b/src/iOS.Autofill/Info.plist index e22e3d512..a52e4ed12 100644 --- a/src/iOS.Autofill/Info.plist +++ b/src/iOS.Autofill/Info.plist @@ -93,8 +93,15 @@ com.apple.authentication-services-credential-provider-ui NSExtensionAttributes - ASCredentialProviderExtensionShowsConfigurationUI - + ASCredentialProviderExtensionCapabilities + + ProvidesPasskeys + + ProvidesPasswords + + ShowsConfigurationUI + + diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index f53f5cb0c..96c816846 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -57,7 +57,7 @@ namespace Bit.iOS.Autofill Core.Constants.AutofillNeedsIdentityReplacementKey); if (needsAutofillReplacement.GetValueOrDefault()) { - await ASHelpers.ReplaceAllIdentities(); + await ASHelpers.ReplaceAllIdentitiesAsync(); } _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index a9f5e66af..005a235ee 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -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; } } diff --git a/src/iOS.Autofill/SetupViewController.cs b/src/iOS.Autofill/SetupViewController.cs index 438d5ada4..f7bae9d0d 100644 --- a/src/iOS.Autofill/SetupViewController.cs +++ b/src/iOS.Autofill/SetupViewController.cs @@ -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) diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs index 385655fc1..0e52c5a57 100644 --- a/src/iOS.Core/Controllers/LoginAddViewController.cs +++ b/src/iOS.Core/Controllers/LoginAddViewController.cs @@ -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); diff --git a/src/iOS.Core/Services/DeviceActionService.cs b/src/iOS.Core/Services/DeviceActionService.cs index 3b91a5da4..b649db1d6 100644 --- a/src/iOS.Core/Services/DeviceActionService.cs +++ b/src/iOS.Core/Services/DeviceActionService.cs @@ -375,7 +375,7 @@ namespace Bit.iOS.Core.Services public async Task OnAccountSwitchCompleteAsync() { - await ASHelpers.ReplaceAllIdentities(); + await ASHelpers.ReplaceAllIdentitiesAsync(); } public Task SetScreenCaptureAllowedAsync() diff --git a/src/iOS.Core/Utilities/ASCredentialIdentityStoreExtensions.cs b/src/iOS.Core/Utilities/ASCredentialIdentityStoreExtensions.cs new file mode 100644 index 000000000..3761bb3b9 --- /dev/null +++ b/src/iOS.Core/Utilities/ASCredentialIdentityStoreExtensions.cs @@ -0,0 +1,39 @@ +using AuthenticationServices; +using Foundation; +using UIKit; + +namespace Bit.iOS.Core.Utilities +{ + public static class ASCredentialIdentityStoreExtensions + { + /// + /// Saves password credential identities to the shared store of + /// Note: This is added to provide the proper method depending on the OS version. + /// + /// Password identities to save + public static Task> SaveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities) + { + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentityEntriesAsync(identities); + } + + return ASCredentialIdentityStore.SharedStore.SaveCredentialIdentitiesAsync(identities); + } + + /// + /// Removes password credential identities of the shared store of + /// Note: This is added to provide the proper method depending on the OS version. + /// + /// Password identities to remove + public static Task> RemoveCredentialIdentitiesAsync(params ASPasswordCredentialIdentity[] identities) + { + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentityEntriesAsync(identities); + } + + return ASCredentialIdentityStore.SharedStore.RemoveCredentialIdentitiesAsync(identities); + } + } +} diff --git a/src/iOS.Core/Utilities/ASHelpers.cs b/src/iOS.Core/Utilities/ASHelpers.cs index dfee195c7..4072ad5fa 100644 --- a/src/iOS.Core/Utilities/ASHelpers.cs +++ b/src/iOS.Core/Utilities/ASHelpers.cs @@ -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("storageService"); - var stateService = ServiceContainer.Resolve("stateService"); - var timeoutAction = await stateService.GetVaultTimeoutActionAsync(); - if (timeoutAction == VaultTimeoutAction.Logout) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - return; - } - var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); - if (await vaultTimeoutService.IsLockedAsync()) - { - await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync(); - await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true); - return; - } - var cipherService = ServiceContainer.Resolve("cipherService"); - var identities = new List(); - 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("storageService"); + var stateService = ServiceContainer.Resolve(); + var timeoutAction = await stateService.GetVaultTimeoutActionAsync(); + if (timeoutAction == VaultTimeoutAction.Logout) + { + await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync(); + return; + } + + var vaultTimeoutService = ServiceContainer.Resolve(); + if (await vaultTimeoutService.IsLockedAsync()) + { + await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync(); + await storageService.SaveAsync(Constants.AutofillNeedsIdentityReplacementKey, true); + return; + } + + var cipherService = ServiceContainer.Resolve(); + if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + await ReplaceAllIdentitiesIOS17Async(cipherService, storageService); + } + else + { + await ReplaceAllIdentitiesIOS12Async(cipherService, storageService); } } - public static async Task IdentitiesCanIncremental() + private static async Task ReplaceAllIdentitiesIOS12Async(ICipherService cipherService, IStorageService storageService) { - var stateService = ServiceContainer.Resolve("stateService"); + var ciphers = await cipherService.GetAllDecryptedAsync(); + var identities = ciphers.Where(c => !c.IsDeleted) + .Select(ToPasswordCredentialIdentity) + .Where(i => i != null) + .Cast() + .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() + .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 IdentitiesSupportIncrementalAsync() + { + var stateService = ServiceContainer.Resolve(); 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 AutofillEnabled() + public static async Task IsAutofillEnabledAsync() { - var state = await ASCredentialIdentityStore.SharedStore?.GetCredentialIdentityStoreStateAsync(); + var state = await ASCredentialIdentityStore.SharedStore.GetCredentialIdentityStoreStateAsync(); return state != null && state.Enabled; } - public static async Task GetCipherIdentityAsync(string cipherId) + public static async Task GetCipherPasswordIdentityAsync(string cipherId) { - var cipherService = ServiceContainer.Resolve("cipherService"); + var cipherService = ServiceContainer.Resolve(); 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); + } } }