[PM-7257] android add support for web authn resident key credential property in our net mobile app 2 (#3170)

* [PM-7257] feat: add ability to override `clientDataHash`

* [PM-7257] feat: add support for clientDataHash and extensions

* PM-7257 Updated the origin to be the correct one and not the android one to be passed to the Fido2Client

---------

Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
Andreas Coroiu 2024-04-19 15:52:19 +02:00 committed by GitHub
parent 76e0f7e1a4
commit c1522e249d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 217 additions and 82 deletions

View File

@ -7,6 +7,7 @@ using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn; using AndroidX.Credentials.WebAuthn;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2.Extensions;
using Bit.Droid; using Bit.Droid;
using Org.Json; using Org.Json;
using Activity = Android.App.Activity; using Activity = Android.App.Activity;
@ -84,7 +85,7 @@ namespace Bit.App.Platforms.Android.Autofill
var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>(); var excludeCredentials = new List<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials) foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
{ {
excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor(){ Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() }); excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor() { Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
} }
var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria() var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
@ -95,7 +96,7 @@ namespace Bit.App.Platforms.Android.Autofill
}; };
var timeout = Convert.ToInt32(credentialCreationOptions.Timeout); var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);
var credentialCreateParams = new Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams() var credentialCreateParams = new Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams()
{ {
Challenge = credentialCreationOptions.GetChallenge(), Challenge = credentialCreationOptions.GetChallenge(),
@ -107,7 +108,7 @@ namespace Bit.App.Platforms.Android.Autofill
Attestation = credentialCreationOptions.Attestation, Attestation = credentialCreationOptions.Attestation,
AuthenticatorSelection = authenticatorSelection, AuthenticatorSelection = authenticatorSelection,
ExcludeCredentials = excludeCredentials.ToArray(), ExcludeCredentials = excludeCredentials.ToArray(),
//Extensions = // Can be improved later to add support for 'credProps' Extensions = MapExtensionsFromJson(credentialCreationOptions),
SameOriginWithAncestors = true SameOriginWithAncestors = true
}; };
@ -121,7 +122,7 @@ namespace Bit.App.Platforms.Android.Autofill
activity.Finish(); activity.Finish();
return; return;
} }
var transportsArray = new JSONArray(); var transportsArray = new JSONArray();
if (clientCreateCredentialResult.Transports != null) if (clientCreateCredentialResult.Transports != null)
{ {
@ -130,7 +131,7 @@ namespace Bit.App.Platforms.Android.Autofill
transportsArray.Put(transport); transportsArray.Put(transport);
} }
} }
var responseInnerAndroidJson = new JSONObject(); var responseInnerAndroidJson = new JSONObject();
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON)); responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData)); responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
@ -144,7 +145,7 @@ namespace Bit.App.Platforms.Android.Autofill
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId)); rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
rootAndroidJson.Put("authenticatorAttachment", "platform"); rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key"); rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", new JSONObject()); rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions));
rootAndroidJson.Put("response", responseInnerAndroidJson); rootAndroidJson.Put("response", responseInnerAndroidJson);
var responseAndroidJson = rootAndroidJson.ToString(); var responseAndroidJson = rootAndroidJson.ToString();
@ -158,5 +159,37 @@ namespace Bit.App.Platforms.Android.Autofill
activity.SetResult(Result.Ok, result); activity.SetResult(Result.Ok, result);
activity.Finish(); activity.Finish();
} }
private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options)
{
if (options == null || !options.Json.Has("extensions"))
{
return null;
}
var extensions = options.Json.GetJSONObject("extensions");
return new Fido2CreateCredentialExtensionsParams
{
CredProps = extensions.Has("credProps") && extensions.GetBoolean("credProps")
};
}
private static JSONObject MapExtensionsToJson(Fido2CreateCredentialExtensionsResult extensions)
{
if (extensions == null)
{
return null;
}
var extensionsJson = new JSONObject();
if (extensions.CredProps != null)
{
var credPropsJson = new JSONObject();
credPropsJson.Put("rk", extensions.CredProps.Rk);
extensionsJson.Put("credProps", credPropsJson);
}
return extensionsJson;
}
} }
} }

