PM-7553 Fix native apps passkeys autofill and creation
This commit is contained in:
parent
1bfe894181
commit
aaa45b18a4
|
@ -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)
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 user’s consent.
|
/// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue