/* 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/WebAuthnHandler.h"

#include "WebAuthnCoseIdentifiers.h"
#include "WebAuthnEnumStrings.h"
#include "WebAuthnTransportIdentifiers.h"
#include "hasht.h"
#include "mozilla/Base64.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/BounceTrackingProtection.h"
#include "mozilla/dom/AuthenticatorAssertionResponse.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
#include "mozilla/dom/PublicKeyCredential.h"
#include "mozilla/dom/WebAuthenticationBinding.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/dom/WindowGlobalChild.h"
#include "mozilla/glean/DomWebauthnMetrics.h"
#include "nsContentUtils.h"
#include "nsHTMLDocument.h"
#include "nsIURIMutator.h"
#include "nsThreadUtils.h"

#ifdef XP_WIN
#  include "WinWebAuthnService.h"
#endif

using namespace mozilla::ipc;

namespace mozilla::dom {

/***********************************************************************
 * Statics
 **********************************************************************/

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebAuthnHandler)
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTION(WebAuthnHandler, mWindow, mTransaction)

NS_IMPL_CYCLE_COLLECTING_ADDREF(WebAuthnHandler)
NS_IMPL_CYCLE_COLLECTING_RELEASE(WebAuthnHandler)

/***********************************************************************
 * Utility Functions
 **********************************************************************/

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) ||
               str.EqualsLiteral(MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_CABLE)) {
      transports |= MOZ_WEBAUTHN_AUTHENTICATOR_TRANSPORT_ID_HYBRID;
    }
  }
  return transports;
}

/***********************************************************************
 * WebAuthnHandler Implementation
 **********************************************************************/

WebAuthnHandler::~WebAuthnHandler() {
  MOZ_ASSERT(NS_IsMainThread());
  if (mActor) {
    if (mTransaction.isSome()) {
      CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
    }
    mActor->SetHandler(nullptr);
  }
}

bool WebAuthnHandler::MaybeCreateActor() {
  MOZ_ASSERT(NS_IsMainThread());

  if (mActor) {
    return true;
  }

  RefPtr<WebAuthnTransactionChild> actor = new WebAuthnTransactionChild();

  WindowGlobalChild* windowGlobalChild = mWindow->GetWindowGlobalChild();
  if (!windowGlobalChild ||
      !windowGlobalChild->SendPWebAuthnTransactionConstructor(actor)) {
    return false;
  }

  mActor = actor;
  mActor->SetHandler(this);

  return true;
}

void WebAuthnHandler::ActorDestroyed() {
  MOZ_ASSERT(NS_IsMainThread());
  mActor = nullptr;
}

