mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-03-04 07:40:42 +00:00

authenticator-rs keeps its device monitor thread alive after a transaction has completed. This is a workaround to ensure that the device monitor thread does not prevent the browser from closing. Differential Revision: https://phabricator.services.mozilla.com/D193481
410 lines
15 KiB
C++
410 lines
15 KiB
C++
/* -*- 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 "mozilla/dom/WebAuthnTransactionParent.h"
|
|
#include "mozilla/ipc/PBackgroundParent.h"
|
|
#include "mozilla/ipc/BackgroundParent.h"
|
|
#include "mozilla/StaticPrefs_security.h"
|
|
|
|
#include "nsThreadUtils.h"
|
|
#include "WebAuthnArgs.h"
|
|
|
|
namespace mozilla::dom {
|
|
|
|
void WebAuthnTransactionParent::CompleteTransaction() {
|
|
if (mTransactionId.isSome()) {
|
|
if (mRegisterPromiseRequest.Exists()) {
|
|
mRegisterPromiseRequest.Complete();
|
|
}
|
|
if (mSignPromiseRequest.Exists()) {
|
|
mSignPromiseRequest.Complete();
|
|
}
|
|
if (mWebAuthnService) {
|
|
// We have to do this to work around Bug 1864526.
|
|
mWebAuthnService->Cancel(mTransactionId.ref());
|
|
}
|
|
mTransactionId.reset();
|
|
}
|
|
}
|
|
|
|
void WebAuthnTransactionParent::DisconnectTransaction() {
|
|
mTransactionId.reset();
|
|
mRegisterPromiseRequest.DisconnectIfExists();
|
|
mSignPromiseRequest.DisconnectIfExists();
|
|
if (mWebAuthnService) {
|
|
mWebAuthnService->Reset();
|
|
}
|
|
}
|
|
|
|
mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestRegister(
|
|
const uint64_t& aTransactionId,
|
|
const WebAuthnMakeCredentialInfo& aTransactionInfo) {
|
|
::mozilla::ipc::AssertIsOnBackgroundThread();
|
|
|
|
if (!mWebAuthnService) {
|
|
mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1");
|
|
if (!mWebAuthnService) {
|
|
return IPC_FAIL_NO_REASON(this);
|
|
}
|
|
}
|
|
|
|
// If there's an ongoing transaction, abort it.
|
|
if (mTransactionId.isSome()) {
|
|
DisconnectTransaction();
|
|
Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR);
|
|
}
|
|
mTransactionId = Some(aTransactionId);
|
|
|
|
RefPtr<WebAuthnRegisterPromiseHolder> promiseHolder =
|
|
new WebAuthnRegisterPromiseHolder(GetCurrentSerialEventTarget());
|
|
|
|
PWebAuthnTransactionParent* parent = this;
|
|
RefPtr<WebAuthnRegisterPromise> promise = promiseHolder->Ensure();
|
|
promise
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[this, parent, aTransactionId,
|
|
inputClientData = aTransactionInfo.ClientDataJSON()](
|
|
const WebAuthnRegisterPromise::ResolveValueType& aValue) {
|
|
CompleteTransaction();
|
|
|
|
nsCString clientData;
|
|
nsresult rv = aValue->GetClientDataJSON(clientData);
|
|
if (rv == NS_ERROR_NOT_AVAILABLE) {
|
|
clientData = inputClientData;
|
|
} else if (NS_FAILED(rv)) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> attObj;
|
|
rv = aValue->GetAttestationObject(attObj);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> credentialId;
|
|
rv = aValue->GetCredentialId(credentialId);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<nsString> transports;
|
|
rv = aValue->GetTransports(transports);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
Maybe<nsString> authenticatorAttachment;
|
|
nsString maybeAuthenticatorAttachment;
|
|
rv = aValue->GetAuthenticatorAttachment(
|
|
maybeAuthenticatorAttachment);
|
|
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
authenticatorAttachment = Some(maybeAuthenticatorAttachment);
|
|
}
|
|
|
|
nsTArray<WebAuthnExtensionResult> extensions;
|
|
bool credPropsRk;
|
|
rv = aValue->GetCredPropsRk(&credPropsRk);
|
|
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
extensions.AppendElement(
|
|
WebAuthnExtensionResultCredProps(credPropsRk));
|
|
}
|
|
|
|
bool hmacCreateSecret;
|
|
rv = aValue->GetHmacCreateSecret(&hmacCreateSecret);
|
|
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
extensions.AppendElement(
|
|
WebAuthnExtensionResultHmacSecret(hmacCreateSecret));
|
|
}
|
|
|
|
WebAuthnMakeCredentialResult result(
|
|
clientData, attObj, credentialId, transports, extensions,
|
|
authenticatorAttachment);
|
|
|
|
Unused << parent->SendConfirmRegister(aTransactionId, result);
|
|
},
|
|
[this, parent, aTransactionId](
|
|
const WebAuthnRegisterPromise::RejectValueType aValue) {
|
|
CompleteTransaction();
|
|
Unused << parent->SendAbort(aTransactionId, aValue);
|
|
})
|
|
->Track(mRegisterPromiseRequest);
|
|
|
|
uint64_t browsingContextId = aTransactionInfo.BrowsingContextId();
|
|
RefPtr<WebAuthnRegisterArgs> args(new WebAuthnRegisterArgs(aTransactionInfo));
|
|
|
|
nsresult rv = mWebAuthnService->MakeCredential(
|
|
aTransactionId, browsingContextId, args, promiseHolder);
|
|
if (NS_FAILED(rv)) {
|
|
promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
}
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestSign(
|
|
const uint64_t& aTransactionId,
|
|
const WebAuthnGetAssertionInfo& aTransactionInfo) {
|
|
::mozilla::ipc::AssertIsOnBackgroundThread();
|
|
|
|
if (!mWebAuthnService) {
|
|
mWebAuthnService = do_GetService("@mozilla.org/webauthn/service;1");
|
|
if (!mWebAuthnService) {
|
|
return IPC_FAIL_NO_REASON(this);
|
|
}
|
|
}
|
|
|
|
if (mTransactionId.isSome()) {
|
|
DisconnectTransaction();
|
|
Unused << SendAbort(mTransactionId.ref(), NS_ERROR_DOM_ABORT_ERR);
|
|
}
|
|
mTransactionId = Some(aTransactionId);
|
|
|
|
RefPtr<WebAuthnSignPromiseHolder> promiseHolder =
|
|
new WebAuthnSignPromiseHolder(GetCurrentSerialEventTarget());
|
|
|
|
PWebAuthnTransactionParent* parent = this;
|
|
RefPtr<WebAuthnSignPromise> promise = promiseHolder->Ensure();
|
|
promise
|
|
->Then(
|
|
GetCurrentSerialEventTarget(), __func__,
|
|
[this, parent, aTransactionId,
|
|
inputClientData = aTransactionInfo.ClientDataJSON()](
|
|
const WebAuthnSignPromise::ResolveValueType& aValue) {
|
|
CompleteTransaction();
|
|
|
|
nsCString clientData;
|
|
nsresult rv = aValue->GetClientDataJSON(clientData);
|
|
if (rv == NS_ERROR_NOT_AVAILABLE) {
|
|
clientData = inputClientData;
|
|
} else if (NS_FAILED(rv)) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> credentialId;
|
|
rv = aValue->GetCredentialId(credentialId);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> signature;
|
|
rv = aValue->GetSignature(signature);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> authenticatorData;
|
|
rv = aValue->GetAuthenticatorData(authenticatorData);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
|
|
nsTArray<uint8_t> userHandle;
|
|
Unused << aValue->GetUserHandle(userHandle); // optional
|
|
|
|
Maybe<nsString> authenticatorAttachment;
|
|
nsString maybeAuthenticatorAttachment;
|
|
rv = aValue->GetAuthenticatorAttachment(
|
|
maybeAuthenticatorAttachment);
|
|
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
authenticatorAttachment = Some(maybeAuthenticatorAttachment);
|
|
}
|
|
|
|
nsTArray<WebAuthnExtensionResult> extensions;
|
|
bool usedAppId;
|
|
rv = aValue->GetUsedAppId(&usedAppId);
|
|
if (rv != NS_ERROR_NOT_AVAILABLE) {
|
|
if (NS_FAILED(rv)) {
|
|
Unused << parent->SendAbort(aTransactionId,
|
|
NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
return;
|
|
}
|
|
extensions.AppendElement(WebAuthnExtensionResultAppId(usedAppId));
|
|
}
|
|
|
|
WebAuthnGetAssertionResult result(
|
|
clientData, credentialId, signature, authenticatorData,
|
|
extensions, userHandle, authenticatorAttachment);
|
|
|
|
Unused << parent->SendConfirmSign(aTransactionId, result);
|
|
},
|
|
[this, parent,
|
|
aTransactionId](const WebAuthnSignPromise::RejectValueType aValue) {
|
|
CompleteTransaction();
|
|
Unused << parent->SendAbort(aTransactionId, aValue);
|
|
})
|
|
->Track(mSignPromiseRequest);
|
|
|
|
RefPtr<WebAuthnSignArgs> args(new WebAuthnSignArgs(aTransactionInfo));
|
|
|
|
nsresult rv = mWebAuthnService->GetAssertion(
|
|
aTransactionId, aTransactionInfo.BrowsingContextId(), args,
|
|
promiseHolder);
|
|
if (NS_FAILED(rv)) {
|
|
promiseHolder->Reject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
|
|
}
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestCancel(
|
|
const Tainted<uint64_t>& aTransactionId) {
|
|
::mozilla::ipc::AssertIsOnBackgroundThread();
|
|
|
|
if (mTransactionId.isNothing() ||
|
|
!MOZ_IS_VALID(aTransactionId, mTransactionId.ref() == aTransactionId)) {
|
|
return IPC_OK();
|
|
}
|
|
|
|
DisconnectTransaction();
|
|
return IPC_OK();
|
|
}
|
|
|
|
mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvRequestIsUVPAA(
|
|
RequestIsUVPAAResolver&& aResolver) {
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
// Try the nsIWebAuthnService. If we're configured for tests we
|
|
// will get a result. Otherwise we expect NS_ERROR_NOT_IMPLEMENTED.
|
|
nsCOMPtr<nsIWebAuthnService> service(
|
|
do_GetService("@mozilla.org/webauthn/service;1"));
|
|
bool available;
|
|
nsresult rv = service->GetIsUVPAA(&available);
|
|
if (NS_SUCCEEDED(rv)) {
|
|
aResolver(available);
|
|
return IPC_OK();
|
|
}
|
|
|
|
// Don't consult the platform API if resident key support is disabled.
|
|
if (!StaticPrefs::
|
|
security_webauthn_webauthn_enable_android_fido2_residentkey()) {
|
|
aResolver(false);
|
|
return IPC_OK();
|
|
}
|
|
|
|
// The GeckoView implementation of
|
|
// isUserVerifiyingPlatformAuthenticatorAvailable does not block, but we must
|
|
// call it on the main thread. It returns a MozPromise which we can ->Then to
|
|
// call aResolver on the IPDL background thread.
|
|
//
|
|
// Bug 1550788: there is an unnecessary layer of dispatching here: ipdl ->
|
|
// main -> a background thread. Other platforms just do ipdl -> a background
|
|
// thread.
|
|
nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
|
|
nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
|
|
__func__, [target, resolver = std::move(aResolver)]() {
|
|
auto result = java::WebAuthnTokenManager::
|
|
WebAuthnIsUserVerifyingPlatformAuthenticatorAvailable();
|
|
auto geckoResult = java::GeckoResult::LocalRef(std::move(result));
|
|
MozPromise<bool, bool, false>::FromGeckoResult(geckoResult)
|
|
->Then(
|
|
target, __func__,
|
|
[resolver](
|
|
const MozPromise<bool, bool, false>::ResolveOrRejectValue&
|
|
aValue) {
|
|
if (aValue.IsResolve()) {
|
|
resolver(aValue.ResolveValue());
|
|
} else {
|
|
resolver(false);
|
|
}
|
|
});
|
|
}));
|
|
NS_DispatchToMainThread(runnable.forget());
|
|
return IPC_OK();
|
|
|
|
#else
|
|
|
|
nsCOMPtr<nsISerialEventTarget> target = GetCurrentSerialEventTarget();
|
|
nsCOMPtr<nsIRunnable> runnable(NS_NewRunnableFunction(
|
|
__func__, [target, resolver = std::move(aResolver)]() {
|
|
bool available;
|
|
nsCOMPtr<nsIWebAuthnService> service(
|
|
do_GetService("@mozilla.org/webauthn/service;1"));
|
|
nsresult rv = service->GetIsUVPAA(&available);
|
|
if (NS_FAILED(rv)) {
|
|
available = false;
|
|
}
|
|
BoolPromise::CreateAndResolve(available, __func__)
|
|
->Then(target, __func__,
|
|
[resolver](const BoolPromise::ResolveOrRejectValue& value) {
|
|
if (value.IsResolve()) {
|
|
resolver(value.ResolveValue());
|
|
} else {
|
|
resolver(false);
|
|
}
|
|
});
|
|
}));
|
|
NS_DispatchBackgroundTask(runnable.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
|
|
return IPC_OK();
|
|
#endif
|
|
}
|
|
|
|
mozilla::ipc::IPCResult WebAuthnTransactionParent::RecvDestroyMe() {
|
|
::mozilla::ipc::AssertIsOnBackgroundThread();
|
|
|
|
// The child was disconnected from the WebAuthnManager instance and will send
|
|
// no further messages. It is kept alive until we delete it explicitly.
|
|
|
|
// The child should have cancelled any active transaction. This means
|
|
// we expect no more messages to the child. We'll crash otherwise.
|
|
|
|
// The IPC roundtrip is complete. No more messages, hopefully.
|
|
IProtocol* mgr = Manager();
|
|
if (!Send__delete__(this)) {
|
|
return IPC_FAIL_NO_REASON(mgr);
|
|
}
|
|
|
|
return IPC_OK();
|
|
}
|
|
|
|
void WebAuthnTransactionParent::ActorDestroy(ActorDestroyReason aWhy) {
|
|
::mozilla::ipc::AssertIsOnBackgroundThread();
|
|
|
|
// Called either by Send__delete__() in RecvDestroyMe() above, or when
|
|
// the channel disconnects. Ensure the token manager forgets about us.
|
|
|
|
if (mTransactionId.isSome()) {
|
|
DisconnectTransaction();
|
|
}
|
|
}
|
|
|
|
} // namespace mozilla::dom
|