From db4219ac6ab88289cabe35341f1b29bd4617b5c4 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Wed, 21 Feb 2024 18:09:00 -0300 Subject: [PATCH] PM-5154 Implement iOS passkey add login --- .../Fido2MakeCredentialUserInterface.cs | 1 + src/iOS.Autofill/LoginAddViewController.cs | 65 ++++++++- src/iOS.Autofill/LoginListViewController.cs | 2 +- src/iOS.Autofill/Models/Context.cs | 2 + .../Controllers/LoginAddViewController.cs | 127 ++++++++++-------- 5 files changed, 140 insertions(+), 57 deletions(-) diff --git a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs index 0a154c83f6d..cd33f468598 100644 --- a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs +++ b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs @@ -22,6 +22,7 @@ public Fido2MakeCredentialUserInterface(Func ensureUnlockedVaultCallback, { _context.ConfirmNewCredentialTcs?.SetCanceled(); _context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>(); + _context.PasskeyCreationParams = confirmNewCredentialParams; _onConfirmingNewCredential(); diff --git a/src/iOS.Autofill/LoginAddViewController.cs b/src/iOS.Autofill/LoginAddViewController.cs index 6aea37ca918..6df71bafa16 100644 --- a/src/iOS.Autofill/LoginAddViewController.cs +++ b/src/iOS.Autofill/LoginAddViewController.cs @@ -1,4 +1,12 @@ using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.Core.Models.View; +using Bit.Core.Resources.Localization; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.iOS.Autofill.Models; +using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; using Foundation; using UIKit; @@ -7,6 +15,8 @@ namespace Bit.iOS.Autofill { public partial class LoginAddViewController : Core.Controllers.LoginAddViewController { + LazyResolve _cipherService = new LazyResolve(); + public LoginAddViewController(IntPtr handle) : base(handle) { @@ -20,12 +30,32 @@ public LoginAddViewController(IntPtr handle) public override UIBarButtonItem BaseCancelButton => CancelBarButton; public override UIBarButtonItem BaseSaveButton => SaveBarButton; - public override Action Success => id => + private new Context Context => (Context)base.Context; + + public override Action Success => cipherId => { + if (IsCreatingPasskey) + { + Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true)); + return; + } + LoginListController?.DismissModal(); LoginSearchController?.DismissModal(); }; + public override void ViewDidLoad() + { + IsCreatingPasskey = Context.IsCreatingPasskey; + if (IsCreatingPasskey) + { + NameCell.TextField.Text = Context.PasskeyCreationParams?.CredentialName; + UsernameCell.TextField.Text = Context.PasskeyCreationParams?.UserName; + } + + base.ViewDidLoad(); + } + partial void CancelBarButton_Activated(UIBarButtonItem sender) { Cancel(); @@ -36,6 +66,39 @@ private void Cancel() DismissViewController(true, null); } + protected override async Task EncryptAndSaveAsync(CipherView cipher) + { + if (!IsCreatingPasskey) + { + await base.EncryptAndSaveAsync(cipher); + return; + } + + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17.")); + return; + } + + var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); + try + { + PresentViewController(loadingAlert, true, null); + + var encryptedCipher = await _cipherService.Value.EncryptAsync(cipher); + await _cipherService.Value.SaveWithServerAsync(encryptedCipher); + + await loadingAlert.DismissViewControllerAsync(true); + + Success(encryptedCipher.Id); + } + catch + { + await loadingAlert.DismissViewControllerAsync(false); + throw; + } + } + async partial void SaveBarButton_Activated(UIBarButtonItem sender) { await SaveAsync(); diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index 35a2d8d9745..28af56c9d26 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -290,7 +290,7 @@ public override UIView GetViewForHeader(UITableView tableView, nint section) return headerItemView; } - return new UIView(CGRect.Empty);// base.GetViewForHeader(tableView, section); + return new UIView(CGRect.Empty); } catch (Exception ex) { diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index b4b496f496f..1117b150050 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -15,7 +15,9 @@ public class Context : AppExtensionContext public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; } public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; } public bool Configuring { get; set; } + public bool IsCreatingPasskey { get; set; } + public Fido2ConfirmNewCredentialParams? PasskeyCreationParams { get; set; } public TaskCompletionSource UnlockVaultTcs { get; set; } public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; } diff --git a/src/iOS.Core/Controllers/LoginAddViewController.cs b/src/iOS.Core/Controllers/LoginAddViewController.cs index 0e52c5a5720..4379a8fbcfa 100644 --- a/src/iOS.Core/Controllers/LoginAddViewController.cs +++ b/src/iOS.Core/Controllers/LoginAddViewController.cs @@ -1,23 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AuthenticationServices; -using Bit.App.Models; +using Bit.App.Models; using Bit.App.Pages; -using Bit.Core.Resources.Localization; using Bit.App.Utilities; using Bit.Core; using Bit.Core.Abstractions; using Bit.Core.Exceptions; using Bit.Core.Models.View; +using Bit.Core.Resources.Localization; +using Bit.Core.Services; using Bit.Core.Utilities; using Bit.iOS.Core.Models; using Bit.iOS.Core.Utilities; using Bit.iOS.Core.Views; using Foundation; -using UIKit; using Microsoft.Maui.Controls.Compatibility; +using UIKit; namespace Bit.iOS.Core.Controllers { @@ -28,7 +24,7 @@ public abstract class LoginAddViewController : ExtendedUITableViewController private IStorageService _storageService; private IEnumerable _folders; - public LoginAddViewController(IntPtr handle) + protected LoginAddViewController(IntPtr handle) : base(handle) { } @@ -47,6 +43,8 @@ public LoginAddViewController(IntPtr handle) public abstract UIBarButtonItem BaseSaveButton { get; } public abstract Action Success { get; } + protected bool IsCreatingPasskey { get; set; } + public override void ViewDidLoad() { _cipherService = ServiceContainer.Resolve("cipherService"); @@ -127,63 +125,84 @@ public override void ViewDidAppear(bool animated) base.ViewDidAppear(animated); } - protected async Task SaveAsync() + protected virtual async Task SaveAsync() { - /* - if (!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - */ - - if (string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text)) + try { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Password), AppResources.Ok); - return; - } + /* + if (!_connectivity.IsConnected) + { + AlertNoConnection(); + return; + } + */ - if (string.IsNullOrWhiteSpace(NameCell?.TextField?.Text)) - { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), AppResources.Ok); - return; - } + if (!IsCreatingPasskey && string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text)) + { + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Password), AppResources.Ok); + return; + } - var cipher = new CipherView - { - Name = NameCell.TextField.Text, - Notes = string.IsNullOrWhiteSpace(NotesCell?.TextView?.Text) ? null : NotesCell.TextView.Text, - Favorite = FavoriteCell.Switch.On, - FolderId = FolderCell.SelectedIndex == 0 ? - null : _folders.ElementAtOrDefault(FolderCell.SelectedIndex - 1)?.Id, - Type = Bit.Core.Enums.CipherType.Login, - Login = new LoginView + if (string.IsNullOrWhiteSpace(NameCell?.TextField?.Text)) { - Uris = null, - Username = string.IsNullOrWhiteSpace(UsernameCell?.TextField?.Text) ? - null : UsernameCell.TextField.Text, - Password = string.IsNullOrWhiteSpace(PasswordCell.TextField.Text) ? - null : PasswordCell.TextField.Text, + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Name), AppResources.Ok); + return; } - }; - if (!string.IsNullOrWhiteSpace(UriCell?.TextField?.Text)) - { - cipher.Login.Uris = new List + var cipher = new CipherView { - new LoginUriView + Name = NameCell.TextField.Text, + Notes = string.IsNullOrWhiteSpace(NotesCell?.TextView?.Text) ? null : NotesCell.TextView.Text, + Favorite = FavoriteCell.Switch.On, + FolderId = FolderCell.SelectedIndex == 0 ? + null : _folders.ElementAtOrDefault(FolderCell.SelectedIndex - 1)?.Id, + Type = Bit.Core.Enums.CipherType.Login, + Login = new LoginView { - Uri = UriCell.TextField.Text + Uris = null, + Username = string.IsNullOrWhiteSpace(UsernameCell?.TextField?.Text) ? + null : UsernameCell.TextField.Text, + Password = string.IsNullOrWhiteSpace(PasswordCell.TextField.Text) ? + null : PasswordCell.TextField.Text, } }; + + if (!string.IsNullOrWhiteSpace(UriCell?.TextField?.Text)) + { + cipher.Login.Uris = new List + { + new LoginUriView + { + Uri = UriCell.TextField.Text + } + }; + } + + await EncryptAndSaveAsync(cipher); + } + catch (ApiException e) + { + if (e?.Error != null) + { + DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + DisplayAlert(AppResources.AnErrorHasOccurred, AppResources.GenericErrorMessage, AppResources.Ok); } + } + protected virtual async Task EncryptAndSaveAsync(CipherView cipher) + { var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); - PresentViewController(loadingAlert, true, null); try { + PresentViewController(loadingAlert, true, null); + var cipherDomain = await _cipherService.EncryptAsync(cipher); await _cipherService.SaveWithServerAsync(cipherDomain); await loadingAlert.DismissViewControllerAsync(true); @@ -202,12 +221,10 @@ protected async Task SaveAsync() } Success(cipherDomain.Id); } - catch (ApiException e) + catch { - if (e?.Error != null) - { - DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); - } + await loadingAlert.DismissViewControllerAsync(false); + throw; } }