View File

@ -68,6 +68,7 @@ namespace Bit.Droid.Autofill
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo); var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
var packageName = getRequest?.CallingAppInfo.PackageName; var packageName = getRequest?.CallingAppInfo.PackageName;
var appInfoOrigin = getRequest?.CallingAppInfo.Origin;
var userInterface = new Fido2GetAssertionUserInterface( var userInterface = new Fido2GetAssertionUserInterface(
cipherId: cipherId, cipherId: cipherId,
@ -76,17 +77,17 @@ namespace Bit.Droid.Autofill
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction, hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction,
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction)); verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction));
var assertParams = new Fido2AuthenticatorGetAssertionParams var clientAssertParams = new Fido2ClientAssertCredentialParams
{ {
Challenge = requestOptions.GetChallenge(), Challenge = requestOptions.GetChallenge(),
RpId = RpId, RpId = RpId,
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification), AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
Hash = credentialPublic.GetClientDataHash(), Origin = appInfoOrigin,
AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } }, SameOriginWithAncestors = true,
Extensions = new object() UserVerification = requestOptions.UserVerification
}; };
var assertResult = await _fido2MediatorService.Value.GetAssertionAsync(assertParams, userInterface); var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, credentialPublic.GetClientDataHash());
var response = new AuthenticatorAssertionResponse( var response = new AuthenticatorAssertionResponse(
requestOptions, requestOptions,
@ -98,7 +99,7 @@ namespace Bit.Droid.Autofill
false, false,
assertResult.SelectedCredential.UserHandle, assertResult.SelectedCredential.UserHandle,
packageName, packageName,
credentialPublic.GetClientDataHash() //clientDataHash assertResult.ClientDataHash
); );
response.SetAuthenticatorData(assertResult.AuthenticatorData); response.SetAuthenticatorData(assertResult.AuthenticatorData);
response.SetSignature(assertResult.Signature); response.SetSignature(assertResult.Signature);
@ -117,7 +118,7 @@ namespace Bit.Droid.Autofill
} }
catch (NotAllowedError) catch (NotAllowedError)
{ {
await MainThread.InvokeOnMainThreadAsync(async() => await MainThread.InvokeOnMainThreadAsync(async () =>
{ {
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
Finish(); Finish();
@ -126,7 +127,7 @@ namespace Bit.Droid.Autofill
catch (Exception ex) catch (Exception ex)
{ {
LoggerHelper.LogEvenIfCantBeResolved(ex); LoggerHelper.LogEvenIfCantBeResolved(ex);
await MainThread.InvokeOnMainThreadAsync(async() => await MainThread.InvokeOnMainThreadAsync(async () =>
{ {
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
Finish(); Finish();

View File

@ -21,7 +21,7 @@ namespace Bit.Core.Abstractions
/// </summary> /// </summary>
/// <param name="createCredentialParams">The parameters for the credential creation operation</param> /// <param name="createCredentialParams">The parameters for the credential creation operation</param>
/// <returns>The new credential</returns> /// <returns>The new credential</returns>
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams); Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash);
/// <summary> /// <summary>
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent. /// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the users consent.
@ -30,6 +30,6 @@ namespace Bit.Core.Abstractions
/// </summary> /// </summary>
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param> /// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
/// <returns>The asserted credential</returns> /// <returns>The asserted credential</returns>
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams); Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash);
} }
} }

View File

@ -4,8 +4,8 @@ namespace Bit.Core.Abstractions
{ {
public interface IFido2MediatorService public interface IFido2MediatorService
{ {
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams); Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null);
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams); Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null);
Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface); Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface); Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);

View File

@ -184,7 +184,7 @@ namespace Bit.Core.Services
{ {
throw new NotAllowedError(); throw new NotAllowedError();
} }
if (!userVerified if (!userVerified
&& &&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions( await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
@ -224,7 +224,7 @@ namespace Bit.Core.Services
return new Fido2AuthenticatorGetAssertionResult return new Fido2AuthenticatorGetAssertionResult
{ {
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential = new Fido2SelectedCredential
{ {
Id = selectedCredentialId.GuidToRawFormat(), Id = selectedCredentialId.GuidToRawFormat(),
UserHandle = selectedFido2Credential.UserHandleValue, UserHandle = selectedFido2Credential.UserHandleValue,

View File

@ -33,7 +33,7 @@ namespace Bit.Core.Services
_makeCredentialUserInterface = makeCredentialUserInterface; _makeCredentialUserInterface = makeCredentialUserInterface;
} }
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null)
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin); var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
@ -109,16 +109,20 @@ namespace Bit.Core.Services
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found"); throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
} }
var clientDataJSON = JsonSerializer.Serialize(new byte[] clientDataJSONBytes = null;
if (clientDataHash == null)
{ {
type = "webauthn.create", var clientDataJSON = JsonSerializer.Serialize(new
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge), {
origin = createCredentialParams.Origin, type = "webauthn.create",
crossOrigin = !createCredentialParams.SameOriginWithAncestors, challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
// tokenBinding: {} // Not supported origin = createCredentialParams.Origin,
}); crossOrigin = !createCredentialParams.SameOriginWithAncestors,
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); // tokenBinding: {} // Not supported
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); });
clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
}
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash); var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
try try
@ -159,7 +163,7 @@ namespace Bit.Core.Services
} }
} }
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null)
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin); var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
@ -198,15 +202,19 @@ namespace Bit.Core.Services
"RP ID cannot be used with this origin"); "RP ID cannot be used with this origin");
} }
var clientDataJSON = JsonSerializer.Serialize(new byte[] clientDataJSONBytes = null;
if (clientDataHash == null)
{ {
type = "webauthn.get", var clientDataJSON = JsonSerializer.Serialize(new
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge), {
origin = assertCredentialParams.Origin, type = "webauthn.get",
crossOrigin = !assertCredentialParams.SameOriginWithAncestors, challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge),
}); origin = assertCredentialParams.Origin,
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); crossOrigin = !assertCredentialParams.SameOriginWithAncestors,
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); });
clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
}
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash); var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);
try try
@ -220,8 +228,8 @@ namespace Bit.Core.Services
Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id), Id = CoreHelpers.Base64UrlEncode(getAssertionResult.SelectedCredential.Id),
RawId = getAssertionResult.SelectedCredential.Id, RawId = getAssertionResult.SelectedCredential.Id,
Signature = getAssertionResult.Signature, Signature = getAssertionResult.Signature,
UserHandle = getAssertionResult.SelectedCredential.UserHandle, SelectedCredential = getAssertionResult.SelectedCredential,
Cipher = getAssertionResult.SelectedCredential.Cipher ClientDataHash = clientDataHash
}; };
} }
catch (InvalidStateError) catch (InvalidStateError)