void WebAuthnHandler::MakeCredential(
    JSContext* aCx, const PublicKeyCredentialCreationOptions& aOptions,
    const Optional<OwningNonNull<AbortSignal>>& aSignal,
    const RefPtr<Promise>& aPromise) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransaction.isSome()) {
    // abort the old transaction and take over control from here.
    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
  }

  if (!MaybeCreateActor()) {
    aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    return;
  }

  nsCOMPtr<Document> doc = mWindow->GetDoc();
  if (!IsWebAuthnAllowedInDocument(doc)) {
    aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();

  nsCString rpId;
  if (aOptions.mRp.mId.WasPassed()) {
    rpId = NS_ConvertUTF16toUTF8(aOptions.mRp.mId.Value());
  } else {
    nsresult rv = DefaultRpId(principal, rpId);
    if (NS_FAILED(rv)) {
      aPromise->MaybeReject(NS_ERROR_FAILURE);
      return;
    }
  }
  // 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) {
    aPromise->MaybeRejectWithTypeError("user.id is too long");
    return;
  }

  // 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 = WebAuthnTimeout(aOptions.mTimeout);

  // <https://w3c.github.io/webauthn/#sctn-appid-extension>
  if (aOptions.mExtensions.mAppid.WasPassed()) {
    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    return;
  }

  // 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()) {
    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    return;
  }

  // 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

  CryptoBuffer challenge;
  if (!challenge.Assign(aOptions.mChallenge)) {
    aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  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);
  }

  // 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));
    }
  }

  if (aOptions.mExtensions.mCredentialProtectionPolicy.WasPassed()) {
    bool enforceCredProtect = false;
    if (aOptions.mExtensions.mEnforceCredentialProtectionPolicy.WasPassed()) {
      enforceCredProtect =
          aOptions.mExtensions.mEnforceCredentialProtectionPolicy.Value();
    }
    extensions.AppendElement(WebAuthnExtensionCredProtect(
        aOptions.mExtensions.mCredentialProtectionPolicy.Value(),
        enforceCredProtect));
  }

  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));
    }
  }

  // <https://w3c.github.io/webauthn/#sctn-large-blob-extension>
  if (aOptions.mExtensions.mLargeBlob.WasPassed()) {
    if (aOptions.mExtensions.mLargeBlob.Value().mRead.WasPassed() ||
        aOptions.mExtensions.mLargeBlob.Value().mWrite.WasPassed()) {
      aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
      return;
    }
    Maybe<bool> supportRequired;
    const Optional<nsString>& largeBlobSupport =
        aOptions.mExtensions.mLargeBlob.Value().mSupport;
    if (largeBlobSupport.WasPassed()) {
      supportRequired.emplace(largeBlobSupport.Value().Equals(u"required"_ns));
    }
    nsTArray<uint8_t> write;  // unused
    extensions.AppendElement(
        WebAuthnExtensionLargeBlob(supportRequired, write));
  }

  // <https://w3c.github.io/webauthn/#prf-extension>
  if (aOptions.mExtensions.mPrf.WasPassed()) {
    const AuthenticationExtensionsPRFInputs& prf =
        aOptions.mExtensions.mPrf.Value();

    Maybe<WebAuthnExtensionPrfValues> eval = Nothing();
    if (prf.mEval.WasPassed()) {
      CryptoBuffer first;
      first.Assign(prf.mEval.Value().mFirst);
      const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed();
      CryptoBuffer second;
      if (secondMaybe) {
        second.Assign(prf.mEval.Value().mSecond.Value());
      }
      eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second));
    }

    const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed();
    nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential;
    if (evalByCredentialMaybe) {
      // evalByCredential is only allowed in GetAssertion.
      // https://w3c.github.io/webauthn/#prf-extension
      aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
      return;
    }

    extensions.AppendElement(
        WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential));
  }

  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);

  // Abort the request if aborted flag is already set.
  if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
    JS::Rooted<JS::Value> reason(aCx);
    aSignal.Value().GetReason(aCx, &reason);
    aPromise->MaybeReject(reason);
    return;
  }

  nsString json;
  nsresult rv = SerializeWebAuthnCreationOptions(
      aCx, NS_ConvertUTF8toUTF16(rpId), aOptions, json);
  if (NS_FAILED(rv)) {
    aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    return;
  }

  WebAuthnMakeCredentialInfo info(
      rpId, challenge, adjustedTimeout, excludeList, rpInfo, userInfo,
      coseAlgos, extensions, authSelection, attestation, aOptions.mHints, json);

  // 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(aPromise, WebAuthnTransactionType::Create));
  mActor->SendRequestRegister(info)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr{this}](
              const PWebAuthnTransactionChild::RequestRegisterPromise::
                  ResolveOrRejectValue& aValue) {
            self->mTransaction.ref().mRegisterHolder.Complete();
            if (aValue.IsResolve() && aValue.ResolveValue().type() ==
                                          WebAuthnMakeCredentialResponse::Type::
                                              TWebAuthnMakeCredentialResult) {
              self->FinishMakeCredential(aValue.ResolveValue());
            } else if (aValue.IsResolve()) {
              self->RejectTransaction(aValue.ResolveValue());
            } else {
              self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
            }
          })
      ->Track(mTransaction.ref().mRegisterHolder);
}

