1
0
mirror of https://github.com/bitwarden/mobile synced 2025-12-11 13:53:29 +00:00

[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
This commit is contained in:
Federico Maccaroni
2024-02-26 09:33:39 -03:00
committed by GitHub
parent 9f92fdeb29
commit e34a58e875
9 changed files with 194 additions and 78 deletions

View File

@@ -34,6 +34,6 @@ namespace Bit.Core.Abstractions
Task<byte[]> DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
Task SoftDeleteWithServerAsync(string id);
Task RestoreWithServerAsync(string id);
Task<string> CreateNewLoginForPasskeyAsync(string rpId);
Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
}
}

View File

@@ -16,6 +16,11 @@ namespace Bit.Core.Abstractions
/// Whether or not the user must be verified before completing the operation.
/// </summary>
public bool UserVerification { get; set; }
/// <summary>
/// The relying party identifier
/// </summary>
public string RpId { get; set; }
}
public interface IFido2MakeCredentialUserInterface : IFido2UserInterface

View File

@@ -1286,17 +1286,18 @@ namespace Bit.Core.Services
cipher.PasswordHistory = encPhs;
}
public async Task<string> CreateNewLoginForPasskeyAsync(string rpId)
public async Task<string> CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams)
{
var newCipher = new CipherView
{
Name = rpId,
Name = newPasskeyParams.CredentialName,
Type = CipherType.Login,
Login = new LoginView
{
Username = newPasskeyParams.UserName,
Uris = new List<LoginUriView>
{
new LoginUriView { Uri = rpId }
new LoginUriView { Uri = newPasskeyParams.RpId }
}
},
Card = new CardView(),

View File

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

View File

@@ -22,6 +22,7 @@ namespace Bit.iOS.Autofill
{
_context.ConfirmNewCredentialTcs?.SetCanceled();
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>();
_context.PasskeyCreationParams = confirmNewCredentialParams;
_onConfirmingNewCredential();

View File

@@ -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<ICipherService> _cipherService = new LazyResolve<ICipherService>();
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<string> Success => id =>
private new Context Context => (Context)base.Context;
public override Action<string> 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();

View File

@@ -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,9 +167,27 @@ namespace Bit.iOS.Autofill
return;
}
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCredentialIdentity.RelyingPartyIdentifier);
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)
{

View File

@@ -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<bool> UnlockVaultTcs { get; set; }
public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; }

View File

@@ -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<FolderView> _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<string> Success { get; }
protected bool IsCreatingPasskey { get; set; }
public override void ViewDidLoad()
{
_cipherService = ServiceContainer.Resolve<ICipherService>("cipherService");
@@ -127,7 +125,9 @@ namespace Bit.iOS.Core.Controllers
base.ViewDidAppear(animated);
}
protected async Task SaveAsync()
protected virtual async Task SaveAsync()
{
try
{
/*
if (!_connectivity.IsConnected)
@@ -137,7 +137,7 @@ namespace Bit.iOS.Core.Controllers
}
*/
if (string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text))
if (!IsCreatingPasskey && string.IsNullOrWhiteSpace(PasswordCell?.TextField?.Text))
{
DisplayAlert(AppResources.AnErrorHasOccurred, string.Format(AppResources.ValidationFieldRequired,
AppResources.Password), AppResources.Ok);
@@ -180,10 +180,29 @@ namespace Bit.iOS.Core.Controllers
};
}
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 @@ 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;
}
}