diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/webauthn/WebAuthnManager.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/webauthn/WebAuthnManager.cpp')
-rw-r--r-- | dom/webauthn/WebAuthnManager.cpp | 864 |
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 |