void WebAuthnHandler::GetAssertion(
    JSContext* aCx, const PublicKeyCredentialRequestOptions& aOptions,
    const bool aConditionallyMediated,
    const Optional<OwningNonNull<AbortSignal>>& aSignal,
    const RefPtr<Promise>& aPromise) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransaction.isSome()) {
    // abort the old transaction and take over control from here.
    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
  }

  if (!MaybeCreateActor()) {
    aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    return;
  }

  nsCOMPtr<Document> doc = mWindow->GetDoc();
  if (!IsWebAuthnAllowedInDocument(doc)) {
    aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();

  nsCString rpId;
  if (aOptions.mRpId.WasPassed()) {
    rpId = NS_ConvertUTF16toUTF8(aOptions.mRpId.Value());
  } else {
    nsresult rv = DefaultRpId(principal, rpId);
    if (NS_FAILED(rv)) {
      aPromise->MaybeReject(NS_ERROR_FAILURE);
      return;
    }
  }
  // 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 = WebAuthnTimeout(aOptions.mTimeout);

  // Abort the request if the allowCredentials set is too large
  if (aOptions.mAllowCredentials.Length() > kWebAuthnMaxAllowedCredentials) {
    aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  CryptoBuffer challenge;
  if (!challenge.Assign(aOptions.mChallenge)) {
    aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return;
  }

  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) {
    aPromise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return;
  }

  // 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()) {
    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    return;
  }

  // minPinLength is only supported in MakeCredentials
  if (aOptions.mExtensions.mMinPinLength.WasPassed()) {
    aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    return;
  }

  // <https://w3c.github.io/webauthn/#sctn-appid-extension>
  Maybe<nsCString> maybeAppId;
  if (aOptions.mExtensions.mAppid.WasPassed()) {
    nsCString appId(NS_ConvertUTF16toUTF8(aOptions.mExtensions.mAppid.Value()));

    // Step 2 of Algorithm 3.1.2 of
    // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-appid-and-facets-v2.0-id-20180227.html#determining-if-a-caller-s-facetid-is-authorized-for-an-appid
    if (appId.IsEmpty() || appId.EqualsLiteral("null")) {
      auto* basePrin = BasePrincipal::Cast(principal);
      nsresult rv = basePrin->GetWebExposedOriginSerialization(appId);
      if (NS_FAILED(rv)) {
        aPromise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
        return;
      }
    }

    maybeAppId.emplace(std::move(appId));
  }

  // <https://w3c.github.io/webauthn/#sctn-large-blob-extension>
  if (aOptions.mExtensions.mLargeBlob.WasPassed()) {
    const AuthenticationExtensionsLargeBlobInputs& extLargeBlob =
        aOptions.mExtensions.mLargeBlob.Value();
    if (extLargeBlob.mSupport.WasPassed() ||
        (extLargeBlob.mRead.WasPassed() && extLargeBlob.mWrite.WasPassed()) ||
        (extLargeBlob.mWrite.WasPassed() &&
         aOptions.mAllowCredentials.Length() != 1)) {
      aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
      return;
    }
    Maybe<bool> read = Nothing();
    if (extLargeBlob.mRead.WasPassed() && extLargeBlob.mRead.Value()) {
      read.emplace(true);
    }

    CryptoBuffer write;
    if (extLargeBlob.mWrite.WasPassed()) {
      read.emplace(false);
      write.Assign(extLargeBlob.mWrite.Value());
    }
    extensions.AppendElement(WebAuthnExtensionLargeBlob(read, write));
  }

  // <https://w3c.github.io/webauthn/#prf-extension>
  if (aOptions.mExtensions.mPrf.WasPassed()) {
    const AuthenticationExtensionsPRFInputs& prf =
        aOptions.mExtensions.mPrf.Value();

    Maybe<WebAuthnExtensionPrfValues> eval = Nothing();
    if (prf.mEval.WasPassed()) {
      CryptoBuffer first;
      first.Assign(prf.mEval.Value().mFirst);
      const bool secondMaybe = prf.mEval.Value().mSecond.WasPassed();
      CryptoBuffer second;
      if (secondMaybe) {
        second.Assign(prf.mEval.Value().mSecond.Value());
      }
      eval = Some(WebAuthnExtensionPrfValues(first, secondMaybe, second));
    }

    const bool evalByCredentialMaybe = prf.mEvalByCredential.WasPassed();
    nsTArray<WebAuthnExtensionPrfEvalByCredentialEntry> evalByCredential;
    if (evalByCredentialMaybe) {
      if (allowList.Length() == 0) {
        aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
        return;
      }

      for (const auto& entry : prf.mEvalByCredential.Value().Entries()) {
        FallibleTArray<uint8_t> evalByCredentialEntryId;
        nsresult rv = Base64URLDecode(NS_ConvertUTF16toUTF8(entry.mKey),
                                      Base64URLDecodePaddingPolicy::Ignore,
                                      evalByCredentialEntryId);
        if (NS_FAILED(rv)) {
          aPromise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR);
          return;
        }

        bool foundMatchingAllowListEntry = false;
        for (const auto& cred : allowList) {
          if (evalByCredentialEntryId == cred.id()) {
            foundMatchingAllowListEntry = true;
          }
        }
        if (!foundMatchingAllowListEntry) {
          aPromise->MaybeReject(NS_ERROR_DOM_SYNTAX_ERR);
          return;
        }

        CryptoBuffer first;
        first.Assign(entry.mValue.mFirst);
        const bool secondMaybe = entry.mValue.mSecond.WasPassed();
        CryptoBuffer second;
        if (secondMaybe) {
          second.Assign(entry.mValue.mSecond.Value());
        }
        evalByCredential.AppendElement(
            WebAuthnExtensionPrfEvalByCredentialEntry(
                evalByCredentialEntryId,
                WebAuthnExtensionPrfValues(first, secondMaybe, second)));
      }
    }

    extensions.AppendElement(
        WebAuthnExtensionPrf(eval, evalByCredentialMaybe, evalByCredential));
  }

  // Abort the request if aborted flag is already set.
  if (aSignal.WasPassed() && aSignal.Value().Aborted()) {
    JS::Rooted<JS::Value> reason(aCx);
    aSignal.Value().GetReason(aCx, &reason);
    aPromise->MaybeReject(reason);
    return;
  }

  nsString json;
  nsresult rv = SerializeWebAuthnRequestOptions(
      aCx, NS_ConvertUTF8toUTF16(rpId), aOptions, json);
  if (NS_FAILED(rv)) {
    aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    return;
  }

  WebAuthnGetAssertionInfo info(rpId, maybeAppId, challenge, adjustedTimeout,
                                allowList, extensions,
                                aOptions.mUserVerification,
                                aConditionallyMediated, aOptions.mHints, json);

  // 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(aPromise, WebAuthnTransactionType::Get));
  mActor->SendRequestSign(info)
      ->Then(
          GetCurrentSerialEventTarget(), __func__,
          [self = RefPtr{this}](
              const PWebAuthnTransactionChild::RequestSignPromise::
                  ResolveOrRejectValue& aValue) {
            self->mTransaction.ref().mSignHolder.Complete();
            if (aValue.IsResolve() && aValue.ResolveValue().type() ==
                                          WebAuthnGetAssertionResponse::Type::
                                              TWebAuthnGetAssertionResult) {
              self->FinishGetAssertion(aValue.ResolveValue());
            } else if (aValue.IsResolve()) {
              self->RejectTransaction(aValue.ResolveValue());
            } else {
              self->RejectTransaction(NS_ERROR_DOM_NOT_ALLOWED_ERR);
            }
          })
      ->Track(mTransaction.ref().mSignHolder);
}

