1
0
mirror of https://github.com/bitwarden/mobile synced 2026-01-10 20:43:41 +00:00
Files
mobile/src/iOS.Autofill/LoginListViewController.cs
Dinis Vieira 856a084a47 [PM-7186] Fallback to password list on exception (#3127)
* PM-7186 Added fallback in case of exception that loads password list

* PM-7186 Added back the error message removed in last commit.
2024-04-02 20:52:21 +01:00

556 lines
21 KiB
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using AuthenticationServices;
using Bit.App.Controls;
using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Resources.Localization;
using Bit.Core.Services;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.iOS.Autofill.ListItems;
using Bit.iOS.Autofill.Models;
using Bit.iOS.Autofill.Utilities;
using Bit.iOS.Core.Controllers;
using Bit.iOS.Core.Utilities;
using Bit.iOS.Core.Views;
using CoreFoundation;
using Foundation;
using Microsoft.Maui.ApplicationModel;
using UIKit;
namespace Bit.iOS.Autofill
{
public partial class LoginListViewController : ExtendedUIViewController, ILoginListViewController
{
internal const string HEADER_SECTION_IDENTIFIER = "headerSectionId";
UIBarButtonItem _cancelButton;
UIControl _accountSwitchButton;
public LoginListViewController(IntPtr handle)
: base(handle)
{
DismissModalAction = Cancel;
}
public Context Context { get; set; }
public CredentialProviderViewController CPViewController { get; set; }
AccountSwitchingOverlayView _accountSwitchingOverlayView;
AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper;
LazyResolve<IBroadcasterService> _broadcasterService = new LazyResolve<IBroadcasterService>();
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
bool _alreadyLoadItemsOnce = false;
bool _isLoading;
private string NavTitle
{
get
{
if (Context.IsCreatingPasskey)
{
return AppResources.SavePasskey;
}
if (Context.IsCreatingOrPreparingListForPasskey)
{
return AppResources.Autofill;
}
return AppResources.Items;
}
}
private TableSource Source => (TableSource)TableView.Source;
public async override void ViewDidLoad()
{
try
{
_cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside);
base.ViewDidLoad();
SubscribeSyncCompleted();
NavItem.Title = NavTitle;
_cancelButton.Title = AppResources.Cancel;
_searchBar.Placeholder = AppResources.Search;
_searchBar.BackgroundColor = _searchBar.BarTintColor = ThemeHelpers.ListHeaderBackgroundColor;
_searchBar.UpdateThemeIfNeeded();
_searchBar.Delegate = new ExtensionSearchDelegate(TableView);
TableView.BackgroundColor = ThemeHelpers.BackgroundColor;
var tableSource = new TableSource(this);
TableView.Source = tableSource;
tableSource.RegisterTableViewCells(TableView);
if (Context.IsCreatingOrPreparingListForPasskey)
{
TableView.SectionHeaderHeight = 55;
TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER);
}
if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0))
{
TableView.SectionHeaderTopPadding = 0;
}
if (Context.IsCreatingPasskey)
{
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, Context.UrlString);
_emptyViewButton.SetTitle(AppResources.SavePasskeyAsNewLogin, UIControlState.Normal);
_emptyViewButton.Layer.BorderWidth = 2;
_emptyViewButton.Layer.BorderColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED).CGColor;
_emptyViewButton.Layer.CornerRadius = 10;
_emptyViewButton.ClipsToBounds = true;
}
var storageService = ServiceContainer.Resolve<IStorageService>("storageService");
var needsAutofillReplacement = await storageService.GetAsync<bool?>(
Core.Constants.AutofillNeedsIdentityReplacementKey);
if (needsAutofillReplacement.GetValueOrDefault())
{
await ASHelpers.ReplaceAllIdentitiesAsync();
}
_accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper();
_accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync();
_accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside;
NavItem.SetLeftBarButtonItems(new UIBarButtonItem[]
{
_cancelButton,
new UIBarButtonItem(_accountSwitchButton)
}, false);
_accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView);
if (Context.IsPreparingListForPasskey)
{
var fido2UserInterface = new Fido2GetAssertionFromListUserInterface(Context,
() => Task.CompletedTask,
() => Context?.VaultUnlockedDuringThisSession ?? false,
CPViewController.VerifyUserAsync,
Source.ReloadWithAllowedFido2Credentials);
DoFido2GetAssertionAsync(fido2UserInterface).FireAndForget();
}
else
{
await ReloadItemsAsync();
_alreadyLoadItemsOnce = true;
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
public async Task DoFido2GetAssertionAsync(IFido2GetAssertionUserInterface fido2GetAssertionUserInterface)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
CPViewController.OnProvidingCredentialException(new InvalidOperationException("Trying to get assertion request before iOS 17"));
return;
}
if (Context.PasskeyCredentialRequestParameters is null)
{
CPViewController.OnProvidingCredentialException(new InvalidOperationException("Trying to get assertion request without a PasskeyCredentialRequestParameters"));
return;
}
try
{
var fido2AssertionResult = await _fido2MediatorService.Value.GetAssertionAsync(new Fido2AuthenticatorGetAssertionParams
{
RpId = Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier,
Hash = Context.PasskeyCredentialRequestParameters.ClientDataHash.ToArray(),
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(Context.PasskeyCredentialRequestParameters.UserVerificationPreference),
AllowCredentialDescriptorList = Context.PasskeyCredentialRequestParameters.AllowedCredentials?
.Select(c => new PublicKeyCredentialDescriptor { Id = c.ToArray() })
.ToArray()
}, fido2GetAssertionUserInterface);
if (fido2AssertionResult.SelectedCredential is null)
{
throw new NullReferenceException("SelectedCredential must have a value");
}
await CPViewController.CompleteAssertionRequest(new ASPasskeyAssertionCredential(
NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle),
Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier,
NSData.FromArray(fido2AssertionResult.Signature),
Context.PasskeyCredentialRequestParameters.ClientDataHash,
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
));
}
catch (InvalidOperationNeedsUIException)
{
return;
}
catch (TaskCanceledException)
{
return;
}
catch
{
try
{
if (Context?.IsExecutingWithoutUserInteraction == false)
{
_ = _platformUtilsService.Value.ShowDialogAsync(
string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, Context.PasskeyCredentialRequestParameters.RelyingPartyIdentifier),
AppResources.ErrorReadingPasskey);
TableView.SectionHeaderHeight = 0;
Context.IsPasswordFallback = true;
await ReloadItemsAsync();
_alreadyLoadItemsOnce = true;
}
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
throw;
}
}
private void CancelButton_TouchUpInside(object sender, EventArgs e)
{
Cancel();
}
private void AccountSwitchedButton_TouchUpInside(object sender, EventArgs e)
{
_accountSwitchingOverlayHelper.OnToolbarItemActivated(_accountSwitchingOverlayView, OverlayView);
}
private void Cancel()
{
CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled);
}
partial void AddBarButton_Activated(UIBarButtonItem sender)
{
PerformSegue(SegueConstants.ADD_LOGIN, this);
}
partial void SearchBarButton_Activated(UIBarButtonItem sender)
{
try
{
if (!Context.IsCreatingOrPreparingListForPasskey)
{
PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this);
return;
}
if (_isLoading)
{
// if it's loading we simplify this logic to just avoid toggling the search bar visibility
// and reloading items while this is taking place.
return;
}
UIView.Animate(0.3f,
() =>
{
_tableViewTopToSearchBarConstraint.Active = !_tableViewTopToSearchBarConstraint.Active;
_searchBar.Hidden = !_searchBar.Hidden;
},
() =>
{
if (_tableViewTopToSearchBarConstraint.Active)
{
_searchBar?.BecomeFirstResponder();
if (Context.IsCreatingPasskey)
{
_emptyView.Hidden = true;
TableView.Hidden = false;
}
}
else
{
_searchBar.Text = string.Empty;
_searchBar.Text = null;
_searchBar.ResignFirstResponder();
ReloadItemsAsync().FireAndForget();
}
});
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
}
partial void EmptyButton_Activated(UIButton sender)
{
SavePasskeyAsNewLoginAsync().FireAndForget(ex =>
{
var message = AppResources.AnErrorHasOccurred;
if (ex is ApiException apiEx && apiEx.Error != null)
{
message = apiEx.Error.GetSingleMessage();
}
_platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, message).FireAndForget();
});
}
private async Task SavePasskeyAsNewLoginAsync()
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
{
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
return;
}
if (Context.PasskeyCreationParams is null)
{
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params."));
return;
}
bool? isUserVerified = null;
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
{
var verification = await VerifyUserAsync();
if (verification.IsCancelled)
{
return;
}
isUserVerified = verification.Result;
if (!isUserVerified.Value && await _userVerificationMediatorService.Value.ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions))
{
await _platformUtilsService.Value.ShowDialogAsync(AppResources.ErrorCreatingPasskey, AppResources.SavePasskey);
return;
}
}
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
try
{
PresentViewController(loadingAlert, true, null);
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCreationParams.Value);
Context.PickCredentialForFido2CreationTcs.TrySetResult((cipherId, isUserVerified));
}
catch
{
await loadingAlert.DismissViewControllerAsync(false);
throw;
}
}
private async Task<CancellableResult<bool>> VerifyUserAsync()
{
try
{
if (Context?.PasskeyCreationParams is null)
{
return new CancellableResult<bool>(false);
}
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(Fido2UserVerificationOptions);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return new CancellableResult<bool>(false);
}
}
private Fido2UserVerificationOptions Fido2UserVerificationOptions
{
get
{
ArgumentNullException.ThrowIfNull(Context);
ArgumentNullException.ThrowIfNull(Context.PasskeyCreationParams);
return new Fido2UserVerificationOptions
(
false,
Context.PasskeyCreationParams.Value.UserVerificationPreference,
Context.VaultUnlockedDuringThisSession,
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier
);
}
}
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
if (segue.DestinationViewController is UINavigationController navController)
{
if (navController.TopViewController is LoginAddViewController addLoginController)
{
addLoginController.Context = Context;
addLoginController.LoginListController = this;
segue.DestinationViewController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(addLoginController.DismissModalAction);
}
if (navController.TopViewController is LoginSearchViewController searchLoginController)
{
searchLoginController.Context = Context;
searchLoginController.CPViewController = CPViewController;
searchLoginController.FromList = true;
segue.DestinationViewController.PresentationController.Delegate =
new CustomPresentationControllerDelegate(searchLoginController.DismissModalAction);
}
}
}
private void SubscribeSyncCompleted()
{
_broadcasterService.Value.Subscribe(nameof(LoginListViewController), message =>
{
if (message.Command == "syncCompleted" && _alreadyLoadItemsOnce)
{
DispatchQueue.MainQueue.DispatchAsync(async () =>
{
try
{
await ReloadItemsAsync();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
});
}
});
}
public void OnItemsLoaded(string searchFilter)
{
if (Context.IsCreatingPasskey)
{
_emptyView.Hidden = !Source.IsEmpty;
TableView.Hidden = Source.IsEmpty;
if (Source.IsEmpty)
{
_emptyViewLabel.Text = string.Format(AppResources.NoItemsForUri, string.IsNullOrEmpty(searchFilter) ? Context.UrlString : searchFilter);
}
}
}
public override void ViewDidUnload()
{
base.ViewDidUnload();
_broadcasterService.Value.Unsubscribe(nameof(LoginListViewController));
}
public void DismissModal()
{
DismissViewController(true, async () =>
{
try
{
await ReloadItemsAsync();
}
catch (Exception ex)
{
_logger.Value.Exception(ex);
}
});
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (_accountSwitchButton != null)
{
_accountSwitchingOverlayHelper.DisposeAccountSwitchToolbarButtonItemImage(_accountSwitchButton);
_accountSwitchButton.TouchUpInside -= AccountSwitchedButton_TouchUpInside;
}
}
base.Dispose(disposing);
}
private async Task LoadSourceAsync()
{
_isLoading = true;
try
{
await MainThread.InvokeOnMainThreadAsync(() =>
{
TableView.Hidden = true;
_searchBar.Hidden = true;
_loadingView.Hidden = false;
});
await Source.LoadAsync(string.IsNullOrEmpty(_searchBar?.Text), _searchBar?.Text);
await MainThread.InvokeOnMainThreadAsync(() =>
{
_loadingView.Hidden = true;
TableView.Hidden = Context.IsCreatingPasskey && Source.IsEmpty;
_searchBar.Hidden = string.IsNullOrEmpty(_searchBar?.Text);
});
}
finally
{
_isLoading = false;
}
}
public async Task ReloadItemsAsync()
{
try
{
await LoadSourceAsync();
_alreadyLoadItemsOnce = true;
await MainThread.InvokeOnMainThreadAsync(TableView.ReloadData);
}
catch
{
_platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget();
throw;
}
}
public void ReloadTableViewData() => TableView.ReloadData();
public class TableSource : BaseLoginListTableSource<LoginListViewController>
{
public TableSource(LoginListViewController controller)
: base(controller)
{
}
protected override string LoginAddSegue => SegueConstants.ADD_LOGIN;
}
}
}