diff --git a/src/Core/Abstractions/IFido2UserInterface.cs b/src/Core/Abstractions/IFido2UserInterface.cs
new file mode 100644
index 0000000000..b01b52d232
--- /dev/null
+++ b/src/Core/Abstractions/IFido2UserInterface.cs
@@ -0,0 +1,46 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ ///
+ /// Parameters used to ask the user to pick a credential from a list of existing credentials.
+ ///
+ public struct Fido2PickCredentialParams
+ {
+ ///
+ /// The IDs of the credentials that the user can pick from.
+ ///
+ public string[] CipherIds { get; set; }
+
+ ///
+ /// Whether or not the user must be verified before completing the operation.
+ ///
+ public bool UserVerification { get; set; }
+ }
+
+ ///
+ /// The result of asking the user to pick a credential from a list of existing credentials.
+ ///
+ public struct Fido2PickCredentialResult
+ {
+ ///
+ /// The ID of the cipher that contains the credentials the user picked.
+ ///
+ public string CipherId { get; set; }
+
+ ///
+ /// Whether or not the user was verified before completing the operation.
+ ///
+ public bool UserVerified { get; set; }
+ }
+
+ public interface IFido2UserInterface
+ {
+ ///
+ /// Ask the user to pick a credential from a list of existing credentials.
+ ///
+ /// The parameters to use when asking the user to pick a credential.
+ /// The ID of the cipher that contains the credentials the user picked.
+ Task PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams);
+ }
+}
diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs
index 92d3ff85b7..5086c72d3e 100644
--- a/src/Core/Models/View/Fido2CredentialView.cs
+++ b/src/Core/Models/View/Fido2CredentialView.cs
@@ -29,7 +29,7 @@ namespace Bit.Core.Models.View
public override string SubTitle => UserName;
public override List> LinkedFieldOptions => new List>();
- public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
+ public bool IsDiscoverable => bool.TryParse(Discoverable, out var isDiscoverable) && isDiscoverable;
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
public string LaunchUri => $"https://{RpId}";
diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs
index 26a8024f10..fb897044d1 100644
--- a/src/Core/Services/Fido2AuthenticatorService.cs
+++ b/src/Core/Services/Fido2AuthenticatorService.cs
@@ -1,27 +1,95 @@
using Bit.Core.Abstractions;
+using Bit.Core.Models.View;
+using Bit.Core.Enums;
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Services
{
public class Fido2AuthenticatorService : IFido2AuthenticatorService
{
+ private INativeLogService _logService;
private ICipherService _cipherService;
+ private ISyncService _syncService;
+ private IFido2UserInterface _userInterface;
- public Fido2AuthenticatorService(ICipherService cipherService)
+ public Fido2AuthenticatorService(INativeLogService logService, ICipherService cipherService, ISyncService syncService, IFido2UserInterface userInterface)
{
+ _logService = logService;
_cipherService = cipherService;
+ _syncService = syncService;
+ _userInterface = userInterface;
}
- public Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
+ public async Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams)
{
- throw new NotAllowedError();
+ // throw new NotAllowedError();
+ List cipherOptions;
+
+ // await userInterfaceSession.ensureUnlockedVault();
+ await _syncService.FullSyncAsync(false);
+
+ if (assertionParams.AllowCredentialDescriptorList?.Length > 0) {
+ cipherOptions = await FindCredentialsById(
+ assertionParams.AllowCredentialDescriptorList,
+ assertionParams.RpId
+ );
+ } else {
+ cipherOptions = new List();
+ // cipherOptions = await this.findCredentialsByRp(params.rpId);
+ }
+
+ if (cipherOptions.Count == 0) {
+ _logService.Info(
+ "[Fido2Authenticator] Aborting because no matching credentials were found in the vault."
+ );
+
+ throw new NotAllowedError();
+ }
+
+ var response = await _userInterface.PickCredentialAsync(new Fido2PickCredentialParams {
+ CipherIds = cipherOptions.Select((cipher) => cipher.Id).ToArray(),
+ UserVerification = assertionParams.RequireUserVerification
+ });
// TODO: IMPLEMENT this
- // return Task.FromResult(new Fido2AuthenticatorGetAssertionResult
- // {
- // AuthenticatorData = new byte[32],
- // Signature = new byte[8]
- // });
+ return new Fido2AuthenticatorGetAssertionResult
+ {
+ AuthenticatorData = new byte[32],
+ Signature = new byte[8]
+ };
}
+
+ private async Task> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId)
+ {
+ var ids = new List();
+
+ foreach (var credential in credentials)
+ {
+ try
+ {
+ ids.Add(GuidToStandardFormat(credential.Id));
+ }
+ catch {}
+ }
+
+ if (ids.Count == 0)
+ {
+ return new List();
+ }
+
+ var ciphers = await _cipherService.GetAllDecryptedAsync();
+ return ciphers.FindAll((cipher) =>
+ !cipher.IsDeleted &&
+ cipher.Type == CipherType.Login &&
+ cipher.Login.HasFido2Credentials &&
+ cipher.Login.Fido2Credentials[0].RpId == rpId &&
+ ids.Contains(cipher.Login.Fido2Credentials[0].CredentialId)
+ );
+ }
+
+ private string GuidToStandardFormat(byte[] bytes)
+ {
+ return new Guid(bytes).ToString();
+ }
}
}
diff --git a/test/Core.Test/Services/Fido2AuthenticatorTests.cs b/test/Core.Test/Services/Fido2AuthenticatorTests.cs
index 76b144bf42..577f3a8cdf 100644
--- a/test/Core.Test/Services/Fido2AuthenticatorTests.cs
+++ b/test/Core.Test/Services/Fido2AuthenticatorTests.cs
@@ -5,6 +5,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
+using Bit.Core.Enums;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Utilities.Fido2;
using Bit.Test.Common.AutoFixture;
@@ -14,6 +15,7 @@ using NSubstitute.ExceptionExtensions;
using Xunit;
using Bit.Core.Utilities;
using System.Collections.Generic;
+using System.Linq;
namespace Bit.Core.Test.Services
{
@@ -33,37 +35,72 @@ namespace Bit.Core.Test.Services
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
{
- var credentialId = RandomBytes(32);
+ var credentialId = Guid.NewGuid();
aParams.RpId = "bitwarden.com";
aParams.AllowCredentialDescriptorList = [
new PublicKeyCredentialDescriptor {
- Id = credentialId,
+ Id = credentialId.ToByteArray(),
Type = "public-key"
}
];
- sutProvider.GetDependency().GetAllDecryptedAsync().Returns(new List {
- new CipherView {
- Login = new LoginView {
- Fido2Credentials = new List {
- new Fido2CredentialView {
- CredentialId = CoreHelpers.Base64UrlEncode(credentialId),
- RpId = "mismatch-rpid"
- }
- }
- }
- }
- });
+ sutProvider.GetDependency().GetAllDecryptedAsync().Returns([
+ CreateCipherView(credentialId.ToString(), "mismatch-rpid", false),
+ ]);
var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.GetAssertionAsync(aParams));
}
#endregion
+ #region vault contains credential
+
+ [Theory]
+ [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
+ public async Task GetAssertionAsync_AsksForAllCredentials_ParamsContainsAllowedCredentialsList(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams)
+ {
+ var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() };
+ List ciphers = [
+ CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false),
+ CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true)
+ ];
+ aParams.RpId = "bitwarden.com";
+ aParams.AllowCredentialDescriptorList = credentialIds.Select((credentialId) => new PublicKeyCredentialDescriptor {
+ Id = credentialId.ToByteArray(),
+ Type = "public-key"
+ }).ToArray();
+ sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers);
+
+ await sutProvider.Sut.GetAssertionAsync(aParams);
+
+ await sutProvider.GetDependency().Received().PickCredentialAsync(Arg.Is(
+ (pickCredentialParams) => pickCredentialParams.CipherIds.SequenceEqual(ciphers.Select((cipher) => cipher.Id)) && pickCredentialParams.UserVerification == aParams.RequireUserVerification
+ ));
+ }
+
+ #endregion
+
private byte[] RandomBytes(int length)
{
var bytes = new byte[length];
new Random().NextBytes(bytes);
return bytes;
}
+
+ #nullable enable
+ private CipherView CreateCipherView(string? credentialId, string? rpId, bool? discoverable)
+ {
+ return new CipherView {
+ Type = CipherType.Login,
+ Login = new LoginView {
+ Fido2Credentials = new List {
+ new Fido2CredentialView {
+ CredentialId = credentialId ?? Guid.NewGuid().ToString(),
+ RpId = rpId ?? "bitwarden.com",
+ Discoverable = discoverable.HasValue ? discoverable.ToString() : "true"
+ }
+ }
+ }
+ };
+ }
}
}