1
0
mirror of https://github.com/bitwarden/mobile synced 2026-02-09 12:59:56 +00:00

[PM-5731] feat: add support for silent discoverability

This commit is contained in:
Andreas Coroiu
2024-01-30 13:10:09 +01:00
parent f3c64a89eb
commit 6bb724ff06
5 changed files with 157 additions and 3 deletions

View File

@@ -6,5 +6,7 @@ namespace Bit.Core.Abstractions
{
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams);
// TODO: Should this return a List? Or maybe IEnumerable?
Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId);
}
}

View File

@@ -194,6 +194,19 @@ namespace Bit.Core.Services
}
}
public async Task<Fido2AuthenticatorDiscoverableCredentialMetadata[]> SilentCredentialDiscoveryAsync(string rpId)
{
var credentials = (await FindCredentialsByRpAsync(rpId)).Select(cipher => new Fido2AuthenticatorDiscoverableCredentialMetadata {
Type = "public-key",
Id = GuidToRawFormat(cipher.Login.MainFido2Credential.CredentialId),
RpId = cipher.Login.MainFido2Credential.RpId,
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
UserName = cipher.Login.MainFido2Credential.UserName
}).ToArray();
return credentials;
}
///<summary>
/// Finds existing crendetials and returns the `CipherId` for each one
///</summary>

View File

@@ -0,0 +1,16 @@
/// <summary>
/// Represents the metadata of a discoverable credential for a FIDO2 authenticator.
/// See: https://www.w3.org/TR/webauthn-3/#sctn-op-silent-discovery
/// </summary>
public class Fido2AuthenticatorDiscoverableCredentialMetadata
{
public string Type { get; set; }
public byte[] Id { get; set; }
public string RpId { get; set; }
public byte[] UserHandle { get; set; }
public string UserName { get; set; }
}

View File

@@ -1,9 +1,9 @@
namespace Bit.Core.Utilities.Fido2
{
public class PublicKeyCredentialDescriptor {
public byte[] Id {get; set;}
public string[] Transports;
public string Type;
public byte[] Id { get; set; }
public string[] Transports { get; set; }
public string Type { get; set; }
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Models.View;
using Bit.Core.Enums;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics.CodeAnalysis;
namespace Bit.Core.Test.Services
{
public class Fido2AuthenticatorSilentCredentialDiscoveryTests
{
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_NoCredentialsExist(SutProvider<Fido2AuthenticatorService> sutProvider)
{
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([]);
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
Assert.Empty(result);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_OnlyNonDiscoverableCredentialsExist(SutProvider<Fido2AuthenticatorService> sutProvider)
{
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
CreateCipherView("bitwarden.com", false),
CreateCipherView("bitwarden.com", false),
CreateCipherView("bitwarden.com", false)
]);
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
Assert.Empty(result);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_NoCredentialsWithMatchingRpIdExist(SutProvider<Fido2AuthenticatorService> sutProvider)
{
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
CreateCipherView("a.bitwarden.com", true),
CreateCipherView("example.com", true)
]);
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
Assert.Empty(result);
}
[Theory]
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
public async Task SilentCredentialDiscoveryAsync_ReturnsCredentials_DiscoverableCredentialsWithMatchingRpIdExist(SutProvider<Fido2AuthenticatorService> sutProvider)
{
var matchingCredentials = new List<CipherView> {
CreateCipherView("bitwarden.com", true),
CreateCipherView("bitwarden.com", true)
};
var nonMatchingCredentials = new List<CipherView> {
CreateCipherView("example.com", true)
};
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(
matchingCredentials.Concat(nonMatchingCredentials).ToList()
);
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
Assert.True(
result.SequenceEqual(matchingCredentials.Select(c => new Fido2AuthenticatorDiscoverableCredentialMetadata {
Type = "public-key",
Id = Guid.Parse(c.Login.MainFido2Credential.CredentialId).ToByteArray(),
RpId = "bitwarden.com",
UserHandle = c.Login.MainFido2Credential.UserHandleValue,
UserName = c.Login.MainFido2Credential.UserName
}), new MetadataComparer())
);
}
private byte[] RandomBytes(int length)
{
var bytes = new byte[length];
new Random().NextBytes(bytes);
return bytes;
}
#nullable enable
private CipherView CreateCipherView(string rpId, bool discoverable)
{
return new CipherView {
Type = CipherType.Login,
Id = Guid.NewGuid().ToString(),
Reprompt = CipherRepromptType.None,
Login = new LoginView {
Fido2Credentials = new List<Fido2CredentialView> {
new Fido2CredentialView {
CredentialId = Guid.NewGuid().ToString(),
RpId = rpId ?? "null.com",
DiscoverableValue = discoverable,
UserHandleValue = RandomBytes(32),
KeyValue = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgO4wC7AlY4eJP7uedRUJGYsAIJAd6gN1Vp7uJh6xXAp6hRANCAARGvr56F_t27DEG1Tzl-qJRhrTUtC7jOEbasAEEZcE3TiMqoWCan0sxKDPylhRYk-1qyrBC_feN1UtGWH57sROa"
}
}
}
};
}
private class MetadataComparer : IEqualityComparer<Fido2AuthenticatorDiscoverableCredentialMetadata>
{
public int GetHashCode([DisallowNull] Fido2AuthenticatorDiscoverableCredentialMetadata obj) => throw new NotImplementedException();
public bool Equals(Fido2AuthenticatorDiscoverableCredentialMetadata? a, Fido2AuthenticatorDiscoverableCredentialMetadata? b) =>
a != null && b != null && a.Type == b.Type && a.RpId == b.RpId && a.UserName == b.UserName && a.Id.SequenceEqual(b.Id) && a.UserHandle.SequenceEqual(b.UserHandle);
}
}
}