From e34a58e8753460887c9fb109b89575eda660e4c9 Mon Sep 17 00:00:00 2001 From: Federico Maccaroni Date: Mon, 26 Feb 2024 09:33:39 -0300 Subject: [PATCH] [PM-5154] Implement iOS Passkey -> Add login item (#3019) * PM-5154 Implement iOS passkey add login * PM-5154 Added Username to Create new login for passkey, for this the param was changed to the Fido2ConfirmNewCredentialParams object so we have access to the proper values. Also added back RpId to the params to have access to it when creating the vault item. Finally added loading to saving the passkey as new login --- src/Core/Abstractions/ICipherService.cs | 2 +- .../IFido2MakeCredentialUserInterface.cs | 5 + src/Core/Services/CipherService.cs | 7 +- .../Services/Fido2AuthenticatorService.cs | 3 +- .../Fido2MakeCredentialUserInterface.cs | 1 + src/iOS.Autofill/LoginAddViewController.cs | 65 +++++++- src/iOS.Autofill/LoginListViewController.cs | 34 +++- src/iOS.Autofill/Models/Context.cs | 2 + .../Controllers/LoginAddViewController.cs | 153 ++++++++++-------- 9 files changed, 194 insertions(+), 78 deletions(-) diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs index a8c363248..19bf0c441 100644 --- a/src/Core/Abstractions/ICipherService.cs +++ b/src/Core/Abstractions/ICipherService.cs @@ -34,6 +34,6 @@ namespace Bit.Core.Abstractions Task DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId); Task SoftDeleteWithServerAsync(string id); Task RestoreWithServerAsync(string id); - Task CreateNewLoginForPasskeyAsync(string rpId); + Task CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams); } } diff --git a/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs b/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs index 67d13141e..0cff512fb 100644 --- a/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs +++ b/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs @@ -16,6 +16,11 @@ namespace Bit.Core.Abstractions /// Whether or not the user must be verified before completing the operation. /// public bool UserVerification { get; set; } + + /// + /// The relying party identifier + /// + public string RpId { get; set; } } public interface IFido2MakeCredentialUserInterface : IFido2UserInterface diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index e55a03f9b..07b3033af 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -1286,17 +1286,18 @@ namespace Bit.Core.Services cipher.PasswordHistory = encPhs; } - public async Task CreateNewLoginForPasskeyAsync(string rpId) + public async Task CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams) { var newCipher = new CipherView { - Name = rpId, + Name = newPasskeyParams.CredentialName, Type = CipherType.Login, Login = new LoginView { + Username = newPasskeyParams.UserName, Uris = new List { - new LoginUriView { Uri = rpId } + new LoginUriView { Uri = newPasskeyParams.RpId } } }, Card = new CardView(), diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 426b8a2e6..78fe619ba 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -47,7 +47,8 @@ namespace Bit.Core.Services { CredentialName = makeCredentialParams.RpEntity.Name, UserName = makeCredentialParams.UserEntity.Name, - UserVerification = makeCredentialParams.RequireUserVerification + UserVerification = makeCredentialParams.RequireUserVerification, + RpId = makeCredentialParams.RpEntity.Id }); var cipherId = response.CipherId; diff --git a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs index 0a154c83f..cd33f4685 100644 --- a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs +++ b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs @@ -22,6 +22,7 @@ namespace Bit.iOS.Autofill { _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 6aea37ca9..6df71bafa 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 @@ namespace Bit.iOS.Autofill 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 @@ namespace Bit.iOS.Autofill 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 35a2d8d97..68657e55c 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; 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; @@ -16,6 +17,7 @@ using CoreFoundation; using CoreGraphics; using Foundation; using UIKit; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Bit.iOS.Autofill { @@ -147,7 +149,13 @@ namespace Bit.iOS.Autofill { SavePasskeyAsNewLoginAsync().FireAndForget(ex => { - _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget(); + var message = AppResources.AnErrorHasOccurred; + if (ex is ApiException apiEx && apiEx.Error != null) + { + message = apiEx.Error.GetSingleMessage(); + } + + _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, message).FireAndForget(); }); } @@ -159,8 +167,26 @@ namespace Bit.iOS.Autofill return; } - var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCredentialIdentity.RelyingPartyIdentifier); - Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true)); + if (Context.PasskeyCreationParams is null) + { + Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params.")); + return; + } + + var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); + + try + { + PresentViewController(loadingAlert, true, null); + + var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCreationParams.Value); + Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true)); + } + catch + { + await loadingAlert.DismissViewControllerAsync(false); + throw; + } } public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) @@ -290,7 +316,7 @@ namespace Bit.iOS.Autofill 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 b4b496f49..1117b1500 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -15,7 +15,9 @@ namespace Bit.iOS.Autofill.Models 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 0e52c5a57..4379a8fbc 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 @@ namespace Bit.iOS.Core.Controllers private IStorageService _storageService; private IEnumerable _folders; - public LoginAddViewController(IntPtr handle) + protected LoginAddViewController(IntPtr handle) : base(handle) { } @@ -47,6 +43,8 @@ namespace Bit.iOS.Core.Controllers 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 @@ namespace Bit.iOS.Core.Controllers base.ViewDidAppear(animated); } - protected async Task SaveAsync() + protected virtual async Task SaveAsync() { - /* - if (!_connectivity.IsConnected) - { - AlertNoConnection(); - return; - } - */ - - if (string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text)) - { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Password), AppResources.Ok); - return; - } - - if (string.IsNullOrWhiteSpace(NameCell?.TextField?.Text)) - { - DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, - AppResources.Name), 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 - { - 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 - } - }; - } - - var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving); - PresentViewController(loadingAlert, true, null); try { + /* + if (!_connectivity.IsConnected) + { + AlertNoConnection(); + return; + } + */ + + if (!IsCreatingPasskey && string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text)) + { + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Password), AppResources.Ok); + return; + } + + if (string.IsNullOrWhiteSpace(NameCell?.TextField?.Text)) + { + DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired, + AppResources.Name), 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 + { + 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); + try + { + PresentViewController(loadingAlert, true, null); + var cipherDomain = await _cipherService.EncryptAsync(cipher); await _cipherService.SaveWithServerAsync(cipherDomain); await loadingAlert.DismissViewControllerAsync(true); @@ -202,12 +221,10 @@ namespace Bit.iOS.Core.Controllers } Success(cipherDomain.Id); } - catch (ApiException e) + catch { - if (e?.Error != null) - { - DisplayAlert(AppResources.AnErrorHasOccurred, e.Error.GetSingleMessage(), AppResources.Ok); - } + await loadingAlert.DismissViewControllerAsync(false); + throw; } }