diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index cbde94e58..78c237a90 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -36,28 +36,42 @@ namespace Bit.Core.Services throw new NotSupportedError(); } - await userInterface.EnsureUnlockedVaultAsync(); - await _syncService.FullSyncAsync(false); + string cipherId = null; + var userVerified = false; + var accountSwitched = false; - var existingCipherIds = await FindExcludedCredentialsAsync( - makeCredentialParams.ExcludeCredentialDescriptorList - ); - if (existingCipherIds.Length > 0) + do { - await userInterface.InformExcludedCredentialAsync(existingCipherIds); - throw new NotAllowedError(); - } + try + { + accountSwitched = false; - var response = await userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams - { - CredentialName = makeCredentialParams.RpEntity.Name, - UserName = makeCredentialParams.UserEntity.Name, - UserVerificationPreference = makeCredentialParams.UserVerificationPreference, - RpId = makeCredentialParams.RpEntity.Id - }); + await userInterface.EnsureUnlockedVaultAsync(); + await _syncService.FullSyncAsync(false); + + var existingCipherIds = await FindExcludedCredentialsAsync( + makeCredentialParams.ExcludeCredentialDescriptorList + ); + if (existingCipherIds.Length > 0) + { + await userInterface.InformExcludedCredentialAsync(existingCipherIds); + throw new NotAllowedError(); + } + + (cipherId, userVerified) = await userInterface.ConfirmNewCredentialAsync(new Fido2ConfirmNewCredentialParams + { + CredentialName = makeCredentialParams.RpEntity.Name, + UserName = makeCredentialParams.UserEntity.Name, + UserVerificationPreference = makeCredentialParams.UserVerificationPreference, + RpId = makeCredentialParams.RpEntity.Id + }); + } + catch (AccountSwitchedException) + { + accountSwitched = true; + } + } while (accountSwitched); - var cipherId = response.CipherId; - var userVerified = response.UserVerified; string credentialId; if (cipherId == null) { @@ -118,37 +132,52 @@ namespace Bit.Core.Services public async Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface) { - List cipherOptions; + List cipherOptions = new List(); - await userInterface.EnsureUnlockedVaultAsync(); - await _syncService.FullSyncAsync(false); + string selectedCipherId = null; + var userVerified = false; + var accountSwitched = false; - if (assertionParams.AllowCredentialDescriptorList?.Length > 0) + do { - cipherOptions = await FindCredentialsByIdAsync( - assertionParams.AllowCredentialDescriptorList, - assertionParams.RpId - ); - } - else - { - cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId); - } - - if (cipherOptions.Count == 0) - { - throw new NotAllowedError(); - } - - var response = await userInterface.PickCredentialAsync( - cipherOptions.Select((cipher) => new Fido2GetAssertionUserInterfaceCredential + try { - CipherId = cipher.Id, - UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.GetUserVerificationPreferenceFrom(assertionParams.UserVerificationPreference, cipher.Reprompt) - }).ToArray() - ); - var selectedCipherId = response.CipherId; - var userVerified = response.UserVerified; + accountSwitched = false; + + await userInterface.EnsureUnlockedVaultAsync(); + await _syncService.FullSyncAsync(false); + + if (assertionParams.AllowCredentialDescriptorList?.Length > 0) + { + cipherOptions = await FindCredentialsByIdAsync( + assertionParams.AllowCredentialDescriptorList, + assertionParams.RpId + ); + } + else + { + cipherOptions = await FindCredentialsByRpAsync(assertionParams.RpId); + } + + if (cipherOptions.Count == 0) + { + throw new NotAllowedError(); + } + + (selectedCipherId, userVerified) = await userInterface.PickCredentialAsync( + cipherOptions.Select((cipher) => new Fido2GetAssertionUserInterfaceCredential + { + CipherId = cipher.Id, + UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.GetUserVerificationPreferenceFrom(assertionParams.UserVerificationPreference, cipher.Reprompt) + }).ToArray() + ); + + } + catch (AccountSwitchedException) + { + accountSwitched = true; + } + } while (accountSwitched); var selectedCipher = cipherOptions.FirstOrDefault((c) => c.Id == selectedCipherId); if (selectedCipher == null) diff --git a/src/Core/Services/Logging/ClipLogger.cs b/src/Core/Services/Logging/ClipLogger.cs index 8d28a3eba..8c22ac1e2 100644 --- a/src/Core/Services/Logging/ClipLogger.cs +++ b/src/Core/Services/Logging/ClipLogger.cs @@ -38,7 +38,7 @@ // { // _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}"); //#if IOS -// UIPasteboard.General.String = _currentBreadcrumbs.ToString(); +// MainThread.BeginInvokeOnMainThread(() => UIPasteboard.General.String = _currentBreadcrumbs.ToString()); //#endif // } diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs index be9fbee46..f4f9c31b3 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorException.cs @@ -34,4 +34,11 @@ namespace Bit.Core.Utilities.Fido2 { } } + + public class AccountSwitchedException : Fido2AuthenticatorException + { + public AccountSwitchedException() : base(nameof(AccountSwitchedException)) + { + } + } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index 8e1486abb..b1664c85f 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -339,7 +339,7 @@ namespace Bit.iOS.Autofill { try { - PerformSegue(SegueConstants.LOGIN_LIST, this); + DismissViewController(false, () => PerformSegue(SegueConstants.LOGIN_LIST, this)); } catch (Exception ex) { @@ -352,26 +352,22 @@ namespace Bit.iOS.Autofill { if (_context.IsCreatingPasskey) { + if (!await IsAuthed() + || + await _vaultTimeoutService.Value.IsLoggedOutByTimeoutAsync() + || + await _vaultTimeoutService.Value.ShouldLogOutByTimeoutAsync()) + { + await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.HomeLogin); + return; + } + if (!await IsLocked()) { return; } - _context.UnlockVaultTcs?.SetCanceled(); - _context.UnlockVaultTcs = new TaskCompletionSource(); - MainThread.BeginInvokeOnMainThread(() => - { - try - { - PerformSegue(SegueConstants.LOCK, this); - } - catch (Exception ex) - { - LoggerHelper.LogEvenIfCantBeResolved(ex); - } - }); - - await _context.UnlockVaultTcs.Task; + await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.Lock); return; } @@ -381,5 +377,17 @@ namespace Bit.iOS.Autofill throw new InvalidOperationNeedsUIException("Not authed or locked"); } } + + private async Task NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget navTarget) + { + _context.UnlockVaultTcs?.TrySetCanceled(); + _context.UnlockVaultTcs = new TaskCompletionSource(); + await MainThread.InvokeOnMainThreadAsync(() => + { + DoNavigate(navTarget); + }); + + await _context.UnlockVaultTcs.Task; + } } } diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 2bcd37f5c..a1e6bbaa5 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -8,6 +8,7 @@ using Bit.App.Utilities; using Bit.App.Utilities.AccountManagement; using Bit.Core.Abstractions; using Bit.Core.Enums; +using Bit.Core.Models.Domain; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Core.Utilities.Fido2; @@ -34,6 +35,8 @@ namespace Bit.iOS.Autofill private readonly LazyResolve _stateService = new LazyResolve(); private readonly LazyResolve _conditionedAwaiterManager = new LazyResolve(); + private readonly LazyResolve _broadcasterService = new LazyResolve(); + private readonly LazyResolve _vaultTimeoutService = new LazyResolve(); public CredentialProviderViewController(IntPtr handle) : base(handle) @@ -377,7 +380,7 @@ namespace Bit.iOS.Autofill if (_context.IsCreatingPasskey) { - _context.UnlockVaultTcs.SetResult(true); + _context.UnlockVaultTcs.TrySetResult(true); return; } @@ -509,8 +512,7 @@ namespace Bit.iOS.Autofill private Task IsLocked() { - var vaultTimeoutService = ServiceContainer.Resolve("vaultTimeoutService"); - return vaultTimeoutService.IsLockedAsync(); + return _vaultTimeoutService.Value.IsLockedAsync(); } private Task IsAuthed() @@ -544,6 +546,8 @@ namespace Bit.iOS.Autofill { iOSCoreHelpers.InitApp(this, Bit.Core.Constants.iOSAutoFillClearCiphersCacheKey, _nfcSession, out _nfcDelegate, out _accountsManager); + + _broadcasterService.Value.Subscribe(nameof(CredentialProviderViewController), OnMessageReceived); } private async Task InitAppIfNeededAsync() @@ -556,6 +560,18 @@ namespace Bit.iOS.Autofill await _stateService.Value.ReloadStateAsync(); } + private void OnMessageReceived(Message message) + { + if (message?.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED + && + _context != null) + { + _context.VaultUnlockedDuringThisSession = false; + _context.PickCredentialForFido2CreationTcs?.TrySetException(new AccountSwitchedException()); + _context.PickCredentialForFido2GetAssertionFromListTcs?.TrySetException(new AccountSwitchedException()); + } + } + private void LaunchHomePage() { var appOptions = new AppOptions { IosExtension = true }; @@ -751,6 +767,24 @@ namespace Bit.iOS.Autofill public Task UpdateThemeAsync() => Task.CompletedTask; public void Navigate(NavigationTarget navTarget, INavigationParams navParams = null) + { + if (_context?.IsCreatingPasskey == true + && + _context.PickCredentialForFido2CreationTcs != null + && + !_context.PickCredentialForFido2CreationTcs.Task.IsCompleted) + { + // if it's creating passkey + // and we have an active pending TaskCompletionSource + // then we let the Fido2 Authenticator flow manage the navigation to avoid issues + // like duplicated navigation. + return; + } + + DoNavigate(navTarget, navParams); + } + + internal void DoNavigate(NavigationTarget navTarget, INavigationParams navParams = null) { switch (navTarget) { diff --git a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs index e780ba7db..8ba369983 100644 --- a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs +++ b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs @@ -31,7 +31,7 @@ namespace Bit.iOS.Autofill public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) { - _context.PickCredentialForFido2CreationTcs?.SetCanceled(); + _context.PickCredentialForFido2CreationTcs?.TrySetCanceled(); _context.PickCredentialForFido2CreationTcs = new TaskCompletionSource<(string, bool?)>(); _context.PasskeyCreationParams = confirmNewCredentialParams; diff --git a/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs index 115025b5c..c0dbe6044 100644 --- a/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs +++ b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs @@ -285,7 +285,7 @@ namespace Bit.iOS.Autofill.Utilities if (Context.IsPreparingListForPasskey && item.IsFido2ListItem) { - Context.PickCredentialForFido2GetAssertionFromListTcs.SetResult(item.Id); + Context.PickCredentialForFido2GetAssertionFromListTcs.TrySetResult(item.Id); return; } @@ -317,7 +317,12 @@ namespace Bit.iOS.Autofill.Utilities return; } - Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null)); + if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt)) + { + return; + } + + Context.PickCredentialForFido2CreationTcs.TrySetResult((item.Id, null)); } private async Task DeselectRowAndGetItemAsync(UITableView tableView, NSIndexPath indexPath) diff --git a/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs b/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs index d5dca7ee5..4709b3a34 100644 --- a/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs +++ b/src/iOS.Autofill/Utilities/Fido2GetAssertionFromListUserInterface.cs @@ -51,7 +51,7 @@ namespace Bit.iOS.Autofill.Utilities _onAllowedFido2Credentials?.Invoke(credentials.Select(c => c.CipherId).ToList() ?? new List()); - _context.PickCredentialForFido2GetAssertionFromListTcs?.SetCanceled(); + _context.PickCredentialForFido2GetAssertionFromListTcs?.TrySetCanceled(); _context.PickCredentialForFido2GetAssertionFromListTcs = new TaskCompletionSource(); var cipherId = await _context.PickCredentialForFido2GetAssertionFromListTcs.Task;