diff --git a/Directory.Build.props b/Directory.Build.props index 5a27c8b90..379578e7f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 8.0.4-nightly.* + 8.0.7-nightly.* Automatic:AppStore iPhone Distribution True diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs index b344bc101..a8c363248 100644 --- a/src/Core/Abstractions/ICipherService.cs +++ b/src/Core/Abstractions/ICipherService.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; using Bit.Core.Models.View; @@ -37,5 +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); } } diff --git a/src/Core/Models/Api/Fido2CredentialApi.cs b/src/Core/Models/Api/Fido2CredentialApi.cs index 7953e06a1..672a7ec47 100644 --- a/src/Core/Models/Api/Fido2CredentialApi.cs +++ b/src/Core/Models/Api/Fido2CredentialApi.cs @@ -1,5 +1,4 @@ -using System; -using Bit.Core.Models.Domain; +using Bit.Core.Models.Domain; namespace Bit.Core.Models.Api { @@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api RpName = fido2Key.RpName?.EncryptedString; UserHandle = fido2Key.UserHandle?.EncryptedString; UserName = fido2Key.UserName?.EncryptedString; + UserDisplayName = fido2Key.UserDisplayName?.EncryptedString; Counter = fido2Key.Counter?.EncryptedString; CreationDate = fido2Key.CreationDate; } @@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api public string RpName { get; set; } public string UserHandle { get; set; } public string UserName { get; set; } + public string UserDisplayName { get; set; } public string Counter { get; set; } public DateTime CreationDate { get; set; } } diff --git a/src/Core/Models/Data/Fido2CredentialData.cs b/src/Core/Models/Data/Fido2CredentialData.cs index 846df59f4..103d03cbf 100644 --- a/src/Core/Models/Data/Fido2CredentialData.cs +++ b/src/Core/Models/Data/Fido2CredentialData.cs @@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data RpName = apiData.RpName; UserHandle = apiData.UserHandle; UserName = apiData.UserName; + UserDisplayName = apiData.UserDisplayName; Counter = apiData.Counter; CreationDate = apiData.CreationDate; } @@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data public string RpName { get; set; } public string UserHandle { get; set; } public string UserName { get; set; } + public string UserDisplayName { get; set; } public string Counter { get; set; } public DateTime CreationDate { get; set; } } diff --git a/src/Core/Models/Domain/Fido2Credential.cs b/src/Core/Models/Domain/Fido2Credential.cs index 7c6928204..313ff2c8f 100644 --- a/src/Core/Models/Domain/Fido2Credential.cs +++ b/src/Core/Models/Domain/Fido2Credential.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data; using Bit.Core.Models.View; namespace Bit.Core.Models.Domain @@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain nameof(RpName), nameof(UserHandle), nameof(UserName), + nameof(UserDisplayName), nameof(Counter) }; @@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain public EncString RpName { get; set; } public EncString UserHandle { get; set; } public EncString UserName { get; set; } + public EncString UserDisplayName { get; set; } public EncString Counter { get; set; } public DateTime CreationDate { get; set; } diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs index 210e56042..89058fc64 100644 --- a/src/Core/Models/View/Fido2CredentialView.cs +++ b/src/Core/Models/View/Fido2CredentialView.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using System.Text.Json.Serialization; +using Bit.Core.Enums; using Bit.Core.Models.Domain; using Bit.Core.Utilities; @@ -25,32 +26,42 @@ namespace Bit.Core.Models.View public string RpName { get; set; } public string UserHandle { get; set; } public string UserName { get; set; } + public string UserDisplayName { get; set; } public string Counter { get; set; } public DateTime CreationDate { get; set; } + [JsonIgnore] public int CounterValue { get => int.TryParse(Counter, out var counter) ? counter : 0; set => Counter = value.ToString(); } + [JsonIgnore] public byte[] UserHandleValue { get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle); set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value); } + [JsonIgnore] public byte[] KeyBytes { get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue); set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value); } + [JsonIgnore] public bool DiscoverableValue { get => bool.TryParse(Discoverable, out var discoverable) && discoverable; set => Discoverable = value.ToString().ToLower(); } + [JsonIgnore] public override string SubTitle => UserName; + public override List> LinkedFieldOptions => new List>(); + + [JsonIgnore] public bool CanLaunch => !string.IsNullOrEmpty(RpId); + [JsonIgnore] public string LaunchUri => $"https://{RpId}"; public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName; diff --git a/src/Core/Resources/Localization/AppResources.Designer.cs b/src/Core/Resources/Localization/AppResources.Designer.cs index 755dff48e..a6651bc7d 100644 --- a/src/Core/Resources/Localization/AppResources.Designer.cs +++ b/src/Core/Resources/Localization/AppResources.Designer.cs @@ -166,15 +166,6 @@ namespace Bit.Core.Resources.Localization { } } - /// - /// Looks up a localized string similar to Credential Provider service - /// - public static string CredentialProviderService { - get { - return ResourceManager.GetString("CredentialProviderService", resourceCulture); - } - } - /// /// Looks up a localized string similar to Bitwarden needs attention - See "Auto-fill Accessibility Service" from Bitwarden settings. /// @@ -1327,6 +1318,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services.. + /// + public static string BitwardenCredentialProviderGoToSettings { + get { + return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bitwarden Help Center. /// @@ -1543,6 +1543,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Choose a login to save this passkey to. + /// + public static string ChooseALoginToSaveThisPasskeyTo { + get { + return ResourceManager.GetString("ChooseALoginToSaveThisPasskeyTo", resourceCulture); + } + } + /// /// Looks up a localized string similar to Choose file. /// @@ -1903,6 +1912,24 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Credential Provider service. + /// + public static string CredentialProviderService { + get { + return ResourceManager.GetString("CredentialProviderService", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device.. + /// + public static string CredentialProviderServiceExplanationLong { + get { + return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture); + } + } + /// /// Looks up a localized string similar to Credits. /// @@ -5110,6 +5137,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Overwrite passkey?. + /// + public static string OverwritePasskey { + get { + return ResourceManager.GetString("OverwritePasskey", resourceCulture); + } + } + /// /// Looks up a localized string similar to Ownership. /// @@ -5137,6 +5173,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Passkeys for {0}. + /// + public static string PasskeysForX { + get { + return ResourceManager.GetString("PasskeysForX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Passkey will not be copied. /// @@ -5326,6 +5371,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Passwords for {0}. + /// + public static string PasswordsForX { + get { + return ResourceManager.GetString("PasswordsForX", resourceCulture); + } + } + /// /// Looks up a localized string similar to Password type. /// @@ -5831,6 +5885,24 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to Save passkey. + /// + public static string SavePasskey { + get { + return ResourceManager.GetString("SavePasskey", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save passkey as new login. + /// + public static string SavePasskeyAsNewLogin { + get { + return ResourceManager.GetString("SavePasskeyAsNewLogin", resourceCulture); + } + } + /// /// Looks up a localized string similar to Saving.... /// @@ -6704,6 +6776,15 @@ namespace Bit.Core.Resources.Localization { } } + /// + /// Looks up a localized string similar to This item already contains a passkey. Are you sure you want to overwrite the current passkey?. + /// + public static string ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey { + get { + return ResourceManager.GetString("ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey", resourceCulture); + } + } + /// /// Looks up a localized string similar to This request is no longer valid. /// @@ -7694,15 +7775,6 @@ namespace Bit.Core.Resources.Localization { } } - /// - /// Looks up a localized string similar to We were unable to automatically open the Android credential provider settings menu for you. You can navigate to the credential provider settings menu manually from Android Settings > System > Passwords & accounts > Passwords, passkeys and data services. - /// - public static string BitwardenCredentialProviderGoToSettings { - get { - return ResourceManager.GetString("BitwardenCredentialProviderGoToSettings", resourceCulture); - } - } - /// /// Looks up a localized string similar to Word separator. /// @@ -7721,15 +7793,6 @@ namespace Bit.Core.Resources.Localization { } } - /// - /// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device. - /// - public static string CredentialProviderServiceExplanationLong { - get { - return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0} hours and one minute. /// diff --git a/src/Core/Resources/Localization/AppResources.resx b/src/Core/Resources/Localization/AppResources.resx index a04d4bcb8..1453f8f53 100644 --- a/src/Core/Resources/Localization/AppResources.resx +++ b/src/Core/Resources/Localization/AppResources.resx @@ -2886,4 +2886,25 @@ Do you want to switch to this account? Set up an unlock option to change your vault timeout action. + + Choose a login to save this passkey to + + + Save passkey as new login + + + Save passkey + + + Passkeys for {0} + + + Passwords for {0} + + + Overwrite passkey? + + + This item already contains a passkey. Are you sure you want to overwrite the current passkey? + diff --git a/src/Core/Services/CipherService.cs b/src/Core/Services/CipherService.cs index 357458970..e55a03f9b 100644 --- a/src/Core/Services/CipherService.cs +++ b/src/Core/Services/CipherService.cs @@ -1286,6 +1286,34 @@ namespace Bit.Core.Services cipher.PasswordHistory = encPhs; } + public async Task CreateNewLoginForPasskeyAsync(string rpId) + { + var newCipher = new CipherView + { + Name = rpId, + Type = CipherType.Login, + Login = new LoginView + { + Uris = new List + { + new LoginUriView { Uri = rpId } + } + }, + Card = new CardView(), + Identity = new IdentityView(), + SecureNote = new SecureNoteView + { + Type = SecureNoteType.Generic + }, + Reprompt = CipherRepromptType.None + }; + + var encryptedCipher = await EncryptAsync(newCipher); + await SaveWithServerAsync(encryptedCipher); + + return encryptedCipher.Id; + } + private class CipherLocaleComparer : IComparer { private readonly II18nService _i18nService; diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 357fff543..bd7e0cc06 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -66,7 +66,7 @@ namespace Bit.Core.Services throw new NotAllowedError(); } - cipher.Login.Fido2Credentials = [fido2Credential]; + cipher.Login.Fido2Credentials = new List { fido2Credential }; var reencrypted = await _cipherService.EncryptAsync(cipher); await _cipherService.SaveWithServerAsync(reencrypted); credentialId = fido2Credential.CredentialId; @@ -287,7 +287,7 @@ namespace Bit.Core.Services UserName = makeCredentialsParams.UserEntity.Name, CounterValue = 0, RpName = makeCredentialsParams.RpEntity.Name, - // UserDisplayName = makeCredentialsParams.UserEntity.DisplayName, + UserDisplayName = makeCredentialsParams.UserEntity.DisplayName, DiscoverableValue = makeCredentialsParams.RequireResidentKey, CreationDate = DateTime.UtcNow }; @@ -316,12 +316,12 @@ namespace Bit.Core.Services ); authData.Add(flags); - authData.AddRange([ + authData.AddRange(new List { (byte)(counter >> 24), (byte)(counter >> 16), (byte)(counter >> 8), (byte)counter - ]); + }); if (isAttestation) { diff --git a/src/Core/Services/Logging/ClipLogger.cs b/src/Core/Services/Logging/ClipLogger.cs new file mode 100644 index 000000000..8d28a3eba --- /dev/null +++ b/src/Core/Services/Logging/ClipLogger.cs @@ -0,0 +1,66 @@ +//using System.Runtime.CompilerServices; +//using System.Text; +//using Bit.Core.Abstractions; + +//#if IOS +//using UIKit; +//#endif + +//namespace Bit.Core.Services +//{ +// /// +// /// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet +// /// so we can use this that copies the logs to the clipboard so one +// /// can paste them and analyze its output. +// /// +// public class ClipLogger : ILogger +// { +// private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder(); + +// static ILogger _instance; +// public static ILogger Instance +// { +// get +// { +// if (_instance is null) +// { +// _instance = new ClipLogger(); +// } +// return _instance; +// } +// } + +// protected ClipLogger() +// { +// } + +// public static void Log(string breadcrumb) +// { +// _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}"); +//#if IOS +// UIPasteboard.General.String = _currentBreadcrumbs.ToString(); +//#endif +// } + +// public void Error(string message, IDictionary extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0) +// { +// var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}"; +// var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}"; +// var properties = new Dictionary +// { +// ["File"] = filePathAndLineNumber, +// ["Method"] = memberName +// }; + +// Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}"); +// } + +// public void Exception(Exception ex) => Log(ex?.ToString()); + +// public Task InitAsync() => Task.CompletedTask; + +// public Task IsEnabled() => Task.FromResult(true); + +// public Task SetEnabled(bool value) => Task.CompletedTask; +// } +//} diff --git a/src/Core/Services/Logging/LoggerHelper.cs b/src/Core/Services/Logging/LoggerHelper.cs index 9cdf225f6..ad5deb0db 100644 --- a/src/Core/Services/Logging/LoggerHelper.cs +++ b/src/Core/Services/Logging/LoggerHelper.cs @@ -1,5 +1,4 @@ -using System; -using Bit.Core.Abstractions; +using Bit.Core.Abstractions; using Bit.Core.Utilities; namespace Bit.Core.Services diff --git a/src/iOS.Autofill/ColorConstants.cs b/src/iOS.Autofill/ColorConstants.cs new file mode 100644 index 000000000..19ea72726 --- /dev/null +++ b/src/iOS.Autofill/ColorConstants.cs @@ -0,0 +1,8 @@ +namespace Bit.iOS.Autofill +{ + public static class ColorConstants + { + public const string LIGHT_SECONDARY_300 = "LightSecondary300"; + public const string LIGHT_TEXT_MUTED = "LightTextMuted"; + } +} diff --git a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs index 4cbac99f1..1db16e998 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.Passkeys.cs @@ -1,16 +1,130 @@ using System; +using System.Linq; using System.Threading.Tasks; using AuthenticationServices; using Bit.App.Abstractions; +using Bit.Core.Abstractions; using Bit.Core.Models.View; +using Bit.Core.Services; using Bit.Core.Utilities; +using Bit.Core.Utilities.Fido2; +using Bit.iOS.Core.Utilities; using Foundation; +using Microsoft.Maui.ApplicationModel; +using ObjCRuntime; using UIKit; namespace Bit.iOS.Autofill { public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost { + private readonly LazyResolve _fido2AuthService = new LazyResolve(); + + public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return; + } + + try + { + switch (registrationRequest?.Type) + { + case ASCredentialRequestType.PasskeyAssertion: + var passkeyRegistrationRequest = Runtime.GetNSObject(registrationRequest.GetHandle()); + await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest); + break; + default: + CancelRequest(ASExtensionErrorCode.Failed); + break; + } + + } + catch (Exception ex) + { + OnProvidingCredentialException(ex); + } + } + + private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null) + { + return; + } + + InitAppIfNeeded(); + + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + + _context.PasskeyCredentialRequest = passkeyRegistrationRequest; + _context.IsCreatingPasskey = true; + + var credIdentity = Runtime.GetNSObject(passkeyRegistrationRequest.CredentialIdentity.GetHandle()); + + _context.UrlString = credIdentity?.RelyingPartyIdentifier; + + var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams + { + Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(), + CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms), + RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required", + RequireResidentKey = true, + RpEntity = new PublicKeyCredentialRpEntity + { + Id = credIdentity.RelyingPartyIdentifier, + Name = credIdentity.RelyingPartyIdentifier + }, + UserEntity = new PublicKeyCredentialUserEntity + { + Id = credIdentity.UserHandle.ToArray(), + Name = credIdentity.UserName, + DisplayName = credIdentity.UserName + } + }, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync, _context, OnConfirmingNewCredential)); + + await ASHelpers.ReplaceAllIdentitiesAsync(); + + var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential( + credIdentity.RelyingPartyIdentifier, + passkeyRegistrationRequest.ClientDataHash, + NSData.FromArray(result.CredentialId), + NSData.FromArray(result.AttestationObject))); + } + + private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms) + { + if (supportedAlgorithms?.Any() != true) + { + return new PublicKeyCredentialParameters[] + { + new PublicKeyCredentialParameters + { + Type = Bit.Core.Constants.DefaultFido2CredentialType, + Alg = (int)Fido2AlgorithmIdentifier.ES256 + }, + new PublicKeyCredentialParameters + { + Type = Bit.Core.Constants.DefaultFido2CredentialType, + Alg = (int)Fido2AlgorithmIdentifier.RS256 + } + }; + } + + return supportedAlgorithms + .Where(alg => (int)alg == (int)Fido2AlgorithmIdentifier.ES256) + .Select(alg => new PublicKeyCredentialParameters + { + Type = Bit.Core.Constants.DefaultFido2CredentialType, + Alg = (int)alg + }).ToArray(); + } + private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest) { InitAppIfNeeded(); @@ -25,7 +139,7 @@ namespace Bit.iOS.Autofill await ProvideCredentialAsync(false); } - public async Task CompleteAssertionRequestAsync(CipherView cipherView) + public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId) { if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { @@ -33,44 +147,127 @@ namespace Bit.iOS.Autofill return; } - // // TODO: Generate the credential Signature and Auth data accordingly - // var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams - // { - // RpId = cipherView.Login.MainFido2Credential.RpId, - // Counter = cipherView.Login.MainFido2Credential.Counter, - // CredentialId = cipherView.Login.MainFido2Credential.CredentialId - // }); - - // CompleteAssertionRequest(new ASPasskeyAssertionCredential( - // cipherView.Login.MainFido2Credential.UserHandle, - // cipherView.Login.MainFido2Credential.RpId, - // NSData.FromArray(fido2AssertionResult.Signature), - // _context.PasskeyCredentialRequest?.ClientDataHash, - // NSData.FromArray(fido2AssertionResult.AuthenticatorData), - // cipherView.Login.MainFido2Credential.CredentialId - // )); - } - - public void CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) - { - if (_context == null) + if (_context.PasskeyCredentialRequest is null) { - ServiceContainer.Reset(); - CancelRequest(ASExtensionErrorCode.UserCanceled); + OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest")); return; } - NSRunLoop.Main.BeginInvokeOnMainThread(() => + try { + // TODO: Add user verification and remove hardcoding on the user interface "userVerified" + var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams + { + RpId = rpId, + Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(), + RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required", + AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] + { + new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor + { + Id = credentialIdData.ToArray() + } + } + }, new Fido2GetAssertionUserInterface(cipherId, true, EnsureUnlockedVaultAsync, () => Task.FromResult(true))); + + var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null + ? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle) + : (NSData)userHandleData; + + var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null + ? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id) + : credentialIdData; + + await CompleteAssertionRequest(new ASPasskeyAssertionCredential( + selectedUserHandleData, + rpId, + NSData.FromArray(fido2AssertionResult.Signature), + _context.PasskeyCredentialRequest.ClientDataHash, + NSData.FromArray(fido2AssertionResult.AuthenticatorData), + selectedCredentialIdData + )); + } + catch (InvalidOperationException) + { + return; + } + } + + public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential) + { + try + { + if (assertionCredential is null) + { + ServiceContainer.Reset(); + CancelRequest(ASExtensionErrorCode.UserCanceled); + return; + } + ServiceContainer.Reset(); - ASExtensionContext?.CompleteAssertionRequest(assertionCredential, null); - }); +#pragma warning disable CA1416 // Validate platform compatibility + var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential); +#pragma warning restore CA1416 // Validate platform compatibility + + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView) { return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials; } + + private void OnConfirmingNewCredential() + { + MainThread.BeginInvokeOnMainThread(() => + { + try + { + PerformSegue(SegueConstants.LOGIN_LIST, this); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); + } + + private async Task EnsureUnlockedVaultAsync() + { + if (_context.IsCreatingPasskey) + { + if (!await IsLocked()) + { + return; + } + + _context.UnlockVaultTcs?.SetCanceled(); + _context.UnlockVaultTcs = new TaskCompletionSource(); + MainThread.BeginInvokeOnMainThread(() => + { + try + { + PerformSegue(SegueConstants.LOCK, this); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + }); + + await _context.UnlockVaultTcs.Task; + return; + } + + if (!await IsAuthed() || await IsLocked()) + { + CancelRequest(ASExtensionErrorCode.UserInteractionRequired); + throw new InvalidOperationException("Not authed or locked"); + } + } } } - diff --git a/src/iOS.Autofill/CredentialProviderViewController.cs b/src/iOS.Autofill/CredentialProviderViewController.cs index 361cbacbe..53643e55c 100644 --- a/src/iOS.Autofill/CredentialProviderViewController.cs +++ b/src/iOS.Autofill/CredentialProviderViewController.cs @@ -1,5 +1,4 @@ using System; -using System.Text; using System.Threading.Tasks; using AuthenticationServices; using Bit.App.Abstractions; @@ -20,6 +19,7 @@ using Foundation; using Microsoft.Maui.ApplicationModel; using Microsoft.Maui.Controls; using Microsoft.Maui.Platform; +using ObjCRuntime; using UIKit; namespace Bit.iOS.Autofill @@ -32,7 +32,6 @@ namespace Bit.iOS.Autofill private IAccountsManager _accountsManager; private readonly LazyResolve _stateService = new LazyResolve(); - private readonly LazyResolve _fido2AuthService = new LazyResolve(); public CredentialProviderViewController(IntPtr handle) : base(handle) @@ -56,7 +55,6 @@ namespace Bit.iOS.Autofill { ExtContext = ExtensionContext }; - } catch (Exception ex) { @@ -85,17 +83,17 @@ namespace Bit.iOS.Autofill } else if (await IsLocked()) { - PerformSegue("lockPasswordSegue", this); + PerformSegue(SegueConstants.LOCK, this); } else { if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) { - PerformSegue("loginSearchSegue", this); + PerformSegue(SegueConstants.LOGIN_SEARCH, this); } else { - PerformSegue("loginListSegue", this); + PerformSegue(SegueConstants.LOGIN_LIST, this); } } } @@ -105,17 +103,67 @@ namespace Bit.iOS.Autofill } } - public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest) + [Export("prepareCredentialListForServiceIdentifiers:requestParameters:")] + public override async void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters) { try { - switch (credentialRequest) + InitAppIfNeeded(); + _context.ServiceIdentifiers = serviceIdentifiers; + if (serviceIdentifiers.Length > 0) { - case ASPasswordCredentialRequest passwordRequest: - await ProvideCredentialWithoutUserInteractionAsync(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + var uri = serviceIdentifiers[0].Identifier; + if (serviceIdentifiers[0].Type == ASCredentialServiceIdentifierType.Domain) + { + uri = string.Concat("https://", uri); + } + _context.UrlString = uri; + } + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + } + else if (await IsLocked()) + { + PerformSegue(SegueConstants.LOCK, this); + } + else + { + if (_context.ServiceIdentifiers == null || _context.ServiceIdentifiers.Length == 0) + { + PerformSegue(SegueConstants.LOGIN_SEARCH, this); + } + else + { + PerformSegue(SegueConstants.LOGIN_LIST, this); + } + } + } + catch (Exception ex) + { + OnProvidingCredentialException(ex); + } + } + + [Export("provideCredentialWithoutUserInteractionForRequest:")] + public override async void ProvideCredentialWithoutUserInteraction(IASCredentialRequest credentialRequest) + { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return; + } + + try + { + switch (credentialRequest?.Type) + { + case ASCredentialRequestType.Password: + var passwordCredentialIdentity = Runtime.GetNSObject(credentialRequest.CredentialIdentity.GetHandle()); + await ProvideCredentialWithoutUserInteractionAsync(passwordCredentialIdentity); break; - case ASPasskeyCredentialRequest passkeyRequest: - await ProvideCredentialWithoutUserInteractionAsync(passkeyRequest); + case ASCredentialRequestType.PasskeyAssertion: + var asPasskeyCredentialRequest = Runtime.GetNSObject(credentialRequest.GetHandle()); + await ProvideCredentialWithoutUserInteractionAsync(asPasskeyCredentialRequest); break; default: CancelRequest(ASExtensionErrorCode.Failed); @@ -128,48 +176,40 @@ namespace Bit.iOS.Autofill } } - public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) - { - try - { - await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity); - } - catch (Exception ex) - { - OnProvidingCredentialException(ex); - } - } - - private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity) - { - InitAppIfNeeded(); - await _stateService.Value.SetPasswordRepromptAutofillAsync(false); - await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); - if (!await IsAuthed() || await IsLocked()) - { - var err = new NSError(new NSString("ASExtensionErrorDomain"), - Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); - ExtensionContext.CancelRequest(err); - return; - } - _context.PasswordCredentialIdentity = credentialIdentity; - await ProvideCredentialAsync(false); - } + //public override async void ProvideCredentialWithoutUserInteraction(ASPasswordCredentialIdentity credentialIdentity) + //{ + // try + // { + // await ProvideCredentialWithoutUserInteractionAsync(credentialIdentity); + // } + // catch (Exception ex) + // { + // OnProvidingCredentialException(ex); + // } + //} + [Export("prepareInterfaceToProvideCredentialForRequest:")] public override async void PrepareInterfaceToProvideCredential(IASCredentialRequest credentialRequest) { + if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + return; + } + try { - switch (credentialRequest) + switch (credentialRequest?.Type) { - case ASPasswordCredentialRequest passwordRequest: - PrepareInterfaceToProvideCredential(passwordRequest.CredentialIdentity as ASPasswordCredentialIdentity); + case ASCredentialRequestType.Password: + var passwordCredentialIdentity = Runtime.GetNSObject(credentialRequest.CredentialIdentity.GetHandle()); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = passwordCredentialIdentity); break; - case ASPasskeyCredentialRequest passkeyRequest: - await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = passkeyRequest); + case ASCredentialRequestType.PasskeyAssertion: + var asPasskeyCredentialRequest = Runtime.GetNSObject(credentialRequest.GetHandle()); + await PrepareInterfaceToProvideCredentialAsync(c => c.PasskeyCredentialRequest = asPasskeyCredentialRequest); break; default: - ExtensionContext?.CancelRequest(new NSError(ASExtensionErrorCodeExtensions.GetDomain(ASExtensionErrorCode.Failed), (int)ASExtensionErrorCode.Failed)); + CancelRequest(ASExtensionErrorCode.Failed); break; } } @@ -179,30 +219,17 @@ namespace Bit.iOS.Autofill } } - public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) - { - try - { - await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity); - } - catch (Exception ex) - { - OnProvidingCredentialException(ex); - } - } - - private async Task PrepareInterfaceToProvideCredentialAsync(Action updateContext) - { - InitAppIfNeeded(); - if (!await IsAuthed()) - { - await _accountsManager.NavigateOnAccountChangeAsync(false); - return; - } - updateContext(_context); - await CheckLockAsync(async () => await ProvideCredentialAsync()); - } - + //public override async void PrepareInterfaceToProvideCredential(ASPasswordCredentialIdentity credentialIdentity) + //{ + // try + // { + // await PrepareInterfaceToProvideCredentialAsync(c => c.PasswordCredentialIdentity = credentialIdentity); + // } + // catch (Exception ex) + // { + // OnProvidingCredentialException(ex); + // } + //} public override async void PrepareInterfaceForExtensionConfiguration() { @@ -222,6 +249,34 @@ namespace Bit.iOS.Autofill OnProvidingCredentialException(ex); } } + + private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasswordCredentialIdentity credentialIdentity) + { + InitAppIfNeeded(); + await _stateService.Value.SetPasswordRepromptAutofillAsync(false); + await _stateService.Value.SetPasswordVerifiedAutofillAsync(false); + if (!await IsAuthed() || await IsLocked()) + { + var err = new NSError(new NSString("ASExtensionErrorDomain"), + Convert.ToInt32(ASExtensionErrorCode.UserInteractionRequired), null); + ExtensionContext.CancelRequest(err); + return; + } + _context.PasswordCredentialIdentity = credentialIdentity; + await ProvideCredentialAsync(false); + } + + private async Task PrepareInterfaceToProvideCredentialAsync(Action updateContext) + { + InitAppIfNeeded(); + if (!await IsAuthed()) + { + await _accountsManager.NavigateOnAccountChangeAsync(false); + return; + } + updateContext(_context); + await CheckLockAsync(async () => await ProvideCredentialAsync()); + } public void CompleteRequest(string id = null, string username = null, string password = null, string totp = null) @@ -262,13 +317,18 @@ namespace Bit.iOS.Autofill private void OnProvidingCredentialException(Exception ex) { - //LoggerHelper.LogEvenIfCantBeResolved(ex); - UIPasteboard.General.String = ex.ToString(); + LoggerHelper.LogEvenIfCantBeResolved(ex); CancelRequest(ASExtensionErrorCode.Failed); } - private void CancelRequest(ASExtensionErrorCode code) + public void CancelRequest(ASExtensionErrorCode code) { + if (_context?.IsPasskey == true) + { + _context.ConfirmNewCredentialTcs?.TrySetCanceled(); + _context.UnlockVaultTcs?.TrySetCanceled(); + } + //var err = new NSError(new NSString("ASExtensionErrorDomain"), Convert.ToInt32(code), null); var err = new NSError(ASExtensionErrorCodeExtensions.GetDomain(code), (int)code); ExtensionContext?.CancelRequest(err); @@ -316,7 +376,7 @@ namespace Bit.iOS.Autofill } } - public async void DismissLockAndContinue() + public void DismissLockAndContinue() { DismissViewController(false, async () => await OnLockDismissedAsync()); } @@ -334,7 +394,13 @@ namespace Bit.iOS.Autofill { try { - if (_context.PasswordCredentialIdentity != null) + if (_context.IsCreatingPasskey) + { + _context.UnlockVaultTcs.SetResult(true); + return; + } + + if (_context.PasswordCredentialIdentity != null || _context.IsPasskey) { await MainThread.InvokeOnMainThreadAsync(() => ProvideCredentialAsync()); return; @@ -364,6 +430,20 @@ namespace Bit.iOS.Autofill { try { + if (_context.IsPasskey && UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + { + if (_context.PasskeyCredentialIdentity is null) + { + CancelRequest(ASExtensionErrorCode.Failed); + } + + await CompleteAssertionRequestAsync(_context.PasskeyCredentialIdentity.RelyingPartyIdentifier, + _context.PasskeyCredentialIdentity.UserHandle, + _context.PasskeyCredentialIdentity.CredentialId, + _context.RecordIdentifier); + return; + } + if (!ServiceContainer.TryResolve(out var cipherService) || _context.RecordIdentifier == null) @@ -411,12 +491,6 @@ namespace Bit.iOS.Autofill } } - if (_context.IsPasskey) - { - await CompleteAssertionRequestAsync(decCipher); - return; - } - string totpCode = null; if (await _stateService.Value.GetDisableAutoTotpCopyAsync() != true) { @@ -702,11 +776,11 @@ namespace Bit.iOS.Autofill } break; case NavigationTarget.Lock: - DismissViewController(false, () => PerformSegue("lockPasswordSegue", this)); + DismissViewController(false, () => PerformSegue(SegueConstants.LOCK, this)); break; case NavigationTarget.AutofillCiphers: case NavigationTarget.Home: - DismissViewController(false, () => PerformSegue("loginListSegue", this)); + DismissViewController(false, () => PerformSegue(SegueConstants.LOGIN_LIST, this)); break; } } diff --git a/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs new file mode 100644 index 000000000..0a154c83f --- /dev/null +++ b/src/iOS.Autofill/Fido2MakeCredentialUserInterface.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Abstractions; +using Bit.iOS.Autofill.Models; + +namespace Bit.iOS.Autofill +{ + public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialUserInterface + { + private readonly Func _ensureUnlockedVaultCallback; + private readonly Context _context; + private readonly Action _onConfirmingNewCredential; + + public Fido2MakeCredentialUserInterface(Func ensureUnlockedVaultCallback, Context context, Action onConfirmingNewCredential) + { + _ensureUnlockedVaultCallback = ensureUnlockedVaultCallback; + _context = context; + _onConfirmingNewCredential = onConfirmingNewCredential; + } + + public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams) + { + _context.ConfirmNewCredentialTcs?.SetCanceled(); + _context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>(); + + _onConfirmingNewCredential(); + + return await _context.ConfirmNewCredentialTcs.Task; + } + + // iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently. + public Task InformExcludedCredentialAsync(string[] existingCipherIds) => Task.CompletedTask; + + public Task EnsureUnlockedVaultAsync() => _ensureUnlockedVaultCallback(); + } +} diff --git a/src/iOS.Autofill/ILoginListViewController.cs b/src/iOS.Autofill/ILoginListViewController.cs new file mode 100644 index 000000000..8f515d3f3 --- /dev/null +++ b/src/iOS.Autofill/ILoginListViewController.cs @@ -0,0 +1,10 @@ +using Bit.iOS.Autofill.Models; + +namespace Bit.iOS.Autofill +{ + public interface ILoginListViewController + { + Context Context { get; } + CredentialProviderViewController CPViewController { get; } + } +} diff --git a/src/iOS.Autofill/ListItems/HeaderItemView.cs b/src/iOS.Autofill/ListItems/HeaderItemView.cs new file mode 100644 index 000000000..4b556f02b --- /dev/null +++ b/src/iOS.Autofill/ListItems/HeaderItemView.cs @@ -0,0 +1,59 @@ +using Bit.Core.Services; +using Foundation; +using ObjCRuntime; +using UIKit; + +namespace Bit.iOS.Autofill.ListItems +{ + public class HeaderItemView : UITableViewHeaderFooterView + { + private readonly UILabel _header = new UILabel(); + private readonly UIView _separator = new UIView(); + + public HeaderItemView(NSString reuseIdentifier) + : base(reuseIdentifier) + { + Setup(); + } + + protected internal HeaderItemView(NativeHandle handle) : base(handle) + { + Setup(); + } + + public void SetHeaderText(string text) => _header.Text = text; + + private void Setup() + { + try + { + _header.TextColor = UIColor.FromName(ColorConstants.LIGHT_TEXT_MUTED); + _header.Font = UIFont.SystemFontOfSize(15); + _separator.BackgroundColor = UIColor.FromName(ColorConstants.LIGHT_SECONDARY_300); + + _header.TranslatesAutoresizingMaskIntoConstraints = false; + _separator.TranslatesAutoresizingMaskIntoConstraints = false; + + ContentView.AddSubview(_header); + ContentView.AddSubview(_separator); + + NSLayoutConstraint.ActivateConstraints(new NSLayoutConstraint[] + { + _header.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 9), + _header.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 9), + _header.TopAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TopAnchor, 3), + + _separator.HeightAnchor.ConstraintEqualTo(2), + _separator.TopAnchor.ConstraintEqualTo(_header.BottomAnchor, 8), + _separator.LeadingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.LeadingAnchor, 5), + _separator.TrailingAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.TrailingAnchor, 5), + _separator.BottomAnchor.ConstraintEqualTo(ContentView.LayoutMarginsGuide.BottomAnchor, 2) + }); + } + catch (System.Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + } + } +} diff --git a/src/iOS.Autofill/LoginListViewController.cs b/src/iOS.Autofill/LoginListViewController.cs index c84cfcf52..35a2d8d97 100644 --- a/src/iOS.Autofill/LoginListViewController.cs +++ b/src/iOS.Autofill/LoginListViewController.cs @@ -1,22 +1,28 @@ using System; -using Bit.App.Abstractions; +using System.Linq; +using System.Threading.Tasks; using Bit.App.Controls; using Bit.Core.Abstractions; using Bit.Core.Resources.Localization; +using Bit.Core.Services; using Bit.Core.Utilities; +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 CoreGraphics; using Foundation; using UIKit; namespace Bit.iOS.Autofill { - public partial class LoginListViewController : ExtendedUIViewController + public partial class LoginListViewController : ExtendedUIViewController, ILoginListViewController { + internal const string HEADER_SECTION_IDENTIFIER = "headerSectionId"; + UIBarButtonItem _cancelButton; UIControl _accountSwitchButton; @@ -24,59 +30,92 @@ namespace Bit.iOS.Autofill : base(handle) { DismissModalAction = Cancel; - PasswordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); } public Context Context { get; set; } public CredentialProviderViewController CPViewController { get; set; } - public IPasswordRepromptService PasswordRepromptService { get; private set; } AccountSwitchingOverlayView _accountSwitchingOverlayView; AccountSwitchingOverlayHelper _accountSwitchingOverlayHelper; - LazyResolve _broadcasterService = new LazyResolve("broadcasterService"); - LazyResolve _logger = new LazyResolve("logger"); + LazyResolve _broadcasterService = new LazyResolve(); + LazyResolve _cipherService = new LazyResolve(); + LazyResolve _platformUtilsService = new LazyResolve(); + LazyResolve _logger = new LazyResolve(); + bool _alreadyLoadItemsOnce = false; public async override void ViewDidLoad() { - _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); - - base.ViewDidLoad(); - - SubscribeSyncCompleted(); - - NavItem.Title = AppResources.Items; - _cancelButton.Title = AppResources.Cancel; - - TableView.RowHeight = UITableView.AutomaticDimension; - TableView.EstimatedRowHeight = 44; - TableView.BackgroundColor = ThemeHelpers.BackgroundColor; - TableView.Source = new TableSource(this); - await ((TableSource)TableView.Source).LoadItemsAsync(); - - _alreadyLoadItemsOnce = true; - - var storageService = ServiceContainer.Resolve("storageService"); - var needsAutofillReplacement = await storageService.GetAsync( - Core.Constants.AutofillNeedsIdentityReplacementKey); - if (needsAutofillReplacement.GetValueOrDefault()) + try { - await ASHelpers.ReplaceAllIdentitiesAsync(); + _cancelButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel, CancelButton_TouchUpInside); + + base.ViewDidLoad(); + + SubscribeSyncCompleted(); + + NavItem.Title = Context.IsCreatingPasskey ? AppResources.SavePasskey : AppResources.Items; + _cancelButton.Title = AppResources.Cancel; + + TableView.RowHeight = UITableView.AutomaticDimension; + TableView.EstimatedRowHeight = 44; + TableView.BackgroundColor = ThemeHelpers.BackgroundColor; + TableView.Source = new TableSource(this); + if (Context.IsCreatingPasskey) + { + TableView.SectionHeaderHeight = 55; + TableView.RegisterClassForHeaderFooterViewReuse(typeof(HeaderItemView), HEADER_SECTION_IDENTIFIER); + } + + if (UIDevice.CurrentDevice.CheckSystemVersion(15, 0)) + { + TableView.SectionHeaderTopPadding = 0; + } + + await ((TableSource)TableView.Source).LoadAsync(); + + if (Context.IsCreatingPasskey) + { + _headerLabel.Text = AppResources.ChooseALoginToSaveThisPasskeyTo; + _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; + + _headerView.Hidden = false; + } + + _alreadyLoadItemsOnce = true; + + var storageService = ServiceContainer.Resolve("storageService"); + var needsAutofillReplacement = await storageService.GetAsync( + 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); } - - _accountSwitchingOverlayHelper = new AccountSwitchingOverlayHelper(); - - _accountSwitchButton = await _accountSwitchingOverlayHelper.CreateAccountSwitchToolbarButtonItemCustomViewAsync(); - _accountSwitchButton.TouchUpInside += AccountSwitchedButton_TouchUpInside; - - NavItem.SetLeftBarButtonItems(new UIBarButtonItem[] + catch (Exception ex) { - _cancelButton, - new UIBarButtonItem(_accountSwitchButton) - }, false); - - _accountSwitchingOverlayView = _accountSwitchingOverlayHelper.CreateAccountSwitchingOverlayView(OverlayView); + LoggerHelper.LogEvenIfCantBeResolved(ex); + } } private void CancelButton_TouchUpInside(object sender, EventArgs e) @@ -91,17 +130,37 @@ namespace Bit.iOS.Autofill private void Cancel() { - CPViewController.CompleteRequest(); + CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled); } partial void AddBarButton_Activated(UIBarButtonItem sender) { - PerformSegue("loginAddSegue", this); + PerformSegue(SegueConstants.ADD_LOGIN, this); } partial void SearchBarButton_Activated(UIBarButtonItem sender) { - PerformSegue("loginSearchFromListSegue", this); + PerformSegue(SegueConstants.LOGIN_SEARCH_FROM_LIST, this); + } + + partial void EmptyButton_Activated(UIButton sender) + { + SavePasskeyAsNewLoginAsync().FireAndForget(ex => + { + _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred).FireAndForget(); + }); + } + + private async Task SavePasskeyAsNewLoginAsync() + { + 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 cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCredentialIdentity.RelyingPartyIdentifier); + Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true)); } public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) @@ -136,7 +195,7 @@ namespace Bit.iOS.Autofill { try { - await ((TableSource)TableView.Source).LoadItemsAsync(); + await ((TableSource)TableView.Source).LoadAsync(); TableView.ReloadData(); } catch (Exception ex) @@ -148,6 +207,13 @@ namespace Bit.iOS.Autofill }); } + public void OnEmptyList() + { + _emptyView.Hidden = false; + _headerView.Hidden = false; + TableView.Hidden = true; + } + public override void ViewDidUnload() { base.ViewDidUnload(); @@ -159,8 +225,15 @@ namespace Bit.iOS.Autofill { DismissViewController(true, async () => { - await ((TableSource)TableView.Source).LoadItemsAsync(); - TableView.ReloadData(); + try + { + await ((TableSource)TableView.Source).LoadAsync(); + TableView.ReloadData(); + } + catch (Exception ex) + { + _logger.Value.Exception(ex); + } }); } @@ -179,20 +252,61 @@ namespace Bit.iOS.Autofill base.Dispose(disposing); } - public class TableSource : ExtensionTableSource + public class TableSource : BaseLoginListTableSource { - private LoginListViewController _controller; - public TableSource(LoginListViewController controller) - : base(controller.Context, controller) + : base(controller) { - _controller = controller; } - public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) + protected override string LoginAddSegue => SegueConstants.ADD_LOGIN; + + public override async Task LoadAsync(bool urlFilter = true, string searchFilter = null) { - await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, - _controller.CPViewController, _controller, _controller.PasswordRepromptService, "loginAddSegue"); + try + { + await base.LoadAsync(urlFilter, searchFilter); + + if (Context.IsCreatingPasskey && !Items.Any()) + { + Controller?.OnEmptyList(); + } + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + } + + public override UIView GetViewForHeader(UITableView tableView, nint section) + { + try + { + if (Context.IsCreatingPasskey + && + tableView.DequeueReusableHeaderFooterView(LoginListViewController.HEADER_SECTION_IDENTIFIER) is HeaderItemView headerItemView) + { + headerItemView.SetHeaderText(AppResources.ChooseALoginToSaveThisPasskeyTo); + return headerItemView; + } + + return new UIView(CGRect.Empty);// base.GetViewForHeader(tableView, section); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + return new UIView(); + } + } + + public override nint RowsInSection(UITableView tableview, nint section) + { + if (Context.IsCreatingPasskey) + { + return Items?.Count() ?? 0; + } + + return base.RowsInSection(tableview, section); } } } diff --git a/src/iOS.Autofill/LoginListViewController.designer.cs b/src/iOS.Autofill/LoginListViewController.designer.cs index 6451849c8..cb6929688 100644 --- a/src/iOS.Autofill/LoginListViewController.designer.cs +++ b/src/iOS.Autofill/LoginListViewController.designer.cs @@ -12,6 +12,24 @@ namespace Bit.iOS.Autofill [Register ("LoginListViewController")] partial class LoginListViewController { + [Outlet] + UIKit.UIView _emptyView { get; set; } + + [Outlet] + UIKit.UIButton _emptyViewButton { get; set; } + + [Outlet] + UIKit.UIImageView _emptyViewImage { get; set; } + + [Outlet] + UIKit.UILabel _emptyViewLabel { get; set; } + + [Outlet] + UIKit.UILabel _headerLabel { get; set; } + + [Outlet] + UIKit.UIView _headerView { get; set; } + [Outlet] [GeneratedCode ("iOS Designer", "1.0")] UIKit.UIBarButtonItem AddBarButton { get; set; } @@ -32,11 +50,44 @@ namespace Bit.iOS.Autofill [Action ("AddBarButton_Activated:")] partial void AddBarButton_Activated (UIKit.UIBarButtonItem sender); + [Action ("EmptyButton_Activated:")] + partial void EmptyButton_Activated (UIKit.UIButton sender); + [Action ("SearchBarButton_Activated:")] partial void SearchBarButton_Activated (UIKit.UIBarButtonItem sender); void ReleaseDesignerOutlets () { + if (_emptyView != null) { + _emptyView.Dispose (); + _emptyView = null; + } + + if (_emptyViewButton != null) { + _emptyViewButton.Dispose (); + _emptyViewButton = null; + } + + if (_emptyViewImage != null) { + _emptyViewImage.Dispose (); + _emptyViewImage = null; + } + + if (_emptyViewLabel != null) { + _emptyViewLabel.Dispose (); + _emptyViewLabel = null; + } + + if (_headerLabel != null) { + _headerLabel.Dispose (); + _headerLabel = null; + } + + if (_headerView != null) { + _headerView.Dispose (); + _headerView = null; + } + if (AddBarButton != null) { AddBarButton.Dispose (); AddBarButton = null; diff --git a/src/iOS.Autofill/LoginSearchViewController.cs b/src/iOS.Autofill/LoginSearchViewController.cs index 82f19fcba..edd1369e8 100644 --- a/src/iOS.Autofill/LoginSearchViewController.cs +++ b/src/iOS.Autofill/LoginSearchViewController.cs @@ -12,19 +12,17 @@ using Bit.Core.Utilities; namespace Bit.iOS.Autofill { - public partial class LoginSearchViewController : ExtendedUITableViewController + public partial class LoginSearchViewController : ExtendedUITableViewController, ILoginListViewController { public LoginSearchViewController(IntPtr handle) : base(handle) { DismissModalAction = Cancel; - PasswordRepromptService = ServiceContainer.Resolve("passwordRepromptService"); } public Context Context { get; set; } public CredentialProviderViewController CPViewController { get; set; } public bool FromList { get; set; } - public IPasswordRepromptService PasswordRepromptService { get; private set; } public async override void ViewDidLoad() { @@ -39,7 +37,7 @@ namespace Bit.iOS.Autofill TableView.EstimatedRowHeight = 44; TableView.Source = new TableSource(this); SearchBar.Delegate = new ExtensionSearchDelegate(TableView); - await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); + await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text); } public override void ViewDidAppear(bool animated) @@ -61,13 +59,13 @@ namespace Bit.iOS.Autofill } else { - CPViewController.CompleteRequest(); + CPViewController.CancelRequest(AuthenticationServices.ASExtensionErrorCode.UserCanceled); } } partial void AddBarButton_Activated(UIBarButtonItem sender) { - PerformSegue("loginAddFromSearchSegue", this); + PerformSegue(SegueConstants.ADD_LOGIN_FROM_SEARCH, this); } public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender) @@ -88,29 +86,19 @@ namespace Bit.iOS.Autofill { DismissViewController(true, async () => { - await ((TableSource)TableView.Source).LoadItemsAsync(false, SearchBar.Text); + await ((TableSource)TableView.Source).LoadAsync(false, SearchBar.Text); TableView.ReloadData(); }); } - public class TableSource : ExtensionTableSource + public class TableSource : BaseLoginListTableSource { - private Context _context; - private LoginSearchViewController _controller; - public TableSource(LoginSearchViewController controller) - : base(controller.Context, controller) + : base(controller) { - _context = controller.Context; - _controller = controller; } - public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) - { - await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, - _controller.CPViewController, _controller, _controller.PasswordRepromptService, - "loginAddFromSearchSegue"); - } + protected override string LoginAddSegue => SegueConstants.ADD_LOGIN_FROM_SEARCH; } } } diff --git a/src/iOS.Autofill/MainInterface.storyboard b/src/iOS.Autofill/MainInterface.storyboard index 4521ddbc8..d684303bc 100644 --- a/src/iOS.Autofill/MainInterface.storyboard +++ b/src/iOS.Autofill/MainInterface.storyboard @@ -4,6 +4,7 @@ + @@ -131,7 +132,75 @@ - + + + @@ -173,13 +242,20 @@ + + + + + + + @@ -207,6 +283,12 @@ + + + + + + @@ -575,7 +657,14 @@ + + + + + + + diff --git a/src/iOS.Autofill/Models/Context.cs b/src/iOS.Autofill/Models/Context.cs index a7b6212eb..b4b496f49 100644 --- a/src/iOS.Autofill/Models/Context.cs +++ b/src/iOS.Autofill/Models/Context.cs @@ -1,6 +1,9 @@ -using AuthenticationServices; +using System.Threading.Tasks; +using AuthenticationServices; +using Bit.Core.Abstractions; using Bit.iOS.Core.Models; using Foundation; +using ObjCRuntime; using UIKit; namespace Bit.iOS.Autofill.Models @@ -12,14 +15,17 @@ 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 TaskCompletionSource UnlockVaultTcs { get; set; } + public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; } public ASPasskeyCredentialIdentity PasskeyCredentialIdentity { get { - if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) + if (PasskeyCredentialRequest != null && UIDevice.CurrentDevice.CheckSystemVersion(17, 0)) { - return PasskeyCredentialRequest?.CredentialIdentity as ASPasskeyCredentialIdentity; + return Runtime.GetNSObject(PasskeyCredentialRequest.CredentialIdentity.GetHandle()); } return null; } diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/Contents.json b/src/iOS.Autofill/Resources/Assets.xcassets/Contents.json new file mode 100755 index 000000000..73c00596a --- /dev/null +++ b/src/iOS.Autofill/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/LightSecondary300.colorset/Contents.json b/src/iOS.Autofill/Resources/Assets.xcassets/LightSecondary300.colorset/Contents.json new file mode 100644 index 000000000..a9b97e061 --- /dev/null +++ b/src/iOS.Autofill/Resources/Assets.xcassets/LightSecondary300.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.863", + "green" : "0.831", + "red" : "0.808" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.924", + "green" : "0.879", + "red" : "0.854" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/LightTextMuted.colorset/Contents.json b/src/iOS.Autofill/Resources/Assets.xcassets/LightTextMuted.colorset/Contents.json new file mode 100644 index 000000000..137c1521a --- /dev/null +++ b/src/iOS.Autofill/Resources/Assets.xcassets/LightTextMuted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.494", + "green" : "0.459", + "red" : "0.427" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.783", + "green" : "0.718", + "red" : "0.671" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json new file mode 100755 index 000000000..2c8771189 --- /dev/null +++ b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Contents.json @@ -0,0 +1,25 @@ +{ + "images" : [ + { + "filename" : "Empty-items-state.pdf", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Empty-items-state-dark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf new file mode 100755 index 000000000..f68a9a3b6 Binary files /dev/null and b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state-dark.pdf differ diff --git a/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf new file mode 100755 index 000000000..1aa4dc22f Binary files /dev/null and b/src/iOS.Autofill/Resources/Assets.xcassets/empty_items_state.imageset/Empty-items-state.pdf differ diff --git a/src/iOS.Autofill/SegueConstants.cs b/src/iOS.Autofill/SegueConstants.cs new file mode 100644 index 000000000..50530fb6a --- /dev/null +++ b/src/iOS.Autofill/SegueConstants.cs @@ -0,0 +1,13 @@ +namespace Bit.iOS.Autofill +{ + public static class SegueConstants + { + public const string LOGIN_LIST = "loginListSegue"; + public const string LOCK = "lockPasswordSegue"; + public const string LOGIN_SEARCH = "loginSearchSegue"; + public const string SETUP = "setupSegue"; + public const string ADD_LOGIN = "loginAddSegue"; + public const string LOGIN_SEARCH_FROM_LIST = "loginSearchFromListSegue"; + public const string ADD_LOGIN_FROM_SEARCH = "loginAddFromSearchSegue"; + } +} diff --git a/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs new file mode 100644 index 000000000..a88daa1ca --- /dev/null +++ b/src/iOS.Autofill/Utilities/BaseLoginListTableSource.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.App.Abstractions; +using Bit.Core.Abstractions; +using Bit.Core.Resources.Localization; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.iOS.Autofill.Models; +using Bit.iOS.Core.Views; +using Foundation; +using UIKit; + +namespace Bit.iOS.Autofill.Utilities +{ + public abstract class BaseLoginListTableSource : ExtensionTableSource + where T : UIViewController, ILoginListViewController + { + private IPasswordRepromptService _passwordRepromptService; + private readonly LazyResolve _platformUtilsService = new LazyResolve(); + + public BaseLoginListTableSource(T controller) + : base(controller.Context, controller) + { + _controller = controller; + _passwordRepromptService = ServiceContainer.Resolve(); + } + + protected Context Context => (Context)_context; + protected T Controller => (T)_controller; + + protected abstract string LoginAddSegue { get; } + + public async override void RowSelected(UITableView tableView, NSIndexPath indexPath) + { + try + { + if (Context.IsCreatingPasskey) + { + await SelectRowForPasskeyCreationAsync(tableView, indexPath); + return; + } + + await AutofillHelpers.TableRowSelectedAsync(tableView, indexPath, this, + Controller.CPViewController, Controller, _passwordRepromptService, LoginAddSegue); + } + catch (Exception ex) + { + LoggerHelper.LogEvenIfCantBeResolved(ex); + } + } + + private async Task SelectRowForPasskeyCreationAsync(UITableView tableView, NSIndexPath indexPath) + { + tableView.DeselectRow(indexPath, true); + tableView.EndEditing(true); + + var item = Items.ElementAt(indexPath.Row); + if (item is null) + { + await _platformUtilsService.Value.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred); + return; + } + + if (item.CipherView.Login.HasFido2Credentials + && + !await _platformUtilsService.Value.ShowDialogAsync( + AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey, + AppResources.OverwritePasskey, + AppResources.Yes, + AppResources.No)) + { + return; + } + + if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(item.Reprompt)) + { + return; + } + + // TODO: Check user verification + + Context.ConfirmNewCredentialTcs.SetResult((item.Id, true)); + } + } +} + diff --git a/src/iOS.Autofill/iOS.Autofill.csproj b/src/iOS.Autofill/iOS.Autofill.csproj index 053c3a273..0323d1c9f 100644 --- a/src/iOS.Autofill/iOS.Autofill.csproj +++ b/src/iOS.Autofill/iOS.Autofill.csproj @@ -7,6 +7,7 @@ com.8bit.bitwarden.autofill 1.0 1 + Bit.iOS.Autofill False @@ -44,6 +45,9 @@ + + + @@ -82,6 +86,12 @@ + + + + + + @@ -180,4 +190,7 @@ + + + diff --git a/src/iOS.Core/Utilities/ASHelpers.cs b/src/iOS.Core/Utilities/ASHelpers.cs index 0f2a0fdf0..22fcf7e7a 100644 --- a/src/iOS.Core/Utilities/ASHelpers.cs +++ b/src/iOS.Core/Utilities/ASHelpers.cs @@ -3,6 +3,7 @@ using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Models.View; using Bit.Core.Utilities; +using Foundation; using UIKit; namespace Bit.iOS.Core.Utilities @@ -146,7 +147,7 @@ namespace Bit.iOS.Core.Utilities return new ASPasskeyCredentialIdentity(cipher.Login.MainFido2Credential.RpId, cipher.Login.MainFido2Credential.UserName, - cipher.Login.MainFido2Credential.CredentialId, + NSData.FromArray(cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat()), cipher.Login.MainFido2Credential.UserHandle, cipher.Id); } diff --git a/src/iOS.Core/Utilities/NSDataExtensions.cs b/src/iOS.Core/Utilities/NSDataExtensions.cs new file mode 100644 index 000000000..3e58ae3c7 --- /dev/null +++ b/src/iOS.Core/Utilities/NSDataExtensions.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; +using Foundation; + +namespace Bit.iOS.Core.Utilities +{ + public static class NSDataExtensions + { + public static byte[] ToByteArray(this NSData data) + { + var bytes = new byte[data.Length]; + Marshal.Copy(data.Bytes, bytes, 0, Convert.ToInt32(data.Length)); + return bytes; + } + } +} diff --git a/src/iOS.Core/Utilities/iOSCoreHelpers.cs b/src/iOS.Core/Utilities/iOSCoreHelpers.cs index 8e55434f1..26ac35529 100644 --- a/src/iOS.Core/Utilities/iOSCoreHelpers.cs +++ b/src/iOS.Core/Utilities/iOSCoreHelpers.cs @@ -138,6 +138,7 @@ namespace Bit.iOS.Core.Utilities logger!.Exception(nreAppGroupContainer); throw nreAppGroupContainer; } + var liteDbStorage = new LiteDbStorageService( Path.Combine(appGroupContainer.Path, "Library", "bitwarden.db")); var localizeService = new LocalizeService(); @@ -189,6 +190,11 @@ namespace Bit.iOS.Core.Utilities public static void RegisterFinallyBeforeBootstrap() { + ServiceContainer.Register(new Fido2AuthenticatorService( + ServiceContainer.Resolve(), + ServiceContainer.Resolve(), + ServiceContainer.Resolve())); + ServiceContainer.Register(new WatchDeviceService(ServiceContainer.Resolve(), ServiceContainer.Resolve(), ServiceContainer.Resolve(), diff --git a/src/iOS.Core/Views/ExtensionTableSource.cs b/src/iOS.Core/Views/ExtensionTableSource.cs index 94bda0889..b44c00449 100644 --- a/src/iOS.Core/Views/ExtensionTableSource.cs +++ b/src/iOS.Core/Views/ExtensionTableSource.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Bit.Core.Resources.Localization; +using System.Diagnostics; using Bit.Core.Abstractions; using Bit.Core.Models.View; +using Bit.Core.Resources.Localization; using Bit.Core.Utilities; using Bit.iOS.Core.Controllers; using Bit.iOS.Core.Models; @@ -25,8 +20,8 @@ namespace Bit.iOS.Core.Views protected ITotpService _totpService; protected IStateService _stateService; protected ISearchService _searchService; - private AppExtensionContext _context; - private UIViewController _controller; + protected AppExtensionContext _context; + protected UIViewController _controller; public ExtensionTableSource(AppExtensionContext context, UIViewController controller) { @@ -36,11 +31,19 @@ namespace Bit.iOS.Core.Views _searchService = ServiceContainer.Resolve("searchService"); _context = context; _controller = controller; + + Items = new List(); } public IEnumerable Items { get; private set; } - public async Task LoadItemsAsync(bool urlFilter = true, string searchFilter = null) + public virtual async Task LoadAsync(bool urlFilter = true, string searchFilter = null) + { + _allItems = await LoadItemsAsync(urlFilter, searchFilter); + FilterResults(searchFilter, new CancellationToken()); + } + + protected virtual async Task> LoadItemsAsync(bool urlFilter = true, string? searchFilter = null) { var combinedLogins = new List(); @@ -62,11 +65,10 @@ namespace Bit.iOS.Core.Views combinedLogins.AddRange(logins); } - _allItems = combinedLogins + return combinedLogins .Where(c => c.Type == Bit.Core.Enums.CipherType.Login && !c.IsDeleted) .Select(s => new CipherViewModel(s)) - .ToList() ?? new List(); - FilterResults(searchFilter, new CancellationToken()); + .ToList(); } public void FilterResults(string searchFilter, CancellationToken ct) @@ -87,7 +89,7 @@ namespace Bit.iOS.Core.Views } } - public IEnumerable TableItems { get; set; } + //public IEnumerable TableItems { get; set; } public override nint RowsInSection(UITableView tableview, nint section) { @@ -135,9 +137,9 @@ namespace Bit.iOS.Core.Views cell.DetailTextLabel.Text = item.Username; } - public async Task GetTotpAsync(CipherViewModel item) + public async Task GetTotpAsync(CipherViewModel item) { - string totp = null; + string? totp = null; var accessPremium = await _stateService.CanAccessPremiumAsync(); if (accessPremium || (item?.CipherView.OrganizationUseTotp ?? false)) { diff --git a/src/iOS.Extension/LoginListViewController.cs b/src/iOS.Extension/LoginListViewController.cs index 67c7bf300..90e08754e 100644 --- a/src/iOS.Extension/LoginListViewController.cs +++ b/src/iOS.Extension/LoginListViewController.cs @@ -44,7 +44,7 @@ namespace Bit.iOS.Extension TableView.RowHeight = UITableView.AutomaticDimension; TableView.EstimatedRowHeight = 44; TableView.Source = new TableSource(this); - await ((TableSource)TableView.Source).LoadItemsAsync(); + await ((TableSource)TableView.Source).LoadAsync(); } public bool CanAutoFill() @@ -93,7 +93,7 @@ namespace Bit.iOS.Extension { DismissViewController(true, async () => { - await ((TableSource)TableView.Source).LoadItemsAsync(); + await ((TableSource)TableView.Source).LoadAsync(); TableView.ReloadData(); }); }