diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/webauthn/WebAuthnController.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip |
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | dom/webauthn/WebAuthnController.cpp | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/dom/webauthn/WebAuthnController.cpp b/dom/webauthn/WebAuthnController.cpp new file mode 100644 index 0000000000..c089baaca3 --- /dev/null +++ b/dom/webauthn/WebAuthnController.cpp @@ -0,0 +1,739 @@ +/* -*- 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 "json/json.h" +#include "mozilla/dom/WebAuthnController.h" +#include "mozilla/dom/PWebAuthnTransactionParent.h" +#include "mozilla/dom/WebAuthnUtil.h" +#include "mozilla/ipc/BackgroundParent.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/StaticPrefs_security.h" +#include "mozilla/Unused.h" +#include "nsEscape.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIThread.h" +#include "nsTextFormatter.h" +#include "mozilla/Telemetry.h" + +#include "AuthrsTransport.h" +#include "CtapArgs.h" +#include "CtapResults.h" +#include "U2FSoftTokenTransport.h" +#include "WebAuthnEnumStrings.h" + +#ifdef MOZ_WIDGET_ANDROID +# include "mozilla/dom/AndroidWebAuthnTokenManager.h" +#endif + +namespace mozilla::dom { + +/*********************************************************************** + * Statics + **********************************************************************/ + +namespace { +static mozilla::LazyLogModule gWebAuthnControllerLog("webauthncontroller"); +StaticRefPtr<WebAuthnController> gWebAuthnController; +static nsIThread* gWebAuthnBackgroundThread; +} // namespace + +// Data for WebAuthn UI prompt notifications. +static const char16_t kPresencePromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"presence\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu}"; +static const char16_t kRegisterDirectPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"register-direct\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu}"; +static const char16_t kCancelPromptNotification[] = + u"{\"is_ctap2\":true,\"action\":\"cancel\",\"tid\":%llu}"; +static const char16_t kSelectSignResultNotification[] = + u"{\"is_ctap2\":true,\"action\":\"select-sign-result\",\"tid\":%llu," + u"\"origin\":\"%s\",\"browsingContextId\":%llu,\"usernames\":[%s]}"; + +/*********************************************************************** + * U2FManager Implementation + **********************************************************************/ + +NS_IMPL_ISUPPORTS(WebAuthnController, nsIWebAuthnController, + nsIU2FTokenManager); + +WebAuthnController::WebAuthnController() : mTransactionParent(nullptr) { + MOZ_ASSERT(XRE_IsParentProcess()); + // Create on the main thread to make sure ClearOnShutdown() works. + MOZ_ASSERT(NS_IsMainThread()); +} + +// static +void WebAuthnController::Initialize() { + if (!XRE_IsParentProcess()) { + return; + } + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!gWebAuthnController); + gWebAuthnController = new WebAuthnController(); + ClearOnShutdown(&gWebAuthnController); +} + +// static +WebAuthnController* WebAuthnController::Get() { + MOZ_ASSERT(XRE_IsParentProcess()); + mozilla::ipc::AssertIsOnBackgroundThread(); + return gWebAuthnController; +} + +void WebAuthnController::AbortTransaction(const uint64_t& aTransactionId, + const nsresult& aError, + bool shouldCancelActiveDialog) { + if (mTransactionParent && mTransaction.isSome() && aTransactionId > 0 && + aTransactionId == mTransaction.ref().mTransactionId) { + Unused << mTransactionParent->SendAbort(aTransactionId, aError); + ClearTransaction(shouldCancelActiveDialog); + } +} + +void WebAuthnController::AbortOngoingTransaction() { + if (mTransaction.isSome()) { + AbortTransaction(mTransaction.ref().mTransactionId, NS_ERROR_DOM_ABORT_ERR, + true); + } +} + +void WebAuthnController::MaybeClearTransaction( + PWebAuthnTransactionParent* aParent) { + // Only clear if we've been requested to do so by our current transaction + // parent. + if (mTransactionParent == aParent) { + ClearTransaction(true); + } +} + +void WebAuthnController::ClearTransaction(bool cancel_prompt) { + if (cancel_prompt && mTransaction.isSome() && + mTransaction.ref().mTransactionId > 0) { + // Remove any prompts we might be showing for the current transaction. + SendPromptNotification(kCancelPromptNotification, + mTransaction.ref().mTransactionId); + } + mTransactionParent = nullptr; + mTransportImpl = nullptr; + + // Forget any pending registration. + mPendingRegisterInfo.reset(); + mPendingSignInfo.reset(); + mPendingSignResults.Clear(); + mTransaction.reset(); +} + +template <typename... T> +void WebAuthnController::SendPromptNotification(const char16_t* aFormat, + T... aArgs) { + MOZ_ASSERT(!NS_IsMainThread()); + nsAutoString json; + nsTextFormatter::ssprintf(json, aFormat, aArgs...); + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>( + "WebAuthnController::RunSendPromptNotification", this, + &WebAuthnController::RunSendPromptNotification, json)); + + MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch( + r.forget(), NS_DISPATCH_NORMAL)); +} + +NS_IMETHODIMP +WebAuthnController::SendPromptNotificationPreformatted( + uint64_t aTransactionId, const nsACString& aJson) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<nsString>( + "WebAuthnController::RunSendPromptNotification", this, + &WebAuthnController::RunSendPromptNotification, + NS_ConvertUTF8toUTF16(aJson))); + MOZ_ALWAYS_SUCCEEDS(GetMainThreadSerialEventTarget()->Dispatch( + r.forget(), NS_DISPATCH_NORMAL)); + return NS_OK; +} + +void WebAuthnController::RunSendPromptNotification(const nsString& aJSON) { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIObserverService> os = services::GetObserverService(); + if (NS_WARN_IF(!os)) { + return; + } + + nsCOMPtr<nsIWebAuthnController> self = this; + MOZ_ALWAYS_SUCCEEDS( + os->NotifyObservers(self, "webauthn-prompt", aJSON.get())); +} + +nsCOMPtr<nsIWebAuthnTransport> WebAuthnController::GetTransportImpl() { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransportImpl) { + return mTransportImpl; + } + +/* Enable in Bug 1819414 */ +#if 0 +# ifdef MOZ_WIDGET_ANDROID + // On Android, prefer the platform support if enabled. + if (StaticPrefs::security_webauth_webauthn_enable_android_fido2()) { + nsCOMPtr<nsIWebAuthnTransport> transport = AndroidWebAuthnTokenManager::GetInstance(); + transport->SetController(this); + return transport; + } +# endif +#endif + + // Prefer the HW token, even if the softtoken is enabled too. + // We currently don't support soft and USB tokens enabled at the + // same time as the softtoken would always win the race to register. + // We could support it for signing though... + if (StaticPrefs::security_webauth_webauthn_enable_usbtoken()) { + nsCOMPtr<nsIWebAuthnTransport> transport = NewAuthrsTransport(); + transport->SetController(this); + return transport; + } + + if (StaticPrefs::security_webauth_webauthn_enable_softtoken()) { + nsCOMPtr<nsIWebAuthnTransport> transport = new U2FSoftTokenTransport( + StaticPrefs::security_webauth_softtoken_counter()); + transport->SetController(this); + return transport; + } + + // TODO Use WebAuthnRequest to aggregate results from all transports, + // once we have multiple HW transport types. + + return nullptr; +} + +void WebAuthnController::Cancel(PWebAuthnTransactionParent* aTransactionParent, + const Tainted<uint64_t>& aTransactionId) { + // The last transaction ID also suffers from the issue described in Bug + // 1696159. A content process could cancel another content processes + // transaction by guessing the last transaction ID. + if (mTransactionParent != aTransactionParent || mTransaction.isNothing() || + !MOZ_IS_VALID(aTransactionId, + mTransaction.ref().mTransactionId == aTransactionId)) { + return; + } + + if (mTransportImpl) { + mTransportImpl->Cancel(); + } + + ClearTransaction(true); +} + +void WebAuthnController::Register( + PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, const WebAuthnMakeCredentialInfo& aInfo) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, + ("WebAuthnController::Register")); + MOZ_ASSERT(aTransactionId > 0); + + if (!gWebAuthnBackgroundThread) { + gWebAuthnBackgroundThread = NS_GetCurrentThread(); + MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!"); + } + + AbortOngoingTransaction(); + mTransactionParent = aTransactionParent; + + /* We could refactor to defer the hashing here */ + CryptoBuffer rpIdHash, clientDataHash; + NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); + nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, + clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We haven't set mTransaction yet, so we can't use AbortTransaction + Unused << mTransactionParent->SendAbort(aTransactionId, + NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + // Hold on to any state that we need to finish the transaction. + mTransaction = Some( + Transaction(aTransactionId, rpIdHash, Nothing(), aInfo.ClientDataJSON())); + + MOZ_ASSERT(mPendingRegisterInfo.isNothing()); + mPendingRegisterInfo = Some(aInfo); + + // Determine whether direct attestation was requested. + bool noneAttestationRequested = true; + +// On Android, let's always reject direct attestations until we have a +// mechanism to solicit user consent, from Bug 1550164 +#ifndef MOZ_WIDGET_ANDROID + // The default attestation type is "none", so set + // noneAttestationRequested=false only if the RP's preference matches one of + // the other known types. This needs to be reviewed if values are added to + // the AttestationConveyancePreference enum. + const nsString& attestation = aInfo.attestationConveyancePreference(); + static_assert(MOZ_WEBAUTHN_ENUM_STRINGS_VERSION == 2); + if (attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_DIRECT) || + attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_INDIRECT) || + attestation.EqualsLiteral( + MOZ_WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_ENTERPRISE)) { + noneAttestationRequested = false; + } +#endif // not MOZ_WIDGET_ANDROID + + // Start a register request immediately if direct attestation + // wasn't requested or the test pref is set. + if (noneAttestationRequested || + StaticPrefs:: + security_webauth_webauthn_testing_allow_direct_attestation()) { + DoRegister(aInfo, noneAttestationRequested); + return; + } + + // If the RP request direct attestation, ask the user for permission and + // store the transaction info until the user proceeds or cancels. + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + SendPromptNotification(kRegisterDirectPromptNotification, aTransactionId, + origin.get(), aInfo.BrowsingContextId()); +} + +void WebAuthnController::DoRegister(const WebAuthnMakeCredentialInfo& aInfo, + bool aForceNoneAttestation) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_ASSERT(mTransaction.isSome()); + if (NS_WARN_IF(mTransaction.isNothing())) { + // Clear prompt? + return; + } + + // Show a prompt that lets the user cancel the ongoing transaction. + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + SendPromptNotification(kPresencePromptNotification, + mTransaction.ref().mTransactionId, origin.get(), + aInfo.BrowsingContextId(), "false"); + + RefPtr<CtapRegisterArgs> args( + new CtapRegisterArgs(aInfo, aForceNoneAttestation)); + + mTransportImpl = GetTransportImpl(); + if (!mTransportImpl) { + AbortTransaction(mTransaction.ref().mTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR, true); + return; + } + nsresult rv = mTransportImpl->MakeCredential( + mTransaction.ref().mTransactionId, aInfo.BrowsingContextId(), args); + if (NS_FAILED(rv)) { + AbortTransaction(mTransaction.ref().mTransactionId, rv, true); + return; + } +} + +NS_IMETHODIMP +WebAuthnController::ResumeRegister(uint64_t aTransactionId, + bool aForceNoneAttestation) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, bool>( + "WebAuthnController::RunResumeRegister", this, + &WebAuthnController::RunResumeRegister, aTransactionId, + aForceNoneAttestation)); + + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunResumeRegister(uint64_t aTransactionId, + bool aForceNoneAttestation) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (NS_WARN_IF(mPendingRegisterInfo.isNothing())) { + return; + } + + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + // Resume registration and cleanup. + DoRegister(mPendingRegisterInfo.ref(), aForceNoneAttestation); +} + +NS_IMETHODIMP +WebAuthnController::FinishRegister(uint64_t aTransactionId, + nsICtapRegisterResult* aResult) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsCOMPtr<nsIRunnable> r( + NewRunnableMethod<uint64_t, RefPtr<nsICtapRegisterResult>>( + "WebAuthnController::RunFinishRegister", this, + &WebAuthnController::RunFinishRegister, aTransactionId, aResult)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunFinishRegister( + uint64_t aTransactionId, const RefPtr<nsICtapRegisterResult>& aResult) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + aTransactionId != mTransaction.ref().mTransactionId) { + // The previous transaction was likely cancelled from the prompt. + return; + } + + nsresult status; + nsresult rv = aResult->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + if (NS_FAILED(status)) { + bool shouldCancelActiveDialog = true; + if (status == NS_ERROR_DOM_OPERATION_ERR) { + // PIN-related errors. Let the dialog show to inform the user + shouldCancelActiveDialog = false; + } + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPRegisterAbort"_ns, 1); + AbortTransaction(aTransactionId, status, shouldCancelActiveDialog); + return; + } + + nsCString clientDataJson = mPendingRegisterInfo.ref().ClientDataJSON(); + + nsTArray<uint8_t> attObj; + rv = aResult->GetAttestationObject(attObj); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<uint8_t> credentialId; + rv = aResult->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<WebAuthnExtensionResult> extensions; + WebAuthnMakeCredentialResult result(clientDataJson, attObj, credentialId, + extensions); + + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPRegisterFinish"_ns, 1); + Unused << mTransactionParent->SendConfirmRegister(aTransactionId, result); + ClearTransaction(true); +} + +void WebAuthnController::Sign(PWebAuthnTransactionParent* aTransactionParent, + const uint64_t& aTransactionId, + const WebAuthnGetAssertionInfo& aInfo) { + mozilla::ipc::AssertIsOnBackgroundThread(); + MOZ_LOG(gWebAuthnControllerLog, LogLevel::Debug, ("WebAuthnSign")); + MOZ_ASSERT(aTransactionId > 0); + + if (!gWebAuthnBackgroundThread) { + gWebAuthnBackgroundThread = NS_GetCurrentThread(); + MOZ_ASSERT(gWebAuthnBackgroundThread, "This should never be null!"); + } + + AbortOngoingTransaction(); + mTransactionParent = aTransactionParent; + + /* We could refactor to defer the hashing here */ + CryptoBuffer rpIdHash, clientDataHash; + NS_ConvertUTF16toUTF8 rpId(aInfo.RpId()); + nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(), rpIdHash, + clientDataHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + // We haven't set mTransaction yet, so we can't use AbortTransaction + Unused << mTransactionParent->SendAbort(aTransactionId, + NS_ERROR_DOM_UNKNOWN_ERR); + return; + } + + Maybe<nsTArray<uint8_t>> appIdHash = Nothing(); + for (const WebAuthnExtension& ext : aInfo.Extensions()) { + if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) { + appIdHash = Some(ext.get_WebAuthnExtensionAppId().AppId().Clone()); + } + } + + // Hold on to any state that we need to finish the transaction. + mTransaction = Some( + Transaction(aTransactionId, rpIdHash, appIdHash, aInfo.ClientDataJSON())); + + mPendingSignInfo = Some(aInfo); + + // Show a prompt that lets the user cancel the ongoing transaction. + NS_ConvertUTF16toUTF8 origin(aInfo.Origin()); + SendPromptNotification(kPresencePromptNotification, + mTransaction.ref().mTransactionId, origin.get(), + aInfo.BrowsingContextId(), "false"); + + RefPtr<CtapSignArgs> args(new CtapSignArgs(aInfo)); + + mTransportImpl = GetTransportImpl(); + if (!mTransportImpl) { + AbortTransaction(mTransaction.ref().mTransactionId, + NS_ERROR_DOM_NOT_ALLOWED_ERR, true); + return; + } + + rv = mTransportImpl->GetAssertion(mTransaction.ref().mTransactionId, + aInfo.BrowsingContextId(), args.get()); + if (NS_FAILED(rv)) { + AbortTransaction(mTransaction.ref().mTransactionId, rv, true); + return; + } +} + +NS_IMETHODIMP +WebAuthnController::FinishSign( + uint64_t aTransactionId, + const nsTArray<RefPtr<nsICtapSignResult>>& aResult) { + MOZ_ASSERT(XRE_IsParentProcess()); + nsTArray<RefPtr<nsICtapSignResult>> ownedResult = aResult.Clone(); + + nsCOMPtr<nsIRunnable> r( + NewRunnableMethod<uint64_t, nsTArray<RefPtr<nsICtapSignResult>>>( + "WebAuthnController::RunFinishSign", this, + &WebAuthnController::RunFinishSign, aTransactionId, + std::move(ownedResult))); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunFinishSign( + uint64_t aTransactionId, + const nsTArray<RefPtr<nsICtapSignResult>>& aResult) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + aTransactionId != mTransaction.ref().mTransactionId) { + return; + } + + if (aResult.Length() == 0) { + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + AbortTransaction(aTransactionId, NS_ERROR_DOM_UNKNOWN_ERR, true); + return; + } + + if (aResult.Length() == 1) { + nsresult status; + nsresult rv = aResult[0]->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + if (NS_FAILED(status)) { + bool shouldCancelActiveDialog = true; + if (status == NS_ERROR_DOM_OPERATION_ERR) { + // PIN-related errors, e.g. blocked token. Let the dialog show to inform + // the user + shouldCancelActiveDialog = false; + } + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + AbortTransaction(aTransactionId, status, shouldCancelActiveDialog); + return; + } + mPendingSignResults = aResult.Clone(); + RunResumeWithSelectedSignResult(aTransactionId, 0); + return; + } + + // If we more than one assertion, all of them should have OK status. + for (const auto& assertion : aResult) { + nsresult status; + nsresult rv = assertion->GetStatus(&status); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + if (NS_WARN_IF(NS_FAILED(status))) { + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignAbort"_ns, 1); + AbortTransaction(aTransactionId, status, true); + return; + } + } + + nsCString usernames; + StringJoinAppend( + usernames, ","_ns, aResult, + [](nsACString& dst, const RefPtr<nsICtapSignResult>& assertion) { + nsCString username; + nsresult rv = assertion->GetUserName(username); + if (NS_FAILED(rv)) { + username.Assign("<Unknown username>"); + } + nsCString escaped_username; + NS_Escape(username, escaped_username, url_XAlphas); + dst.Append("\""_ns + escaped_username + "\""_ns); + }); + + mPendingSignResults = aResult.Clone(); + NS_ConvertUTF16toUTF8 origin(mPendingSignInfo.ref().Origin()); + SendPromptNotification(kSelectSignResultNotification, + mTransaction.ref().mTransactionId, origin.get(), + mPendingSignInfo.ref().BrowsingContextId(), + usernames.get()); +} + +NS_IMETHODIMP +WebAuthnController::SignatureSelectionCallback(uint64_t aTransactionId, + uint64_t idx) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, uint64_t>( + "WebAuthnController::RunResumeWithSelectedSignResult", this, + &WebAuthnController::RunResumeWithSelectedSignResult, aTransactionId, + idx)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunResumeWithSelectedSignResult( + uint64_t aTransactionId, uint64_t idx) { + mozilla::ipc::AssertIsOnBackgroundThread(); + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + if (NS_WARN_IF(mPendingSignResults.Length() <= idx)) { + return; + } + + RefPtr<nsICtapSignResult>& selected = mPendingSignResults[idx]; + + nsTArray<uint8_t> credentialId; + nsresult rv = selected->GetCredentialId(credentialId); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<uint8_t> signature; + rv = selected->GetSignature(signature); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<uint8_t> authenticatorData; + rv = selected->GetAuthenticatorData(authenticatorData); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<uint8_t> rpIdHash; + rv = selected->GetRpIdHash(rpIdHash); + if (NS_WARN_IF(NS_FAILED(rv))) { + AbortTransaction(aTransactionId, NS_ERROR_FAILURE, true); + return; + } + + nsTArray<uint8_t> userHandle; + Unused << selected->GetUserHandle(userHandle); // optional + + nsTArray<WebAuthnExtensionResult> extensions; + if (mTransaction.ref().mAppIdHash.isSome()) { + bool usedAppId = (rpIdHash == mTransaction.ref().mAppIdHash.ref()); + extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId)); + } + + WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON, + credentialId, signature, authenticatorData, + extensions, userHandle); + + Telemetry::ScalarAdd(Telemetry::ScalarID::SECURITY_WEBAUTHN_USED, + u"CTAPSignFinish"_ns, 1); + Unused << mTransactionParent->SendConfirmSign(aTransactionId, result); + ClearTransaction(true); +} + +NS_IMETHODIMP +WebAuthnController::PinCallback(uint64_t aTransactionId, + const nsACString& aPin) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t, nsCString>( + "WebAuthnController::RunPinCallback", this, + &WebAuthnController::RunPinCallback, aTransactionId, aPin)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunPinCallback(uint64_t aTransactionId, + const nsCString& aPin) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransportImpl) { + mTransportImpl->PinCallback(aTransactionId, aPin); + } +} + +NS_IMETHODIMP +WebAuthnController::Cancel(uint64_t aTransactionId) { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIRunnable> r(NewRunnableMethod<uint64_t>( + "WebAuthnController::RunCancel", this, &WebAuthnController::RunCancel, + aTransactionId)); + + if (!gWebAuthnBackgroundThread) { + return NS_ERROR_FAILURE; + } + return gWebAuthnBackgroundThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +void WebAuthnController::RunCancel(uint64_t aTransactionId) { + mozilla::ipc::AssertIsOnBackgroundThread(); + + if (mTransaction.isNothing() || + mTransaction.ref().mTransactionId != aTransactionId) { + return; + } + + // Cancel the request. + if (mTransportImpl) { + mTransportImpl->Cancel(); + } + + // Reject the promise. + AbortTransaction(aTransactionId, NS_ERROR_DOM_NOT_ALLOWED_ERR, true); +} + +} // namespace mozilla::dom |