View File

@ -18,21 +18,21 @@ namespace Bit.Core.Services
_cipherService = cipherService; _cipherService = cipherService;
} }
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null)
{ {
var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams); var result = await _fido2ClientService.AssertCredentialAsync(assertCredentialParams, clientDataHash);
if (result?.Cipher != null) if (result?.SelectedCredential?.Cipher != null)
{ {
await _cipherService.CopyTotpCodeIfNeededAsync(result.Cipher); await _cipherService.CopyTotpCodeIfNeededAsync(result.SelectedCredential.Cipher);
} }
return result; return result;
} }
public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null)
{ {
return _fido2ClientService.CreateCredentialAsync(createCredentialParams); return _fido2ClientService.CreateCredentialAsync(createCredentialParams, clientDataHash);
} }
public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface) public async Task<Fido2AuthenticatorGetAssertionResult> GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface)

View File

@ -8,16 +8,7 @@ namespace Bit.Core.Utilities.Fido2
public byte[] Signature { get; set; } public byte[] Signature { get; set; }
public Fido2AuthenticatorGetAssertionSelectedCredential SelectedCredential { get; set; } public Fido2SelectedCredential SelectedCredential { get; set; }
}
public class Fido2AuthenticatorGetAssertionSelectedCredential {
public byte[] Id { get; set; }
#nullable enable
public byte[]? UserHandle { get; set; }
public CipherView? Cipher { get; set; }
} }
} }

