PM-7553 Fix native apps passkeys autofill and creation

This commit is contained in:
Federico Maccaroni 2024-04-24 14:09:16 -03:00
parent 1bfe894181
commit aaa45b18a4
No known key found for this signature in database
GPG Key ID: 5D233F8F2B034536
9 changed files with 211 additions and 78 deletions

View File

@ -7,9 +7,11 @@ using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.Provider; using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn; using AndroidX.Credentials.WebAuthn;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.App.Droid.Utilities;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
using Bit.Core.Utilities.Fido2.Extensions; using Bit.Core.Utilities.Fido2.Extensions;
using Bit.Droid; using Bit.Droid;
using Org.Json; using Org.Json;
@ -65,7 +67,7 @@ namespace Bit.App.Platforms.Android.Autofill
var request = new PublicKeyCredentialCreationOptions(json); var request = new PublicKeyCredentialCreationOptions(json);
var jsonObj = new JSONObject(json); var jsonObj = new JSONObject(json);
var authenticatorSelection = jsonObj.GetJSONObject("authenticatorSelection"); var authenticatorSelection = jsonObj.GetJSONObject("authenticatorSelection");
request.AuthenticatorSelection = new AuthenticatorSelectionCriteria( request.AuthenticatorSelection = new AndroidX.Credentials.WebAuthn.AuthenticatorSelectionCriteria(
authenticatorSelection.OptString("authenticatorAttachment", "platform"), authenticatorSelection.OptString("authenticatorAttachment", "platform"),
authenticatorSelection.OptString("residentKey", null), authenticatorSelection.OptString("residentKey", null),
authenticatorSelection.OptBoolean("requireResidentKey", false), authenticatorSelection.OptBoolean("requireResidentKey", false),
@ -77,14 +79,23 @@ namespace Bit.App.Platforms.Android.Autofill
public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity) public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
{ {
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest; var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
if (callingRequest is null)
{
if (ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService))
{
await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, string.Empty, AppResources.Ok);
}
FailAndFinish();
return;
}
var origin = callingRequest.Origin; var origin = callingRequest.Origin;
var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson); var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson);
if (origin is null if (origin is null)
&&
ServiceContainer.TryResolve<IDeviceActionService>(out var deviceActionService))
{ {
await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok); origin = getRequest.CallingAppInfo?.GetAndroidOrigin();
} }
var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity() var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
@ -121,7 +132,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 Fido2ClientCreateCredentialParams()
{ {
Challenge = credentialCreationOptions.GetChallenge(), Challenge = credentialCreationOptions.GetChallenge(),
Origin = origin, Origin = origin,
@ -136,14 +147,17 @@ namespace Bit.App.Platforms.Android.Autofill
SameOriginWithAncestors = true SameOriginWithAncestors = true
}; };
var credentialExtraCreateParams = new Fido2ExtraCreateCredentialParams
(
callingRequest.GetClientDataHash(),
getRequest.CallingAppInfo?.PackageName
);
var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>(); var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams); var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams, credentialExtraCreateParams);
if (clientCreateCredentialResult == null) if (clientCreateCredentialResult == null)
{ {
var resultErrorIntent = new Intent(); FailAndFinish();
PendingIntentHandler.SetCreateCredentialException(resultErrorIntent, new CreateCredentialUnknownException());
activity.SetResult(Result.Ok, resultErrorIntent);
activity.Finish();
return; return;
} }
@ -157,7 +171,10 @@ namespace Bit.App.Platforms.Android.Autofill
} }
var responseInnerAndroidJson = new JSONObject(); var responseInnerAndroidJson = new JSONObject();
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON)); if (clientCreateCredentialResult.ClientDataJSON != null)
{
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
}
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData)); responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject)); responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
responseInnerAndroidJson.Put("transports", transportsArray); responseInnerAndroidJson.Put("transports", transportsArray);
@ -172,16 +189,21 @@ namespace Bit.App.Platforms.Android.Autofill
rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions)); rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions));
rootAndroidJson.Put("response", responseInnerAndroidJson); rootAndroidJson.Put("response", responseInnerAndroidJson);
var responseAndroidJson = rootAndroidJson.ToString();
System.Diagnostics.Debug.WriteLine(responseAndroidJson);
var result = new Intent(); var result = new Intent();
var publicKeyResponse = new CreatePublicKeyCredentialResponse(responseAndroidJson); var publicKeyResponse = new CreatePublicKeyCredentialResponse(rootAndroidJson.ToString());
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse); PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
activity.SetResult(Result.Ok, result); activity.SetResult(Result.Ok, result);
activity.Finish(); activity.Finish();
void FailAndFinish()
{
var result = new Intent();
PendingIntentHandler.SetCreateCredentialException(result, new CreateCredentialUnknownException());
activity.SetResult(Result.Ok, result);
activity.Finish();
}
} }
private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options) private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options)

