summaryrefslogtreecommitdiffstats
path: root/dom/webauthn/WebAuthnManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webauthn/WebAuthnManager.cpp')
-rw-r--r--dom/webauthn/WebAuthnManager.cpp864
1 files changed, 864 insertions, 0 deletions
diff --git a/dom/webauthn/WebAuthnManager.cpp b/dom/webauthn/WebAuthnManager.cpp
new file mode 100644
index 0000000000..9a15f62bb4
--- /dev/null
+++ b/dom/webauthn/WebAuthnManager.cpp
@@ -0,0 +1,864 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "hasht.h"
+#include "nsHTMLDocument.h"
+#include "nsIURIMutator.h"
+#include "nsThreadUtils.h"
+#include "WebAuthnCoseIdentifiers.h"
+#include "WebAuthnEnumStrings.h"
+#include "WebAuthnTransportIdentifiers.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/AuthenticatorAssertionResponse.h"
+#include "mozilla/dom/AuthenticatorAttestationResponse.h"
+#include "mozilla/dom/PublicKeyCredential.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PWebAuthnTransaction.h"
+#include "mozilla/dom/WebAuthnManager.h"
+#include "mozilla/dom/WebAuthnTransactionChild.h"
+#include "mozilla/dom/WebAuthnUtil.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+
+#ifdef OS_WIN
+# include "WinWebAuthnManager.h"
+#endif
+
+using namespace mozilla::ipc;
+
+namespace mozilla::dom {
+
+/***********************************************************************
+ * Statics
+ **********************************************************************/
+
+namespace {
+static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(WebAuthnManager,
+ WebAuthnManagerBase)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(WebAuthnManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WebAuthnManager,
+ WebAuthnManagerBase)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTransaction)
+ tmp->mTransaction.reset();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WebAuthnManager,
+ WebAuthnManagerBase)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTransaction)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+/***********************************************************************
+ * Utility Functions
+ **********************************************************************/
+
+static nsresult AssembleClientData(
+ const nsAString& aOrigin, const CryptoBuffer& aChallenge,
+ const nsAString& aType,
+ const AuthenticationExtensionsClientInputs& aExtensions,
+ /* out */ nsACString& aJsonOut) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsString challengeBase64;
+ nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CollectedClientData clientDataObject;
+ clientDataObject.mType.Assign(aType);
+ clientDataObject.mChallenge.Assign(challengeBase64);
+ clientDataObject.mOrigin.Assign(aOrigin);
+
+ nsAutoString temp;
+ if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
+ return NS_OK;
+}
+
+nsresult GetOrigin(nsPIDOMWindowInner* aParent,
+ /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost) {
+ MOZ_ASSERT(aParent);
+ nsCOMPtr<Document> doc = aParent->GetDoc();
+ MOZ_ASSERT(doc);
+
+ nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+ nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
+ if (NS_WARN_IF(NS_FAILED(rv)) || NS_WARN_IF(aOrigin.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (principal->GetIsIpAddress()) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ if (aOrigin.EqualsLiteral("null")) {
+ // 4.1.1.3 If callerOrigin is an opaque origin, reject promise with a
+ // DOMException whose name is "NotAllowedError", and terminate this
+ // algorithm
+ MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
+ ("Rejecting due to opaque origin"));
+ return NS_ERROR_DOM_NOT_ALLOWED_ERR;
+ }
+
+ nsCOMPtr<nsIURI> originUri;
+ auto* basePrin = BasePrincipal::Cast(principal);
+ if (NS_FAILED(basePrin->GetURI(getter_AddRefs(originUri)))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(originUri->GetAsciiHost(aHost))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult RelaxSameOrigin(nsPIDOMWindowInner* aParent,
+ const nsAString& aInputRpId,
+ /* out */ nsACString& aRelaxedRpId) {
+ MOZ_ASSERT(aParent);
+ nsCOMPtr<Document> doc = aParent->GetDoc();
+ MOZ_ASSERT(doc);
+
+ nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+ auto* basePrin = BasePrincipal::Cast(principal);
+ nsCOMPtr<nsIURI> uri;
+
+ if (NS_FAILED(basePrin->GetURI(getter_AddRefs(uri)))) {
+ return NS_ERROR_FAILURE;
+ }
+ nsAutoCString originHost;
+ if (NS_FAILED(uri->GetAsciiHost(originHost))) {
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<Document> document = aParent->GetDoc();
+ if (!document || !document->IsHTMLDocument()) {
+ return NS_ERROR_FAILURE;
+ }
+ nsHTMLDocument* html = document->AsHTMLDocument();
+ // See if the given RP ID is a valid domain string.
+ // (We use the document's URI here as a template so we don't have to come up
+ // with our own scheme, etc. If we can successfully set the host as the given
+ // RP ID, then it should be a valid domain string.)
+ nsCOMPtr<nsIURI> inputRpIdURI;
+ nsresult rv = NS_MutateURI(uri)
+ .SetHost(NS_ConvertUTF16toUTF8(aInputRpId))
+ .Finalize(inputRpIdURI);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+ nsAutoCString inputRpId;
+ if (NS_FAILED(inputRpIdURI->GetAsciiHost(inputRpId))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!html->IsRegistrableDomainSuffixOfOrEqualTo(
+ NS_ConvertUTF8toUTF16(inputRpId), originHost)) {
+ return NS_ERROR_DOM_SECURITY_ERR;
+ }
+
+ aRelaxedRpId.Assign(inputRpId);
+ return NS_OK;
+}
+
+/***********************************************************************
+ * WebAuthnManager Implementation
+ **********************************************************************/
+
+void WebAuthnManager::ClearTransaction() {
+ if (!mTransaction.isNothing()) {
+ StopListeningForVisibilityEvents();
+ }
+
+ mTransaction.reset();
+ Unfollow();
+}
+
+void WebAuthnManager::RejectTransaction(const nsresult& aError) {
+ if (!NS_WARN_IF(mTransaction.isNothing())) {
+ mTransaction.ref().mPromise->MaybeReject(aError);
+ }
+
+ ClearTransaction();
+}
+
+void WebAuthnManager::CancelTransaction(const nsresult& aError) {
+ if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
+ mChild->SendRequestCancel(mTransaction.ref().mId);
+ }
+
+ RejectTransaction(aError);
+}
+
+void WebAuthnManager::HandleVisibilityChange() {
+ if (mTransaction.isSome()) {
+ mTransaction.ref().mVisibilityChanged = true;
+ }
+}
+
+WebAuthnManager::~WebAuthnManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTransaction.isSome()) {
+ ClearTransaction();
+ }
+
+ if (mChild) {
+ RefPtr<WebAuthnTransactionChild> c;
+ mChild.swap(c);
+ c->Disconnect();
+ }
+}
+
+already_AddRefed<Promise> WebAuthnManager::MakeCredential(
+ const PublicKeyCredentialCreationOptions& aOptions,
+ const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
+
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (mTransaction.isSome()) {
+ // If there hasn't been a visibility change during the current
+ // transaction, then let's let that one complete rather than
+ // cancelling it on a subsequent call.
+ if (!mTransaction.ref().mVisibilityChanged) {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ return promise.forget();
+ }
+
+ // Otherwise, the user may well have clicked away, so let's
+ // abort the old transaction and take over control from here.
+ CancelTransaction(NS_ERROR_ABORT);
+ }
+
+ // Abort the request if aborted flag is already set.
+ if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ return promise.forget();
+ }
+
+ nsString origin;
+ nsCString rpId;
+ nsresult rv = GetOrigin(mParent, origin, rpId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(rv);
+ return promise.forget();
+ }
+
+ // Enforce 5.4.3 User Account Parameters for Credential Generation
+ // When we add UX, we'll want to do more with this value, but for now
+ // we just have to verify its correctness.
+
+ CryptoBuffer userId;
+ userId.Assign(aOptions.mUser.mId);
+ if (userId.Length() > 64) {
+ promise->MaybeRejectWithTypeError("user.id is too long");
+ return promise.forget();
+ }
+
+ // If timeoutSeconds was specified, check if its value lies within a
+ // reasonable range as defined by the platform and if not, correct it to the
+ // closest value lying within that range.
+
+ uint32_t adjustedTimeout = 30000;
+ if (aOptions.mTimeout.WasPassed()) {
+ adjustedTimeout = aOptions.mTimeout.Value();
+ adjustedTimeout = std::max(15000u, adjustedTimeout);
+ adjustedTimeout = std::min(120000u, adjustedTimeout);
+ }
+
+ if (aOptions.mRp.mId.WasPassed()) {
+ // If rpId is specified, then invoke the procedure used for relaxing the
+ // same-origin restriction by setting the document.domain attribute, using
+ // rpId as the given value but without changing the current document’s
+ // domain. If no errors are thrown, set rpId to the value of host as
+ // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
+ // Otherwise, reject promise with a DOMException whose name is
+ // "SecurityError", and terminate this algorithm.
+
+ if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRp.mId.Value(), rpId))) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+ }
+
+ // <https://w3c.github.io/webauthn/#sctn-appid-extension>
+ if (aOptions.mExtensions.mAppid.WasPassed()) {
+ promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return promise.forget();
+ }
+
+ // Process each element of mPubKeyCredParams using the following steps, to
+ // produce a new sequence of coseAlgos.
+ nsTArray<CoseAlg> coseAlgos;
+ // If pubKeyCredParams is empty, append ES256 and RS256
+ if (aOptions.mPubKeyCredParams.IsEmpty()) {
+ coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::ES256));
+ coseAlgos.AppendElement(static_cast<long>(CoseAlgorithmIdentifier::RS256));
+ } else {
+ for (size_t a = 0; a < aOptions.mPubKeyCredParams.Length(); ++a) {
+ // If current.type does not contain a PublicKeyCredentialType
+ // supported by this implementation, then stop processing current and move
+ // on to the next element in mPubKeyCredParams.
+ if (!aOptions.mPubKeyCredParams[a].mType.EqualsLiteral(
+ MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
+ continue;
+ }
+
+ coseAlgos.AppendElement(aOptions.mPubKeyCredParams[a].mAlg);
+ }
+ }
+
+ // If there are algorithms specified, but none are Public_key algorithms,
+ // reject the promise.
+ if (coseAlgos.IsEmpty() && !aOptions.mPubKeyCredParams.IsEmpty()) {
+ promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return promise.forget();
+ }
+
+ // If excludeList is undefined, set it to the empty list.
+ //
+ // If extensions was specified, process any extensions supported by this
+ // client platform, to produce the extension data that needs to be sent to the
+ // authenticator. If an error is encountered while processing an extension,
+ // skip that extension and do not produce any extension data for it. Call the
+ // result of this processing clientExtensions.
+ //
+ // Currently no extensions are supported
+ //
+ // Use attestationChallenge, callerOrigin and rpId, along with the token
+ // binding key associated with callerOrigin (if any), to create a ClientData
+ // structure representing this request. Choose a hash algorithm for hashAlg
+ // and compute the clientDataJSON and clientDataHash.
+
+ CryptoBuffer challenge;
+ if (!challenge.Assign(aOptions.mChallenge)) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ nsAutoCString clientDataJSON;
+ nsresult srv = AssembleClientData(origin, challenge, u"webauthn.create"_ns,
+ aOptions.mExtensions, clientDataJSON);
+ if (NS_WARN_IF(NS_FAILED(srv))) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ nsTArray<WebAuthnScopedCredential> excludeList;
+ for (const auto& s : aOptions.mExcludeCredentials) {
+ WebAuthnScopedCredential c;
+ CryptoBuffer cb;
+ cb.Assign(s.mId);
+ c.id() = cb;
+ excludeList.AppendElement(c);
+ }
+
+ if (!MaybeCreateBackgroundActor()) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ // TODO: Add extension list building
+ nsTArray<WebAuthnExtension> extensions;
+
+ // <https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-hmac-secret-extension>
+ if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
+ bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
+ if (hmacCreateSecret) {
+ extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
+ }
+ }
+
+ const auto& selection = aOptions.mAuthenticatorSelection;
+ const auto& attachment = selection.mAuthenticatorAttachment;
+ const nsString& attestation = aOptions.mAttestation;
+
+ // Attachment
+ Maybe<nsString> authenticatorAttachment;
+ if (attachment.WasPassed()) {
+ authenticatorAttachment.emplace(attachment.Value());
+ }
+
+ // The residentKey field was added in WebAuthn level 2. It takes precedent
+ // over the requireResidentKey field if and only if it is present and it is a
+ // member of the ResidentKeyRequirement enum.
+ static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
+ bool useResidentKeyValue =
+ selection.mResidentKey.WasPassed() &&
+ (selection.mResidentKey.Value().EqualsLiteral(
+ MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED) ||
+ selection.mResidentKey.Value().EqualsLiteral(
+ MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_PREFERRED) ||
+ selection.mResidentKey.Value().EqualsLiteral(
+ MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED));
+
+ nsString residentKey;
+ if (useResidentKeyValue) {
+ residentKey = selection.mResidentKey.Value();
+ } else {
+ // "If no value is given then the effective value is required if
+ // requireResidentKey is true or discouraged if it is false or absent."
+ if (selection.mRequireResidentKey) {
+ residentKey.AssignLiteral(MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_REQUIRED);
+ } else {
+ residentKey.AssignLiteral(
+ MOZ_WEBAUTHN_RESIDENT_KEY_REQUIREMENT_DISCOURAGED);
+ }
+ }
+
+ // Create and forward authenticator selection criteria.
+ WebAuthnAuthenticatorSelection authSelection(
+ residentKey, selection.mUserVerification, authenticatorAttachment);
+
+ nsString rpIcon;
+ if (aOptions.mRp.mIcon.WasPassed()) {
+ rpIcon = aOptions.mRp.mIcon.Value();
+ }
+
+ nsString userIcon;
+ if (aOptions.mUser.mIcon.WasPassed()) {
+ userIcon = aOptions.mUser.mIcon.Value();
+ }
+
+ WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName, rpIcon);
+
+ WebAuthnMakeCredentialUserInfo userInfo(
+ userId, aOptions.mUser.mName, userIcon, aOptions.mUser.mDisplayName);
+
+ BrowsingContext* context = mParent->GetBrowsingContext();
+ if (!context) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ WebAuthnMakeCredentialInfo info(
+ origin, NS_ConvertUTF8toUTF16(rpId), challenge, clientDataJSON,
+ adjustedTimeout, excludeList, rpInfo, userInfo, coseAlgos, extensions,
+ authSelection, attestation, context->Top()->Id());
+
+#ifdef OS_WIN
+ if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
+ ListenForVisibilityEvents();
+ }
+#else
+ ListenForVisibilityEvents();
+#endif
+
+ AbortSignal* signal = nullptr;
+ if (aSignal.WasPassed()) {
+ signal = &aSignal.Value();
+ Follow(signal);
+ }
+
+ MOZ_ASSERT(mTransaction.isNothing());
+ mTransaction = Some(WebAuthnTransaction(promise));
+ mChild->SendRequestRegister(mTransaction.ref().mId, info);
+
+ return promise.forget();
+}
+
+const size_t MAX_ALLOWED_CREDENTIALS = 20;
+
+already_AddRefed<Promise> WebAuthnManager::GetAssertion(
+ const PublicKeyCredentialRequestOptions& aOptions,
+ const Optional<OwningNonNull<AbortSignal>>& aSignal, ErrorResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
+
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (mTransaction.isSome()) {
+ // If there hasn't been a visibility change during the current
+ // transaction, then let's let that one complete rather than
+ // cancelling it on a subsequent call.
+ if (!mTransaction.ref().mVisibilityChanged) {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ return promise.forget();
+ }
+
+ // Otherwise, the user may well have clicked away, so let's
+ // abort the old transaction and take over control from here.
+ CancelTransaction(NS_ERROR_ABORT);
+ }
+
+ // Abort the request if aborted flag is already set.
+ if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ return promise.forget();
+ }
+
+ nsString origin;
+ nsCString rpId;
+ nsresult rv = GetOrigin(mParent, origin, rpId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(rv);
+ return promise.forget();
+ }
+
+ // If timeoutSeconds was specified, check if its value lies within a
+ // reasonable range as defined by the platform and if not, correct it to the
+ // closest value lying within that range.
+
+ uint32_t adjustedTimeout = 30000;
+ if (aOptions.mTimeout.WasPassed()) {
+ adjustedTimeout = aOptions.mTimeout.Value();
+ adjustedTimeout = std::max(15000u, adjustedTimeout);
+ adjustedTimeout = std::min(120000u, adjustedTimeout);
+ }
+
+ if (aOptions.mRpId.WasPassed()) {
+ // If rpId is specified, then invoke the procedure used for relaxing the
+ // same-origin restriction by setting the document.domain attribute, using
+ // rpId as the given value but without changing the current document’s
+ // domain. If no errors are thrown, set rpId to the value of host as
+ // computed by this procedure, and rpIdHash to the SHA-256 hash of rpId.
+ // Otherwise, reject promise with a DOMException whose name is
+ // "SecurityError", and terminate this algorithm.
+
+ if (NS_FAILED(RelaxSameOrigin(mParent, aOptions.mRpId.Value(), rpId))) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+ }
+
+ CryptoBuffer rpIdHash;
+ if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
+ promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return promise.forget();
+ }
+
+ // Abort the request if the allowCredentials set is too large
+ if (aOptions.mAllowCredentials.Length() > MAX_ALLOWED_CREDENTIALS) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ // Use assertionChallenge, callerOrigin and rpId, along with the token binding
+ // key associated with callerOrigin (if any), to create a ClientData structure
+ // representing this request. Choose a hash algorithm for hashAlg and compute
+ // the clientDataJSON and clientDataHash.
+ CryptoBuffer challenge;
+ if (!challenge.Assign(aOptions.mChallenge)) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ nsAutoCString clientDataJSON;
+ rv = AssembleClientData(origin, challenge, u"webauthn.get"_ns,
+ aOptions.mExtensions, clientDataJSON);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ nsTArray<WebAuthnScopedCredential> allowList;
+ for (const auto& s : aOptions.mAllowCredentials) {
+ if (s.mType.EqualsLiteral(
+ MOZ_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_TYPE_PUBLIC_KEY)) {
+ WebAuthnScopedCredential c;
+ CryptoBuffer cb;
+ cb.Assign(s.mId);
+ c.id() = cb;
+
+ // Serialize transports.
+ if (s.mTransports.WasPassed()) {
+ uint8_t transports = 0;
+
+ // We ignore unknown transports for forward-compatibility, but this
+ // needs to be reviewed if values are added to the
+ // AuthenticatorTransport enum.
+ static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2);
+ for (const nsAString& str : s.mTransports.Value()) {
+ if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_USB)) {
+ transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_USB;
+ } else if (str.EqualsLiteral(
+ MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_NFC)) {
+ transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_NFC;
+ } else if (str.EqualsLiteral(
+ MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_BLE)) {
+ transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_BLE;
+ } else if (str.EqualsLiteral(
+ MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_INTERNAL)) {
+ transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_INTERNAL;
+ }
+ }
+ c.transports() = transports;
+ }
+
+ allowList.AppendElement(c);
+ }
+ }
+
+ if (!MaybeCreateBackgroundActor()) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ // If extensions were specified, process any extensions supported by this
+ // client platform, to produce the extension data that needs to be sent to the
+ // authenticator. If an error is encountered while processing an extension,
+ // skip that extension and do not produce any extension data for it. Call the
+ // result of this processing clientExtensions.
+ nsTArray<WebAuthnExtension> extensions;
+
+ // <https://w3c.github.io/webauthn/#sctn-appid-extension>
+ if (aOptions.mExtensions.mAppid.WasPassed()) {
+ nsString appId(aOptions.mExtensions.mAppid.Value());
+
+ // Check that the appId value is allowed.
+ if (!EvaluateAppID(mParent, origin, appId)) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ CryptoBuffer appIdHash;
+ if (!appIdHash.SetLength(SHA256_LENGTH, fallible)) {
+ promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
+ return promise.forget();
+ }
+
+ // We need the SHA-256 hash of the appId.
+ rv = HashCString(NS_ConvertUTF16toUTF8(appId), appIdHash);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
+ return promise.forget();
+ }
+
+ // Append the hash and send it to the backend.
+ extensions.AppendElement(WebAuthnExtensionAppId(appIdHash, appId));
+ }
+
+ BrowsingContext* context = mParent->GetBrowsingContext();
+ if (!context) {
+ promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
+ return promise.forget();
+ }
+
+ WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge,
+ clientDataJSON, adjustedTimeout, allowList,
+ extensions, aOptions.mUserVerification,
+ context->Top()->Id());
+
+#ifdef OS_WIN
+ if (!WinWebAuthnManager::AreWebAuthNApisAvailable()) {
+ ListenForVisibilityEvents();
+ }
+#else
+ ListenForVisibilityEvents();
+#endif
+
+ AbortSignal* signal = nullptr;
+ if (aSignal.WasPassed()) {
+ signal = &aSignal.Value();
+ Follow(signal);
+ }
+
+ MOZ_ASSERT(mTransaction.isNothing());
+ mTransaction = Some(WebAuthnTransaction(promise));
+ mChild->SendRequestSign(mTransaction.ref().mId, info);
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> WebAuthnManager::Store(const Credential& aCredential,
+ ErrorResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
+
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (mTransaction.isSome()) {
+ // If there hasn't been a visibility change during the current
+ // transaction, then let's let that one complete rather than
+ // cancelling it on a subsequent call.
+ if (!mTransaction.ref().mVisibilityChanged) {
+ promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
+ return promise.forget();
+ }
+
+ // Otherwise, the user may well have clicked away, so let's
+ // abort the old transaction and take over control from here.
+ CancelTransaction(NS_ERROR_ABORT);
+ }
+
+ promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return promise.forget();
+}
+
+void WebAuthnManager::FinishMakeCredential(
+ const uint64_t& aTransactionId,
+ const WebAuthnMakeCredentialResult& aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Check for a valid transaction.
+ if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
+ return;
+ }
+
+ CryptoBuffer clientDataBuf;
+ if (NS_WARN_IF(!clientDataBuf.Assign(aResult.ClientDataJSON()))) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ CryptoBuffer attObjBuf;
+ if (NS_WARN_IF(!attObjBuf.Assign(aResult.AttestationObject()))) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ CryptoBuffer keyHandleBuf;
+ if (NS_WARN_IF(!keyHandleBuf.Assign(aResult.KeyHandle()))) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ nsAutoString keyHandleBase64Url;
+ nsresult rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ RejectTransaction(rv);
+ return;
+ }
+
+ // Create a new PublicKeyCredential object and populate its fields with the
+ // values returned from the authenticator as well as the clientDataJSON
+ // computed earlier.
+ RefPtr<AuthenticatorAttestationResponse> attestation =
+ new AuthenticatorAttestationResponse(mParent);
+ attestation->SetClientDataJSON(clientDataBuf);
+ attestation->SetAttestationObject(attObjBuf);
+
+ RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
+ credential->SetId(keyHandleBase64Url);
+ credential->SetType(u"public-key"_ns);
+ credential->SetRawId(keyHandleBuf);
+ credential->SetResponse(attestation);
+
+ // Forward client extension results.
+ for (const auto& ext : aResult.Extensions()) {
+ if (ext.type() ==
+ WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
+ bool hmacCreateSecret =
+ ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
+ credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
+ }
+ }
+
+ mTransaction.ref().mPromise->MaybeResolve(credential);
+ ClearTransaction();
+}
+
+void WebAuthnManager::FinishGetAssertion(
+ const uint64_t& aTransactionId, const WebAuthnGetAssertionResult& aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Check for a valid transaction.
+ if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
+ return;
+ }
+
+ CryptoBuffer clientDataBuf;
+ if (!clientDataBuf.Assign(aResult.ClientDataJSON())) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ CryptoBuffer credentialBuf;
+ if (!credentialBuf.Assign(aResult.KeyHandle())) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ CryptoBuffer signatureBuf;
+ if (!signatureBuf.Assign(aResult.Signature())) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ CryptoBuffer authenticatorDataBuf;
+ if (!authenticatorDataBuf.Assign(aResult.AuthenticatorData())) {
+ RejectTransaction(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ nsAutoString credentialBase64Url;
+ nsresult rv = credentialBuf.ToJwkBase64(credentialBase64Url);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ RejectTransaction(rv);
+ return;
+ }
+
+ CryptoBuffer userHandleBuf;
+ // U2FTokenManager don't return user handle.
+ // Best effort.
+ userHandleBuf.Assign(aResult.UserHandle());
+
+ // If any authenticator returns success:
+
+ // Create a new PublicKeyCredential object named value and populate its fields
+ // with the values returned from the authenticator as well as the
+ // clientDataJSON computed earlier.
+ RefPtr<AuthenticatorAssertionResponse> assertion =
+ new AuthenticatorAssertionResponse(mParent);
+ assertion->SetClientDataJSON(clientDataBuf);
+ assertion->SetAuthenticatorData(authenticatorDataBuf);
+ assertion->SetSignature(signatureBuf);
+ if (!userHandleBuf.IsEmpty()) {
+ assertion->SetUserHandle(userHandleBuf);
+ }
+
+ RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
+ credential->SetId(credentialBase64Url);
+ credential->SetType(u"public-key"_ns);
+ credential->SetRawId(credentialBuf);
+ credential->SetResponse(assertion);
+
+ // Forward client extension results.
+ for (const auto& ext : aResult.Extensions()) {
+ if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
+ bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
+ credential->SetClientExtensionResultAppId(appid);
+ }
+ }
+
+ mTransaction.ref().mPromise->MaybeResolve(credential);
+ ClearTransaction();
+}
+
+void WebAuthnManager::RequestAborted(const uint64_t& aTransactionId,
+ const nsresult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
+ RejectTransaction(aError);
+ }
+}
+
+void WebAuthnManager::RunAbortAlgorithm() {
+ CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
+}
+
+} // namespace mozilla::dom