View File

@ -25,6 +25,11 @@ namespace Bit.Core.Utilities.Fido2
/// </summary> /// </summary>
public required byte[] ClientDataJSON { get; set; } public required byte[] ClientDataJSON { get; set; }
/// <summary>
/// The hash of the serialized client data used to generate the assertion.
/// </summary>
public required byte[] ClientDataHash { get; set; }
/// <summary> /// <summary>
/// The authenticator data returned by the authenticator. /// The authenticator data returned by the authenticator.
/// </summary> /// </summary>
@ -36,14 +41,8 @@ namespace Bit.Core.Utilities.Fido2
public required byte[] Signature { get; set; } public required byte[] Signature { get; set; }
/// <summary> /// <summary>
/// The user handle returned from the authenticator, or null if the authenticator did not /// The selected credential that was used to generate the assertion.
/// return a user handle.
/// </summary> /// </summary>
public byte[]? UserHandle { get; set; } public Fido2SelectedCredential SelectedCredential { get; set; }
/// <summary>
/// The selected cipher login item that has the credential
/// </summary>
public CipherView? Cipher { get; set; }
} }
} }

View File

@ -0,0 +1,10 @@
using Bit.Core.Models.View;
public class Fido2SelectedCredential
{
public byte[] Id { get; set; }
public byte[] UserHandle { get; set; }
public CipherView Cipher { get; set; }
}

View File

@ -5,6 +5,7 @@ using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
@ -20,10 +21,12 @@ namespace Bit.Core.Test.Services
private readonly SutProvider<Fido2ClientService> _sutProvider = new SutProvider<Fido2ClientService>().Create(); private readonly SutProvider<Fido2ClientService> _sutProvider = new SutProvider<Fido2ClientService>().Create();
private Fido2ClientAssertCredentialParams _params; private Fido2ClientAssertCredentialParams _params;
private Fido2AuthenticatorGetAssertionResult _authenticatorResult;
public Fido2ClientAssertCredentialTests() public Fido2ClientAssertCredentialTests()
{ {
_params = new Fido2ClientAssertCredentialParams { _params = new Fido2ClientAssertCredentialParams
{
Origin = "https://bitwarden.com", Origin = "https://bitwarden.com",
Challenge = RandomBytes(32), Challenge = RandomBytes(32),
RpId = "bitwarden.com", RpId = "bitwarden.com",
@ -39,11 +42,22 @@ namespace Bit.Core.Test.Services
Timeout = 60000, Timeout = 60000,
}; };
_authenticatorResult = new Fido2AuthenticatorGetAssertionResult
{
AuthenticatorData = RandomBytes(32),
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential
{
Id = RandomBytes(16),
UserHandle = RandomBytes(32)
},
Signature = RandomBytes(32)
};
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>())); _sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>()));
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true); _sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true);
} }
public void Dispose() public void Dispose()
{ {
} }
@ -174,22 +188,57 @@ namespace Bit.Core.Test.Services
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
} }
[Fact]
public async Task AssertCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided()
{
// Arrange
var mockHash = RandomBytes(32);
_sutProvider.GetDependency<ICryptoFunctionService>()
.HashAsync(Arg.Any<byte[]>(), Arg.Is(CryptoHashAlgorithm.Sha256))
.Returns(Task.FromResult(mockHash));
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>())
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.AssertCredentialAsync(_params);
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
);
}
[Fact]
public async Task AssertCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided()
{
// Arrange
var mockHash = RandomBytes(32);
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>())
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.AssertCredentialAsync(_params, mockHash);
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
);
}
[Fact] [Fact]
public async Task AssertCredentialAsync_ReturnsAssertion() public async Task AssertCredentialAsync_ReturnsAssertion()
{ {
// Arrange // Arrange
_params.UserVerification = "required"; _params.UserVerification = "required";
var authenticatorResult = new Fido2AuthenticatorGetAssertionResult {
AuthenticatorData = RandomBytes(32),
SelectedCredential = new Fido2AuthenticatorGetAssertionSelectedCredential {
Id = RandomBytes(16),
UserHandle = RandomBytes(32)
},
Signature = RandomBytes(32)
};
_sutProvider.GetDependency<IFido2AuthenticatorService>() _sutProvider.GetDependency<IFido2AuthenticatorService>()
.GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>()) .GetAssertionAsync(Arg.Any<Fido2AuthenticatorGetAssertionParams>(), _sutProvider.GetDependency<IFido2GetAssertionUserInterface>())
.Returns(authenticatorResult); .Returns(_authenticatorResult);
// Act // Act
var result = await _sutProvider.Sut.AssertCredentialAsync(_params); var result = await _sutProvider.Sut.AssertCredentialAsync(_params);
@ -203,14 +252,14 @@ namespace Bit.Core.Test.Services
x.UserVerificationPreference == Fido2UserVerificationPreference.Required && x.UserVerificationPreference == Fido2UserVerificationPreference.Required &&
x.AllowCredentialDescriptorList.Length == 1 && x.AllowCredentialDescriptorList.Length == 1 &&
x.AllowCredentialDescriptorList[0].Id == _params.AllowCredentials[0].Id x.AllowCredentialDescriptorList[0].Id == _params.AllowCredentials[0].Id
), ),
_sutProvider.GetDependency<IFido2GetAssertionUserInterface>() _sutProvider.GetDependency<IFido2GetAssertionUserInterface>()
); );
Assert.Equal(authenticatorResult.SelectedCredential.Id, result.RawId); Assert.Equal(_authenticatorResult.SelectedCredential.Id, result.RawId);
Assert.Equal(CoreHelpers.Base64UrlEncode(authenticatorResult.SelectedCredential.Id), result.Id); Assert.Equal(CoreHelpers.Base64UrlEncode(_authenticatorResult.SelectedCredential.Id), result.Id);
Assert.Equal(authenticatorResult.AuthenticatorData, result.AuthenticatorData); Assert.Equal(_authenticatorResult.AuthenticatorData, result.AuthenticatorData);
Assert.Equal(authenticatorResult.Signature, result.Signature); Assert.Equal(_authenticatorResult.Signature, result.Signature);
var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON)); var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON));
Assert.Equal("webauthn.get", clientDataJSON["type"].GetValue<string>()); Assert.Equal("webauthn.get", clientDataJSON["type"].GetValue<string>());