View File

@ -5,15 +5,16 @@ using Android.OS;
using AndroidX.Credentials; using AndroidX.Credentials;
using AndroidX.Credentials.Provider; using AndroidX.Credentials.Provider;
using AndroidX.Credentials.WebAuthn; using AndroidX.Credentials.WebAuthn;
using Bit.App.Droid.Utilities;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.App.Droid.Utilities;
using Bit.Core.Resources.Localization; using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
using Java.Security;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.App.Platforms.Android.Autofill; using Bit.App.Platforms.Android.Autofill;
using AndroidX.Credentials.Exceptions;
using Org.Json;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
@ -58,7 +59,13 @@ namespace Bit.Droid.Autofill
{ {
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent); var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
var credentialOption = getRequest?.CredentialOptions.FirstOrDefault(); if (getRequest is null)
{
FailAndFinish();
return;
}
var credentialOption = getRequest.CredentialOptions.FirstOrDefault();
var credentialPublic = credentialOption as GetPublicKeyCredentialOption; var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson); var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
@ -68,14 +75,14 @@ namespace Bit.Droid.Autofill
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra); var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false); var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo); var androidOrigin = getRequest.CallingAppInfo.GetAndroidOrigin();
var packageName = getRequest?.CallingAppInfo.PackageName; var packageName = getRequest.CallingAppInfo.PackageName;
var appInfoOrigin = getRequest?.CallingAppInfo.Origin; var origin = getRequest.CallingAppInfo.Origin ?? androidOrigin;
if (appInfoOrigin is null) if (origin is null)
{ {
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
Finish(); FailAndFinish();
return; return;
} }
@ -91,31 +98,41 @@ namespace Bit.Droid.Autofill
Challenge = requestOptions.GetChallenge(), Challenge = requestOptions.GetChallenge(),
RpId = RpId, RpId = RpId,
AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } }, AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
Origin = appInfoOrigin, Origin = origin,
SameOriginWithAncestors = true, SameOriginWithAncestors = true,
UserVerification = requestOptions.UserVerification UserVerification = requestOptions.UserVerification
}; };
var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, credentialPublic.GetClientDataHash()); var extraAssertParams = new Fido2ExtraAssertCredentialParams
(
var response = new AuthenticatorAssertionResponse( getRequest.CallingAppInfo.Origin != null ? credentialPublic.GetClientDataHash() : null,
requestOptions, packageName
assertResult.SelectedCredential.Id,
androidOrigin,
false, // These flags have no effect, we set our own within `SetAuthenticatorData`
false,
false,
false,
assertResult.SelectedCredential.UserHandle,
packageName,
assertResult.ClientDataHash
); );
response.SetAuthenticatorData(assertResult.AuthenticatorData);
response.SetSignature(assertResult.Signature); var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, extraAssertParams);
var result = new Intent(); var result = new Intent();
var fidoCredential = new FidoPublicKeyCredential(assertResult.SelectedCredential.Id, response, "platform");
var cred = new PublicKeyCredential(fidoCredential.Json()); var responseInnerAndroidJson = new JSONObject();
if (assertResult.ClientDataJSON != null)
{
responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(assertResult.ClientDataJSON));
}
responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(assertResult.AuthenticatorData));
responseInnerAndroidJson.Put("signature", CoreHelpers.Base64UrlEncode(assertResult.Signature));
responseInnerAndroidJson.Put("userHandle", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.UserHandle));
var rootAndroidJson = new JSONObject();
rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
rootAndroidJson.Put("authenticatorAttachment", "platform");
rootAndroidJson.Put("type", "public-key");
rootAndroidJson.Put("clientExtensionResults", new JSONObject());
rootAndroidJson.Put("response", responseInnerAndroidJson);
var json = rootAndroidJson.ToString();
var cred = new PublicKeyCredential(json);
var credResponse = new GetCredentialResponse(cred); var credResponse = new GetCredentialResponse(cred);
PendingIntentHandler.SetGetCredentialResponse(result, credResponse); PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
@ -130,7 +147,7 @@ namespace Bit.Droid.Autofill
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(); FailAndFinish();
}); });
} }
catch (Exception ex) catch (Exception ex)
@ -139,17 +156,18 @@ namespace Bit.Droid.Autofill
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(); FailAndFinish();
}); });
} }
} }
private string AppInfoToOrigin(CallingAppInfo info) private void FailAndFinish()
{ {
var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray(); var result = new Intent();
var md = MessageDigest.GetInstance("SHA-256"); PendingIntentHandler.SetGetCredentialException(result, new GetCredentialUnknownException());
var certHash = md.Digest(cert);
return $"android:apk-key-hash:${CoreHelpers.Base64UrlEncode(certHash)}"; SetResult(Result.Ok, result);
Finish();
} }
} }
} }

