mirror of
https://github.com/bitwarden/mobile.git
synced 2024-09-27 03:52:57 +02:00
[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:
parent
76e0f7e1a4
commit
c1522e249d
@ -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()
|
||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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 user’s consent.
|
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
src/Core/Utilities/Fido2/Fido2SelectedCredential.cs
Normal file
10
src/Core/Utilities/Fido2/Fido2SelectedCredential.cs
Normal 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; }
|
||||||
|
}
|
@ -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,6 +42,17 @@ 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);
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
@ -207,10 +256,10 @@ namespace Bit.Core.Test.Services
|
|||||||
_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>());
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user