View File

@ -5,6 +5,7 @@ using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
@ -320,6 +321,49 @@ namespace Bit.Core.Test.Services
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
} }
[Fact]
public async Task AssertCredentialAsync_ConstructsClientDataHash_WhenHashIsNotProvided()
{
// Arrange
var mockHash = RandomBytes(32);
_sutProvider.GetDependency<ICryptoFunctionService>()
.HashAsync(Arg.Any<byte[]>(), Arg.Is(CryptoHashAlgorithm.Sha256))
.Returns(Task.FromResult(mockHash));
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>(), _sutProvider.GetDependency<IFido2MakeCredentialUserInterface>())
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.CreateCredentialAsync(_params);
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
);
}
[Fact]
public async Task AssertCredentialAsync_UsesProvidedClientDataHash_WhenHashIsProvided()
{
// Arrange
var mockHash = RandomBytes(32);
_sutProvider.GetDependency<IFido2AuthenticatorService>()
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>(), _sutProvider.GetDependency<IFido2MakeCredentialUserInterface>())
.Returns(_authenticatorResult);
// Act
await _sutProvider.Sut.CreateCredentialAsync(_params, mockHash);
// Assert
await _sutProvider.GetDependency<IFido2AuthenticatorService>().Received()
.GetAssertionAsync(
Arg.Is((Fido2AuthenticatorGetAssertionParams x) => x.Hash == mockHash),
Arg.Any<IFido2GetAssertionUserInterface>()
);
}
[Fact] [Fact]
public async Task CreateCredentialAsync_ReturnsCredPropsRkTrue_WhenCreatingDiscoverableCredential() public async Task CreateCredentialAsync_ReturnsCredPropsRkTrue_WhenCreatingDiscoverableCredential()
{ {