View File

@ -0,0 +1,23 @@
using Android.OS;
using AndroidX.Credentials.Provider;
using Bit.Core.Utilities;
using Java.Security;
namespace Bit.App.Droid.Utilities
{
public static class CallingAppInfoExtensions
{
public static string GetAndroidOrigin(this CallingAppInfo callingAppInfo)
{
if (Build.VERSION.SdkInt < BuildVersionCodes.P || callingAppInfo?.SigningInfo?.GetApkContentsSigners().Any() != true)
{
return null;
}
var cert = callingAppInfo.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
var md = MessageDigest.GetInstance("SHA-256");
var certHash = md.Digest(cert);
return $"android:apk-key-hash:{CoreHelpers.Base64UrlEncode(certHash)}";
}
}
}

View File

@ -20,8 +20,9 @@ namespace Bit.Core.Abstractions
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
/// </summary> /// </summary>
/// <param name="createCredentialParams">The parameters for the credential creation operation</param> /// <param name="createCredentialParams">The parameters for the credential creation operation</param>
/// <param name="extraParams">Extra parameters for the credential creation operation</param>
/// <returns>The new credential</returns> /// <returns>The new credential</returns>
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash); Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
/// <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.
@ -29,7 +30,8 @@ namespace Bit.Core.Abstractions
/// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
/// </summary> /// </summary>
/// <param name="assertCredentialParams">The parameters for the credential assertion operation</param> /// <param name="assertCredentialParams">The parameters for the credential assertion operation</param>
/// <param name="extraParams">Extra parameters for the credential assertion operation</param>
/// <returns>The asserted credential</returns> /// <returns>The asserted credential</returns>
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash); Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
} }
} }

View File