void WebAuthnHandler::Store(const Credential& aCredential,
                            const RefPtr<Promise>& aPromise) {
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransaction.isSome()) {
    // abort the old transaction and take over control from here.
    CancelTransaction(NS_ERROR_DOM_ABORT_ERR);
  }

  aPromise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
}

void WebAuthnHandler::IsUVPAA(const RefPtr<Promise>& aPromise) {
  if (!MaybeCreateActor()) {
    aPromise->MaybeReject(NS_ERROR_DOM_OPERATION_ERR);
    return;
  }

  mActor->SendRequestIsUVPAA()->Then(
      GetCurrentSerialEventTarget(), __func__,
      [promise = RefPtr{aPromise}](
          const PWebAuthnTransactionChild::RequestIsUVPAAPromise::
              ResolveOrRejectValue& aValue) {
        if (aValue.IsResolve()) {
          promise->MaybeResolve(aValue.ResolveValue());
        } else {
          promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
        }
      });
}

void WebAuthnHandler::FinishMakeCredential(
    const WebAuthnMakeCredentialResult& aResult) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mTransaction.isSome());

  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(mWindow);
  attestation->SetClientDataJSON(aResult.ClientDataJSON());
  attestation->SetAttestationObject(aResult.AttestationObject());
  attestation->SetTransports(aResult.Transports());

  RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow);
  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);
    }
    if (ext.type() ==
        WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) {
      credential->InitClientExtensionResultLargeBlob();
      credential->SetClientExtensionResultLargeBlobSupported(
          ext.get_WebAuthnExtensionResultLargeBlob().flag());
    }
    if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) {
      credential->InitClientExtensionResultPrf();
      const Maybe<bool> prfEnabled =
          ext.get_WebAuthnExtensionResultPrf().enabled();
      if (prfEnabled.isSome()) {
        credential->SetClientExtensionResultPrfEnabled(prfEnabled.value());
      }
      const Maybe<WebAuthnExtensionPrfValues> prfValues =
          ext.get_WebAuthnExtensionResultPrf().results();
      if (prfValues.isSome()) {
        credential->SetClientExtensionResultPrfResultsFirst(
            prfValues.value().first());
        if (prfValues.value().secondMaybe()) {
          credential->SetClientExtensionResultPrfResultsSecond(
              prfValues.value().second());
        }
      }
    }
  }

  ResolveTransaction(credential);
}

