using ProtonVPN.OperatingSystems.WebAuthn.Enums;
using ProtonVPN.OperatingSystems.WebAuthn.FIDO;
using ProtonVPN.OperatingSystems.WebAuthn.Interop;
using ProtonVPN.OperatingSystems.WebAuthn.Interop.Enums;
using ProtonVPN.OperatingSystems.WebAuthn.Interop.Marshalers;
using ProtonVPN.OperatingSystems.WebAuthn.Interop.Structs;
using ProtonVPN.OperatingSystems.WebAuthn.Interop.Structs.GetAssertion;
namespace ProtonVPN.OperatingSystems.WebAuthn;
///
/// Windows WebAuthn API
///
///
/// Requires Windows 10 1903+ to work.
///
public partial class WebAuthnApi
{
private static ApiVersion? _apiVersionCache;
private Guid? _cancellationId;
///
/// Gets the API version information.
///
///
/// Indicates the presence of APIs and features.
///
public static ApiVersion? ApiVersion
{
get
{
try
{
return NativeMethods.GetApiVersionNumber();
}
catch (TypeLoadException)
{
// The WebAuthNGetApiVersionNumber() function was added in Windows 10 1903.
return null;
}
}
}
///
/// Indicates the availability of the WebAuthn API.
///
public static bool IsAvailable => ApiVersion >= Enums.ApiVersion.Version1;
///
/// Indicates the availability of the Credential Protection extension.
///
///
/// Support for the credProtect extension was added in V2 API.
///
public static bool IsCredProtectExtensionSupported => ApiVersion >= Enums.ApiVersion.Version2;
///
/// Indicates the availability of enterprise attestation.
///
///
/// Support for the enterprise attestation was added in V3 API.
///
public static bool IsEnterpriseAttestationSupported => ApiVersion >= Enums.ApiVersion.Version3;
///
/// Indicates the availability of the Credential Blob extension.
///
///
/// Support for the credBlob extension was added in V3 API.
///
public static bool IsCredBlobSupported => ApiVersion >= Enums.ApiVersion.Version3;
///
/// Indicates the availability of the large blobs.
///
///
/// Support for the large blobs was added in V5 API.
///
public static bool IsLargeBlobSupported => ApiVersion >= Enums.ApiVersion.Version5;
///
/// Indicates the API can differentiate between browser modes.
///
///
/// Support for the browser mode indicator was added in V5 API.
///
public static bool IsPrivateBrowserModeIndicatorSupported => ApiVersion >= Enums.ApiVersion.Version5;
///
/// Indicates the availability of the API for platform credential management.
///
///
/// Support for platform credential management was added in V4 API.
///
public static bool IsPlatformCredentialManagementSupported => ApiVersion >= Enums.ApiVersion.Version4;
///
/// Indicates the availability of the minimum PIN length extension.
///
///
/// Support for the minPinLength extension was added in V3 API.
///
public static bool IsMinPinLengthSupported => ApiVersion >= Enums.ApiVersion.Version3;
///
/// Indicates the availability of the psuedo-random function (PRF) extension.
///
///
/// Support for the prf extension was added in V6 API.
///
public static bool IsPseudoRandomFunctionSupported => ApiVersion >= Enums.ApiVersion.Version6;
///
/// Indicates whether operation cancellation is supported by the API.
///
public bool IsCancellationSupported => _cancellationId.HasValue;
///
/// Indicates the support for unsigned extension outputs.
///
///
/// Support for the unsigned extension outputs was added in V7 API.
///
public static bool IsUnsignedExtensionOutputSupported => ApiVersion >= Enums.ApiVersion.Version7;
///
/// Indicates the support for linked device data.
///
///
/// Support for linked device data was added in V7 API.
///
public static bool IsHybridStorageLinkedDataSupported => ApiVersion >= Enums.ApiVersion.Version7;
///
/// Indicates the availability of user-verifying platform authenticator (e.g. Windows Hello).
///
public static bool IsUserVerifyingPlatformAuthenticatorAvailable
{
get
{
try
{
HResult result = NativeMethods.IsUserVerifyingPlatformAuthenticatorAvailable(out bool value);
ApiHelper.Validate(result);
return value;
}
catch (TypeLoadException)
{
// If the IsUserVerifyingPlatformAuthenticatorAvailable function cannot be found, the feature is definitely not supported.
return false;
}
}
}
///
/// Initializes a new instance of the class.
///
public WebAuthnApi()
{
if (!IsAvailable)
{
throw new NotSupportedException("The WebAuthN API is not supported on this OS.");
}
_cancellationId = GetCancellationId();
}
///
/// Produces an assertion signature representing an assertion by the authenticator that the user has consented to a specific transaction, such as logging in or completing a purchase.
///
public AuthenticatorAssertionResponse AuthenticatorGetAssertion(
string rpId,
byte[] challenge,
UserVerificationRequirement userVerificationRequirement,
AuthenticatorAttachment authenticatorAttachment = AuthenticatorAttachment.Any,
int timeoutMilliseconds = ApiConstants.DefaultTimeoutMilliseconds,
IReadOnlyList allowCredentials = null,
AuthenticationExtensionsClientInputs extensions = null,
CredentialLargeBlobOperation largeBlobOperation = CredentialLargeBlobOperation.None,
byte[] largeBlob = null,
bool browserInPrivateMode = false,
HybridStorageLinkedData linkedDevice = null,
WindowHandle windowHandle = default
)
{
if (rpId == null)
{
throw new ArgumentNullException(nameof(rpId));
}
if (challenge == null)
{
throw new ArgumentNullException(nameof(challenge));
}
// TODO: Handle U2F attachment
// Add "https://" to RpId if missing
UriBuilder origin = new(rpId)
{
Scheme = Uri.UriSchemeHttps
};
CollectedClientData clientData = new()
{
Type = ApiConstants.ClientDataCredentialGet,
Challenge = challenge,
Origin = origin.Uri.ToString(),
CrossOrigin = false
};
return AuthenticatorGetAssertion(
rpId,
clientData,
userVerificationRequirement,
authenticatorAttachment,
timeoutMilliseconds,
allowCredentials,
extensions,
largeBlobOperation,
largeBlob,
browserInPrivateMode,
linkedDevice,
windowHandle
);
}
///
/// Produces an assertion signature representing an assertion by the authenticator that the user has consented to a specific transaction, such as logging in or completing a purchase.
///
public AuthenticatorAssertionResponse AuthenticatorGetAssertion(
string rpId,
CollectedClientData clientData,
UserVerificationRequirement userVerificationRequirement,
AuthenticatorAttachment authenticatorAttachment = AuthenticatorAttachment.Any,
int timeoutMilliseconds = ApiConstants.DefaultTimeoutMilliseconds,
IReadOnlyList allowCredentials = null,
AuthenticationExtensionsClientInputs extensions = null,
CredentialLargeBlobOperation largeBlobOperation = CredentialLargeBlobOperation.None,
byte[] largeBlob = null,
bool browserInPrivateMode = false,
HybridStorageLinkedData linkedDevice = null,
WindowHandle windowHandle = default
)
{
if (rpId == null)
{
throw new ArgumentNullException(nameof(rpId));
}
if (clientData == null)
{
throw new ArgumentNullException(nameof(clientData));
}
if (extensions?.GetCredentialBlob == true && IsCredBlobSupported == false)
{
// This feature is only supported in API V3.
throw new NotSupportedException("Credential blobs are not supported on this OS.");
}
if ((largeBlobOperation != CredentialLargeBlobOperation.None || largeBlob != null) && IsLargeBlobSupported == false)
{
// This feature is only supported in API V5.
throw new NotSupportedException("Large blobs are not supported on this OS.");
}
if (browserInPrivateMode == true && IsPrivateBrowserModeIndicatorSupported == false)
{
// This feature is only supported in API V5.
throw new NotSupportedException("The browser private mode indicator is not supported on this OS.");
}
if (extensions?.HmacGetSecret != null && IsPseudoRandomFunctionSupported == false)
{
// This feature is only supported in API V6.
throw new NotSupportedException("The PRF extension is not supported on this OS.");
}
if (linkedDevice != null && IsHybridStorageLinkedDataSupported == false)
{
// This feature is only supported in API V7.
throw new NotSupportedException("Hybrid storage linked data is not supported on this OS.");
}
if (!windowHandle.IsValid)
{
windowHandle = WindowHandle.ForegroundWindow;
}
using (DisposableList allowCreds = new())
using (DisposableList allowCredsEx = new())
{
if (allowCredentials != null)
{
allowCreds.AddRange(allowCredentials.Select(credential =>
new CredentialIn(credential.Id, credential.Type)));
allowCredsEx.AddRange(allowCredentials.Select(credential =>
new CredentialEx(credential.Id, credential.Type, credential.Transports)));
}
using (Credentials allowCredList = new(allowCreds.ToArray()))
using (CredentialList allowCredListEx = new(allowCredsEx.ToArray()))
using (ClientData clientDataNative = new(clientData))
using (HmacSecretSaltIn globalHmacSalt = ApiHelper.Translate(extensions?.HmacGetSecret))
using (HmacSecretSaltValuesIn hmacSecretSaltValues = new(globalHmacSalt, null))
using (DisposableList extensionsList = ApiHelper.Translate(extensions))
using (ExtensionsIn nativeExtensions = new(extensionsList.ToArray()))
using (AuthenticatorGetAssertionOptions options = new())
{
// Prepare native options
options.TimeoutMilliseconds = timeoutMilliseconds;
options.AuthenticatorAttachment = authenticatorAttachment;
options.UserVerificationRequirement = userVerificationRequirement;
options.AllowCredentials = allowCredList;
options.AllowCredentialsEx = allowCredListEx;
options.U2fAppId = extensions?.AppID;
options.LargeBlobOperation = largeBlobOperation;
options.Extensions = nativeExtensions;
options.LargeBlob = largeBlob;
options.BrowserInPrivateMode = browserInPrivateMode;
options.HmacSecretSaltValues = hmacSecretSaltValues;
options.LinkedDevice = linkedDevice;
options.CancellationId = _cancellationId;
// Perform the Win32 API call
HResult result = NativeMethods.AuthenticatorGetAssertion(
windowHandle,
rpId,
clientDataNative,
options,
out AssertionSafeHandle assertionHandle
);
ApiHelper.Validate(result);
try
{
Assertion assertion = assertionHandle.ToManaged();
AuthenticationExtensionsClientOutputs extensionsOut = new()
{
HmacGetSecret = new HMACGetSecretOutput
{
Output1 = assertion.HmacSecret?.First,
Output2 = assertion.HmacSecret?.Second,
}
};
byte[] credBlob = assertion.Extensions?.CredBlob;
// Wrap the raw results
return new AuthenticatorAssertionResponse()
{
ClientDataJson = clientDataNative.ClientDataRaw,
AuthenticatorData = assertion.AuthenticatorData,
Signature = assertion.Signature,
UserHandle = assertion.UserId,
CredentialId = assertion.Credential.Id,
};
}
finally
{
// Release native buffers.
assertionHandle.Dispose();
}
}
}
}
///
/// Cancels the WebAuthn operation currently in progress.
///
///
/// When this operation is invoked by the client in an authenticator session,
/// it has the effect of terminating any AuthenticatorMakeCredential or AuthenticatorGetAssertion operation
/// currently in progress in that authenticator session.
/// The authenticator stops prompting for, or accepting, any user input related to authorizing the canceled operation. The client ignores any further responses from the authenticator for the canceled operation.
///
public void CancelCurrentOperation()
{
if (_cancellationId.HasValue)
{
HResult result = NativeMethods.CancelCurrentOperation(_cancellationId.Value);
ApiHelper.Validate(result);
}
}
///
/// Gets the cancellation ID for a canceled operation.
///
/// ID of the cancelled operation.
private static Guid? GetCancellationId()
{
try
{
HResult result = NativeMethods.GetCancellationId(out Guid cancellationId);
ApiHelper.Validate(result);
return cancellationId;
}
catch (TypeLoadException)
{
// Async support is not present in earlier versions of Windows 10.
return null;
}
}
}