@ -4,8 +4,8 @@ namespace Bit.Core.Abstractions
{ {
public interface IFido2MediatorService public interface IFido2MediatorService
{ {
Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null); Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null); Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
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

@ -1,5 +1,6 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -33,7 +34,7 @@ namespace Bit.Core.Services
_makeCredentialUserInterface = makeCredentialUserInterface; _makeCredentialUserInterface = makeCredentialUserInterface;
} }
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, byte[] clientDataHash = null) public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams)
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin); var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
@ -72,14 +73,15 @@ namespace Bit.Core.Services
"The length of user.id is not between 1 and 64 bytes (inclusive)"); "The length of user.id is not between 1 and 64 bytes (inclusive)");
} }
if (!createCredentialParams.Origin.StartsWith("https://")) var isAndroidOrigin = createCredentialParams.Origin.StartsWith("android:apk-key-hash");
if (!isAndroidOrigin && !createCredentialParams.Origin.StartsWith("https://"))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError, Fido2ClientException.ErrorCode.SecurityError,
"Origin is not a valid https origin"); "Origin is not a valid https origin");
} }
if (!Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin)) if (!isAndroidOrigin && !Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError, Fido2ClientException.ErrorCode.SecurityError,
@ -110,17 +112,23 @@ namespace Bit.Core.Services
} }
byte[] clientDataJSONBytes = null; byte[] clientDataJSONBytes = null;
var clientDataHash = extraParams.ClientDataHash;
if (clientDataHash == null) if (clientDataHash == null)
{ {
var clientDataJSON = JsonSerializer.Serialize(new var clientDataJsonObject = new JsonObject
{ {
type = "webauthn.create", { "type", "webauthn.create" },
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge), { "challenge", CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge) },
origin = createCredentialParams.Origin, { "origin", createCredentialParams.Origin },
crossOrigin = !createCredentialParams.SameOriginWithAncestors, { "crossOrigin", !createCredentialParams.SameOriginWithAncestors }
// tokenBinding: {} // Not supported // tokenBinding: {} // Not supported
}); };
clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); if (!string.IsNullOrWhiteSpace(extraParams.AndroidPackageName))
{
clientDataJsonObject.Add("androidPackageName", extraParams.AndroidPackageName);
}
clientDataJSONBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(clientDataJsonObject));
clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
} }
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash); var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
@ -163,7 +171,7 @@ namespace Bit.Core.Services
} }
} }
public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, byte[] clientDataHash = null) public async Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams)
{ {
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync(); var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin); var domain = CoreHelpers.GetHostname(assertCredentialParams.Origin);
@ -188,14 +196,15 @@ namespace Bit.Core.Services
"Saving Bitwarden credentials in a Bitwarden vault is not allowed"); "Saving Bitwarden credentials in a Bitwarden vault is not allowed");
} }
if (!assertCredentialParams.Origin.StartsWith("https://")) var isAndroidOrigin = assertCredentialParams.Origin.StartsWith("android:apk-key-hash");
if (!isAndroidOrigin && !assertCredentialParams.Origin.StartsWith("https://"))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError, Fido2ClientException.ErrorCode.SecurityError,
"Origin is not a valid https origin"); "Origin is not a valid https origin");
} }
if (!Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin)) if (!isAndroidOrigin && !Fido2DomainUtils.IsValidRpId(assertCredentialParams.RpId, assertCredentialParams.Origin))
{ {
throw new Fido2ClientException( throw new Fido2ClientException(
Fido2ClientException.ErrorCode.SecurityError, Fido2ClientException.ErrorCode.SecurityError,
@ -203,16 +212,23 @@ namespace Bit.Core.Services
} }
byte[] clientDataJSONBytes = null; byte[] clientDataJSONBytes = null;
var clientDataHash = extraParams.ClientDataHash;
if (clientDataHash == null) if (clientDataHash == null)
{ {
var clientDataJSON = JsonSerializer.Serialize(new var clientDataJsonObject = new JsonObject
{ {
type = "webauthn.get", { "type", "webauthn.get" },
challenge = CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge), { "challenge", CoreHelpers.Base64UrlEncode(assertCredentialParams.Challenge) },
origin = assertCredentialParams.Origin, { "origin", assertCredentialParams.Origin },
crossOrigin = !assertCredentialParams.SameOriginWithAncestors, { "crossOrigin", !assertCredentialParams.SameOriginWithAncestors }
}); // tokenBinding: {} // Not supported
clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON); };
if (!string.IsNullOrWhiteSpace(extraParams.AndroidPackageName))
{
clientDataJsonObject.Add("androidPackageName", extraParams.AndroidPackageName);
}
clientDataJSONBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(clientDataJsonObject));
clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256); clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
} }
var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash); var getAssertionParams = MapToGetAssertionParams(assertCredentialParams, clientDataHash);

View File

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

View File

@ -0,0 +1,26 @@
namespace Bit.Core.Utilities.Fido2
{
#nullable enable
/// <summary>
/// Extra parameters for asserting a credential.
/// </summary>
public class Fido2ExtraAssertCredentialParams
{
public Fido2ExtraAssertCredentialParams(byte[]? clientDataHash = null, string? androidPackageName = null)
{
ClientDataHash = clientDataHash;
AndroidPackageName = androidPackageName;
}
/// <summary>
/// The hash of the serialized client data.
/// </summary>
public byte[]? ClientDataHash { get; }
/// <summary>
/// The Android package name.
/// </summary>
public string? AndroidPackageName { get; }
}
}

View File

@ -0,0 +1,26 @@
namespace Bit.Core.Utilities.Fido2
{
#nullable enable
/// <summary>
/// Extra parameters for creating a new credential.
/// </summary>
public struct Fido2ExtraCreateCredentialParams
{
public Fido2ExtraCreateCredentialParams(byte[]? clientDataHash = null, string? androidPackageName = null)
{
ClientDataHash = clientDataHash;
AndroidPackageName = androidPackageName;
}
/// <summary>
/// The hash of the serialized client data.
/// </summary>
public byte[]? ClientDataHash { get; }
/// <summary>
/// The Android package name.
/// </summary>
public string? AndroidPackageName { get; }
}
}