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; } }