void WebAuthnHandler::FinishGetAssertion(
    const WebAuthnGetAssertionResult& aResult) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(mTransaction.isSome());

  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(mWindow);
  assertion->SetClientDataJSON(aResult.ClientDataJSON());
  assertion->SetAuthenticatorData(aResult.AuthenticatorData());
  assertion->SetSignature(aResult.Signature());
  assertion->SetUserHandle(aResult.UserHandle());  // may be empty

  RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mWindow);
  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);
    }
    if (ext.type() ==
        WebAuthnExtensionResult::TWebAuthnExtensionResultLargeBlob) {
      if (ext.get_WebAuthnExtensionResultLargeBlob().flag() &&
          ext.get_WebAuthnExtensionResultLargeBlob().written()) {
        // Signal a read failure by including an empty largeBlob extension.
        credential->InitClientExtensionResultLargeBlob();
      } else if (ext.get_WebAuthnExtensionResultLargeBlob().flag()) {
        const nsTArray<uint8_t>& largeBlobValue =
            ext.get_WebAuthnExtensionResultLargeBlob().blob();
        credential->InitClientExtensionResultLargeBlob();
        credential->SetClientExtensionResultLargeBlobValue(largeBlobValue);
      } else {
        bool largeBlobWritten =
            ext.get_WebAuthnExtensionResultLargeBlob().written();
        credential->InitClientExtensionResultLargeBlob();
        credential->SetClientExtensionResultLargeBlobWritten(largeBlobWritten);
      }
    }
    if (ext.type() == WebAuthnExtensionResult::TWebAuthnExtensionResultPrf) {
      credential->InitClientExtensionResultPrf();
      Maybe<WebAuthnExtensionPrfValues> prfResults =
          ext.get_WebAuthnExtensionResultPrf().results();
      if (prfResults.isSome()) {
        credential->SetClientExtensionResultPrfResultsFirst(
            prfResults.value().first());
        if (prfResults.value().secondMaybe()) {
          credential->SetClientExtensionResultPrfResultsSecond(
              prfResults.value().second());
        }
      }
    }
  }

  // Treat successful assertion as user activation for BounceTrackingProtection.
  nsIGlobalObject* global = mTransaction.ref().mPromise->GetGlobalObject();
  if (global) {
    nsPIDOMWindowInner* window = global->GetAsInnerWindow();
    if (window) {
      (void)BounceTrackingProtection::RecordUserActivation(
          window->GetWindowContext());
    }
  }

  ResolveTransaction(credential);
}

void WebAuthnHandler::RunAbortAlgorithm() {
  if (NS_WARN_IF(mTransaction.isNothing())) {
    return;
  }

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);

  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 WebAuthnHandler::ResolveTransaction(
    const RefPtr<PublicKeyCredential>& aCredential) {
  MOZ_ASSERT(mTransaction.isSome());

  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;
  }

  // Bug 1969341 - we need to reset the transaction before resolving the
  // promise. This lets us handle the case where resolving the promise initiates
  // a new WebAuthn request.
  RefPtr<Promise> promise = mTransaction.ref().mPromise;
  mTransaction.reset();
  Unfollow();

  promise->MaybeResolve(aCredential);
}

template <typename T>
void WebAuthnHandler::RejectTransaction(const T& aReason) {
  MOZ_ASSERT(mTransaction.isSome());

  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;
  }

  // Bug 1969341 - we need to reset the transaction before rejecting the
  // promise. This lets us handle the case where rejecting the promise initiates
  // a new WebAuthn request.
  RefPtr<Promise> promise = mTransaction.ref().mPromise;
  mTransaction.reset();
  Unfollow();

  promise->MaybeReject(aReason);
}

}  // namespace mozilla::dom
