Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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,
#include "hasht.h"
#include "nsHTMLDocument.h"
#include "nsIURIMutator.h"
#include "nsThreadUtils.h"
#include "WebAuthnCoseIdentifiers.h"
#include "WebAuthnEnumStrings.h"
#include "WebAuthnTransportIdentifiers.h"
#include "mozilla/Base64.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/BounceTrackingProtection.h"
#include "mozilla/glean/GleanMetrics.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/PWebAuthnTransactionChild.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"
#include "mozilla/JSONStringWriteFuncs.h"
#include "mozilla/JSONWriter.h"
#ifdef XP_WIN
# include "WinWebAuthnService.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 nsACString& aType,
const AuthenticationExtensionsClientInputs& aExtensions,
/* out */ nsACString& aJsonOut) {
MOZ_ASSERT(NS_IsMainThread());
nsAutoCString challengeBase64;
nsresult rv =
Base64URLEncode(aChallenge.Length(), aChallenge.Elements(),
Base64URLEncodePaddingPolicy::Omit, challengeBase64);
if (NS_WARN_IF(NS_FAILED(rv))) {
return NS_ERROR_FAILURE;
}
// Serialize the collected client data using the algorithm from
// Please update the definition of CollectedClientData in
// dom/webidl/WebAuthentication.webidl when changes are made here.
JSONStringRefWriteFunc f(aJsonOut);
JSONWriter w(f, JSONWriter::CollectionStyle::SingleLineStyle);
w.Start();
// Steps 2 and 3
w.StringProperty("type", aType);
// Steps 4 and 5
w.StringProperty("challenge", challengeBase64);
// Steps 6 and 7
w.StringProperty("origin", NS_ConvertUTF16toUTF8(aOrigin));
w.End();
return NS_OK;
}
static uint8_t SerializeTransports(
const mozilla::dom::Sequence<nsString>& aTransports) {
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 == 3);
for (const nsAString& str : aTransports) {
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;
} else if (str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_HYBRID)) {
transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID;
}
}
return transports;
}
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::GetWebExposedOriginSerialization(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->IsHTMLOrXHTML()) {
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() {
mTransaction.reset();
Unfollow();
}
void WebAuthnManager::CancelParent() {
if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
mChild->SendRequestCancel(mTransaction.ref().mId);
}
}
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()) {
// abort the old transaction and take over control from here.
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
}
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();
}
}
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, "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;
if (s.mTransports.WasPassed()) {
c.transports() = SerializeTransports(s.mTransports.Value());
}
excludeList.AppendElement(c);
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// TODO: Add extension list building
nsTArray<WebAuthnExtension> extensions;
if (aOptions.mExtensions.mHmacCreateSecret.WasPassed()) {
bool hmacCreateSecret = aOptions.mExtensions.mHmacCreateSecret.Value();
if (hmacCreateSecret) {
extensions.AppendElement(WebAuthnExtensionHmacSecret(hmacCreateSecret));
}
}
if (aOptions.mExtensions.mCredProps.WasPassed()) {
bool credProps = aOptions.mExtensions.mCredProps.Value();
if (credProps) {
extensions.AppendElement(WebAuthnExtensionCredProps(credProps));
}
}
if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
bool minPinLength = aOptions.mExtensions.mMinPinLength.Value();
if (minPinLength) {
extensions.AppendElement(WebAuthnExtensionMinPinLength(minPinLength));
}
}
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 == 3);
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);
WebAuthnMakeCredentialRpInfo rpInfo(aOptions.mRp.mName);
WebAuthnMakeCredentialUserInfo userInfo(userId, aOptions.mUser.mName,
aOptions.mUser.mDisplayName);
BrowsingContext* context = mParent->GetBrowsingContext();
if (!context) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> reason(cx);
aSignal.Value().GetReason(cx, &reason);
promise->MaybeReject(reason);
return promise.forget();
}
WebAuthnMakeCredentialInfo info(
origin, NS_ConvertUTF8toUTF16(rpId), challenge, clientDataJSON,
adjustedTimeout, excludeList, rpInfo, userInfo, coseAlgos, extensions,
authSelection, attestation, context->Top()->Id());
// Set up the transaction state. Fallible operations should not be performed
// below this line, as we must not leave the transaction state partially
// initialized. Once the transaction state is initialized the only valid ways
// to end the transaction are CancelTransaction, RejectTransaction, and
// FinishMakeCredential.
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction =
Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Create));
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 bool aConditionallyMediated,
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()) {
// abort the old transaction and take over control from here.
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
}
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();
}
}
// 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, "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;
if (s.mTransports.WasPassed()) {
c.transports() = SerializeTransports(s.mTransports.Value());
}
allowList.AppendElement(c);
}
}
if (allowList.Length() == 0 && aOptions.mAllowCredentials.Length() != 0) {
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
return promise.forget();
}
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;
// credProps is only supported in MakeCredentials
if (aOptions.mExtensions.mCredProps.WasPassed()) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
// minPinLength is only supported in MakeCredentials
if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
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();
}
// Append the hash and send it to the backend.
extensions.AppendElement(WebAuthnExtensionAppId(appId));
}
BrowsingContext* context = mParent->GetBrowsingContext();
if (!context) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
// Abort the request if aborted flag is already set.
if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR);
return promise.forget();
}
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> reason(cx);
aSignal.Value().GetReason(cx, &reason);
promise->MaybeReject(reason);
return promise.forget();
}
WebAuthnGetAssertionInfo info(origin, NS_ConvertUTF8toUTF16(rpId), challenge,
clientDataJSON, adjustedTimeout, allowList,
extensions, aOptions.mUserVerification,
aConditionallyMediated, context->Top()->Id());
// Set up the transaction state. Fallible operations should not be performed
// below this line, as we must not leave the transaction state partially
// initialized. Once the transaction state is initialized the only valid ways
// to end the transaction are CancelTransaction, RejectTransaction, and
// FinishGetAssertion.
AbortSignal* signal = nullptr;
if (aSignal.WasPassed()) {
signal = &aSignal.Value();
Follow(signal);
}
MOZ_ASSERT(mTransaction.isNothing());
mTransaction =
Some(WebAuthnTransaction(promise, WebAuthnTransactionType::Get));
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()) {
// abort the old transaction and take over control from here.
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
}
promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
return promise.forget();
}
already_AddRefed<Promise> WebAuthnManager::IsUVPAA(GlobalObject& aGlobal,
ErrorResult& aError) {
RefPtr<Promise> promise =
Promise::Create(xpc::CurrentNativeGlobal(aGlobal.Context()), aError);
if (aError.Failed()) {
return nullptr;
}
if (!MaybeCreateBackgroundActor()) {
promise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
return promise.forget();
}
mChild->SendRequestIsUVPAA()->Then(
GetCurrentSerialEventTarget(), __func__,
[promise](const PWebAuthnTransactionChild::RequestIsUVPAAPromise::
ResolveOrRejectValue& aValue) {
if (aValue.IsResolve()) {
promise->MaybeResolve(aValue.ResolveValue());
} else {
promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_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;
}
nsAutoCString keyHandleBase64Url;
nsresult rv = Base64URLEncode(
aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
Base64URLEncodePaddingPolicy::Omit, 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(aResult.ClientDataJSON());
attestation->SetAttestationObject(aResult.AttestationObject());
attestation->SetTransports(aResult.Transports());
RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetAttestationResponse(attestation);
if (aResult.AuthenticatorAttachment().isSome()) {
credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
mozilla::glean::webauthn_create::authenticator_attachment
.Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref()))
.Add(1);
} else {
mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns)
.Add(1);
}
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {
if (ext.type() ==
WebAuthnExtensionResult::TWebAuthnExtensionResultCredProps) {
bool credPropsRk = ext.get_WebAuthnExtensionResultCredProps().rk();
credential->SetClientExtensionResultCredPropsRk(credPropsRk);
if (credPropsRk) {
mozilla::glean::webauthn_create::passkey.Add(1);
}
}
if (ext.type() ==
WebAuthnExtensionResult::TWebAuthnExtensionResultHmacSecret) {
bool hmacCreateSecret =
ext.get_WebAuthnExtensionResultHmacSecret().hmacCreateSecret();
credential->SetClientExtensionResultHmacSecret(hmacCreateSecret);
}
}
ResolveTransaction(credential);
}
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;
}
nsAutoCString keyHandleBase64Url;
nsresult rv = Base64URLEncode(
aResult.KeyHandle().Length(), aResult.KeyHandle().Elements(),
Base64URLEncodePaddingPolicy::Omit, keyHandleBase64Url);
if (NS_WARN_IF(NS_FAILED(rv))) {
RejectTransaction(rv);
return;
}
// 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(aResult.ClientDataJSON());
assertion->SetAuthenticatorData(aResult.AuthenticatorData());
assertion->SetSignature(aResult.Signature());
assertion->SetUserHandle(aResult.UserHandle()); // may be empty
RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mParent);
credential->SetId(NS_ConvertASCIItoUTF16(keyHandleBase64Url));
credential->SetType(u"public-key"_ns);
credential->SetRawId(aResult.KeyHandle());
credential->SetAssertionResponse(assertion);
if (aResult.AuthenticatorAttachment().isSome()) {
credential->SetAuthenticatorAttachment(aResult.AuthenticatorAttachment());
mozilla::glean::webauthn_get::authenticator_attachment
.Get(NS_ConvertUTF16toUTF8(aResult.AuthenticatorAttachment().ref()))
.Add(1);
} else {
mozilla::glean::webauthn_get::authenticator_attachment.Get("unknown"_ns)
.Add(1);
}
// Forward client extension results.
for (const auto& ext : aResult.Extensions()) {
if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultAppId) {
bool appid = ext.get_WebAuthnExtensionResultAppId().AppId();
credential->SetClientExtensionResultAppId(appid);
}
}
// Treat successful assertion as user activation for BounceTrackingProtection.
nsIGlobalObject* global = mTransaction.ref().mPromise->GetGlobalObject();
if (global) {
nsPIDOMWindowInner* window = global->GetAsInnerWindow();
if (window) {
Unused << BounceTrackingProtection::RecordUserActivation(
window->GetWindowContext());
}
}
ResolveTransaction(credential);
}
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() {
if (NS_WARN_IF(mTransaction.isNothing())) {
return;
}
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mParent);
AutoJSAPI jsapi;
if (!jsapi.Init(global)) {
CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
return;
}
JSContext* cx = jsapi.cx();
JS::Rooted<JS::Value> reason(cx);
Signal()->GetReason(cx, &reason);
CancelTransaction(reason);
}
void WebAuthnManager::ResolveTransaction(
const RefPtr<PublicKeyCredential>& aCredential) {
if (NS_WARN_IF(mTransaction.isNothing())) {
ClearTransaction();
return;
}
switch (mTransaction.ref().mType) {
case WebAuthnTransactionType::Create:
mozilla::glean::webauthn_create::success.Add(1);
break;
case WebAuthnTransactionType::Get:
mozilla::glean::webauthn_get::success.Add(1);
break;
}
mTransaction.ref().mPromise->MaybeResolve(aCredential);
ClearTransaction();
}
template <typename T>
void WebAuthnManager::RejectTransaction(const T& aReason) {
if (NS_WARN_IF(mTransaction.isNothing())) {
ClearTransaction();
return;
}
switch (mTransaction.ref().mType) {
case WebAuthnTransactionType::Create:
mozilla::glean::webauthn_create::failure.Add(1);
break;
case WebAuthnTransactionType::Get:
mozilla::glean::webauthn_get::failure.Add(1);
break;
}
mTransaction.ref().mPromise->MaybeReject(aReason);
ClearTransaction();
}
} // namespace mozilla::dom