/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "hasht.h"
#include "nsICryptoHash.h"
#include "nsNetCID.h"
#include "nsThreadUtils.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/AuthenticatorAttestationResponse.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
#include "mozilla/dom/U2FUtil.h"
#include "mozilla/dom/WebAuthnCBORUtil.h"
#include "mozilla/dom/WebAuthnManager.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"

using namespace mozilla::ipc;

namespace mozilla {
namespace dom {

/***********************************************************************
 * Protocol Constants
 **********************************************************************/

const uint8_t FLAG_TUP = 0x01; // Test of User Presence required
const uint8_t FLAG_AT = 0x40; // Authenticator Data is provided

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

namespace {
StaticRefPtr<WebAuthnManager> gWebAuthnManager;
static mozilla::LazyLogModule gWebAuthnManagerLog("webauthnmanager");
}

NS_NAMED_LITERAL_STRING(kVisibilityChange, "visibilitychange");

NS_IMPL_ISUPPORTS(WebAuthnManager, nsIIPCBackgroundChildCreateCallback,
                  nsIDOMEventListener);

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

template<class OOS>
static nsresult
GetAlgorithmName(const OOS& aAlgorithm,
                 /* out */ nsString& aName)
{
  if (aAlgorithm.IsString()) {
    // If string, then treat as algorithm name
    aName.Assign(aAlgorithm.GetAsString());
  } else {
    // TODO: Coerce to string and extract name. See WebCryptoTask.cpp
  }

  // Only ES256 is currently supported
  if (NORMALIZED_EQUALS(aName, JWK_ALG_ECDSA_P_256)) {
    aName.AssignLiteral(JWK_ALG_ECDSA_P_256);
  } else {
    return NS_ERROR_DOM_SYNTAX_ERR;
  }

  return NS_OK;
}

static nsresult
AssembleClientData(const nsAString& aOrigin, const CryptoBuffer& aChallenge,
                   /* out */ nsACString& aJsonOut)
{
  MOZ_ASSERT(NS_IsMainThread());

  nsString challengeBase64;
  nsresult rv = aChallenge.ToJwkBase64(challengeBase64);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return NS_ERROR_FAILURE;
  }

  CollectedClientData clientDataObject;
  clientDataObject.mChallenge.Assign(challengeBase64);
  clientDataObject.mOrigin.Assign(aOrigin);
  clientDataObject.mHashAlg.Assign(NS_LITERAL_STRING("SHA-256"));

  nsAutoString temp;
  if (NS_WARN_IF(!clientDataObject.ToJSON(temp))) {
    return NS_ERROR_FAILURE;
  }

  aJsonOut.Assign(NS_ConvertUTF16toUTF8(temp));
  return NS_OK;
}

nsresult
GetOrigin(nsPIDOMWindowInner* aParent,
          /*out*/ nsAString& aOrigin, /*out*/ nsACString& aHost)
{
  MOZ_ASSERT(aParent);
  nsCOMPtr<nsIDocument> doc = aParent->GetDoc();
  MOZ_ASSERT(doc);

  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
  nsresult rv = nsContentUtils::GetUTFOrigin(principal, aOrigin);
  if (NS_WARN_IF(NS_FAILED(rv)) ||
      NS_WARN_IF(aOrigin.IsEmpty())) {
    return NS_ERROR_FAILURE;
  }

  if (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;
  if (NS_FAILED(principal->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<nsIDocument> doc = aParent->GetDoc();
  MOZ_ASSERT(doc);

  nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
  nsCOMPtr<nsIURI> uri;
  if (NS_FAILED(principal->GetURI(getter_AddRefs(uri)))) {
    return NS_ERROR_FAILURE;
  }
  nsAutoCString originHost;
  if (NS_FAILED(uri->GetAsciiHost(originHost))) {
    return NS_ERROR_FAILURE;
  }
  nsCOMPtr<nsIDocument> document = aParent->GetDoc();
  if (!document || !document->IsHTMLDocument()) {
    return NS_ERROR_FAILURE;
  }
  nsHTMLDocument* html = document->AsHTMLDocument();
  if (NS_WARN_IF(!html)) {
    return NS_ERROR_FAILURE;
  }

  if (!html->IsRegistrableDomainSuffixOfOrEqualTo(aInputRpId, originHost)) {
    return NS_ERROR_DOM_SECURITY_ERR;
  }

  aRelaxedRpId.Assign(NS_ConvertUTF16toUTF8(aInputRpId));
  return NS_OK;
}

static void
ListenForVisibilityEvents(nsPIDOMWindowInner* aParent,
                          WebAuthnManager* aListener)
{
  MOZ_ASSERT(aParent);
  MOZ_ASSERT(aListener);

  nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    return;
  }

  nsresult rv = doc->AddSystemEventListener(kVisibilityChange, aListener,
                                            /* use capture */ true,
                                            /* wants untrusted */ false);
  Unused << NS_WARN_IF(NS_FAILED(rv));
}

static void
StopListeningForVisibilityEvents(nsPIDOMWindowInner* aParent,
                                 WebAuthnManager* aListener)
{
  MOZ_ASSERT(aParent);
  MOZ_ASSERT(aListener);

  nsCOMPtr<nsIDocument> doc = aParent->GetExtantDoc();
  if (NS_WARN_IF(!doc)) {
    return;
  }

  nsresult rv = doc->RemoveSystemEventListener(kVisibilityChange, aListener,
                                               /* use capture */ true);
  Unused << NS_WARN_IF(NS_FAILED(rv));
}

/***********************************************************************
 * WebAuthnManager Implementation
 **********************************************************************/

WebAuthnManager::WebAuthnManager()
{
  MOZ_ASSERT(NS_IsMainThread());
}

void
WebAuthnManager::MaybeClearTransaction()
{
  mClientData.reset();
  mInfo.reset();
  mTransactionPromise = nullptr;
  if (mCurrentParent) {
    StopListeningForVisibilityEvents(mCurrentParent, this);
    mCurrentParent = nullptr;
  }

  if (mChild) {
    RefPtr<WebAuthnTransactionChild> c;
    mChild.swap(c);
    c->Send__delete__(c);
  }
}

WebAuthnManager::~WebAuthnManager()
{
  MaybeClearTransaction();
}

RefPtr<WebAuthnManager::BackgroundActorPromise>
WebAuthnManager::GetOrCreateBackgroundActor()
{
  MOZ_ASSERT(NS_IsMainThread());

  PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
  RefPtr<WebAuthnManager::BackgroundActorPromise> promise =
    mPBackgroundCreationPromise.Ensure(__func__);

  if (actor) {
    ActorCreated(actor);
  } else {
    bool ok = BackgroundChild::GetOrCreateForCurrentThread(this);
    if (NS_WARN_IF(!ok)) {
      ActorFailed();
    }
  }

  return promise;
}

//static
WebAuthnManager*
WebAuthnManager::GetOrCreate()
{
  MOZ_ASSERT(NS_IsMainThread());
  if (gWebAuthnManager) {
    return gWebAuthnManager;
  }

  gWebAuthnManager = new WebAuthnManager();
  ClearOnShutdown(&gWebAuthnManager);
  return gWebAuthnManager;
}

//static
WebAuthnManager*
WebAuthnManager::Get()
{
  MOZ_ASSERT(NS_IsMainThread());
  return gWebAuthnManager;
}

already_AddRefed<Promise>
WebAuthnManager::MakeCredential(nsPIDOMWindowInner* aParent,
                                const MakeCredentialOptions& aOptions)
{
  MOZ_ASSERT(aParent);

  MaybeClearTransaction();

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);

  ErrorResult rv;
  RefPtr<Promise> promise = Promise::Create(global, rv);
  if(rv.Failed()) {
    return nullptr;
  }

  nsString origin;
  nsCString rpId;
  rv = GetOrigin(aParent, origin, rpId);
  if (NS_WARN_IF(rv.Failed())) {
    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.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(aParent, aOptions.mRp.mId.Value(), rpId))) {
      promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
      return promise.forget();
    }
  }

  CryptoBuffer rpIdHash;
  if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
    promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
    return promise.forget();
  }

  nsresult srv;
  nsCOMPtr<nsICryptoHash> hashService =
    do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  srv = HashCString(hashService, rpId, rpIdHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  // Process each element of cryptoParameters using the following steps, to
  // produce a new sequence normalizedParameters.
  nsTArray<PublicKeyCredentialParameters> normalizedParams;
  for (size_t a = 0; a < aOptions.mParameters.Length(); ++a) {
    // Let current be the currently selected element of
    // cryptoParameters.

    // If current.type does not contain a PublicKeyCredentialType
    // supported by this implementation, then stop processing current and move
    // on to the next element in cryptoParameters.
    if (aOptions.mParameters[a].mType != PublicKeyCredentialType::Public_key) {
      continue;
    }

    // Let normalizedAlgorithm be the result of normalizing an algorithm using
    // the procedure defined in [WebCryptoAPI], with alg set to
    // current.algorithm and op set to 'generateKey'. If an error occurs during
    // this procedure, then stop processing current and move on to the next
    // element in cryptoParameters.

    nsString algName;
    if (NS_FAILED(GetAlgorithmName(aOptions.mParameters[a].mAlgorithm,
                                   algName))) {
      continue;
    }

    // Add a new object of type PublicKeyCredentialParameters to
    // normalizedParameters, with type set to current.type and algorithm set to
    // normalizedAlgorithm.
    PublicKeyCredentialParameters normalizedObj;
    normalizedObj.mType = aOptions.mParameters[a].mType;
    normalizedObj.mAlgorithm.SetAsString().Assign(algName);

    if (!normalizedParams.AppendElement(normalizedObj, mozilla::fallible)){
      promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
      return promise.forget();
    }
  }

  // If normalizedAlgorithm is empty and cryptoParameters was not empty, cancel
  // the timer started in step 2, reject promise with a DOMException whose name
  // is "NotSupportedError", and terminate this algorithm.
  if (normalizedParams.IsEmpty() && !aOptions.mParameters.IsEmpty()) {
    promise->MaybeReject(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
    return promise.forget();
  }

  // TODO: The following check should not be here. This is checking for
  // parameters specific to the soft key, and should be put in the soft key
  // manager in the parent process. Still need to serialize
  // PublicKeyCredentialParameters first.

  // Check if at least one of the specified combinations of
  // PublicKeyCredentialParameters and cryptographic parameters is supported. If
  // not, return an error code equivalent to NotSupportedError and terminate the
  // operation.

  bool isValidCombination = false;

  for (size_t a = 0; a < normalizedParams.Length(); ++a) {
    if (normalizedParams[a].mType == PublicKeyCredentialType::Public_key &&
        normalizedParams[a].mAlgorithm.IsString() &&
        normalizedParams[a].mAlgorithm.GetAsString().EqualsLiteral(
          JWK_ALG_ECDSA_P_256)) {
      isValidCombination = true;
      break;
    }
  }
  if (!isValidCombination) {
    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;
  srv = AssembleClientData(origin, challenge, clientDataJSON);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  CryptoBuffer clientDataHash;
  if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  srv = HashCString(hashService, clientDataJSON, clientDataHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  nsTArray<WebAuthnScopedCredentialDescriptor> excludeList;
  if (aOptions.mExcludeList.WasPassed()) {
    for (const auto& s: aOptions.mExcludeList.Value()) {
      WebAuthnScopedCredentialDescriptor c;
      CryptoBuffer cb;
      cb.Assign(s.mId);
      c.id() = cb;
      excludeList.AppendElement(c);
    }
  }

  // TODO: Add extension list building
  nsTArray<WebAuthnExtension> extensions;

  WebAuthnTransactionInfo info(rpIdHash,
                               clientDataHash,
                               adjustedTimeout,
                               excludeList,
                               extensions);
  RefPtr<MozPromise<nsresult, nsresult, false>> p = GetOrCreateBackgroundActor();
  p->Then(GetMainThreadSerialEventTarget(), __func__,
          []() {
            WebAuthnManager* mgr = WebAuthnManager::Get();
            if (!mgr) {
              return;
            }
            mgr->StartRegister();
          },
          []() {
            // This case can't actually happen, we'll have crashed if the child
            // failed to create.
          });
  mTransactionPromise = promise;
  mClientData = Some(clientDataJSON);
  mCurrentParent = aParent;
  mInfo = Some(info);
  ListenForVisibilityEvents(aParent, this);

  return promise.forget();
}

void
WebAuthnManager::StartRegister() {
  if (mChild) {
    mChild->SendRequestRegister(mInfo.ref());
  }
}

void
WebAuthnManager::StartSign() {
  if (mChild) {
    mChild->SendRequestSign(mInfo.ref());
  }
}

void
WebAuthnManager::StartCancel() {
  if (mChild) {
    mChild->SendRequestCancel();
  }
}

already_AddRefed<Promise>
WebAuthnManager::GetAssertion(nsPIDOMWindowInner* aParent,
                              const PublicKeyCredentialRequestOptions& aOptions)
{
  MOZ_ASSERT(aParent);

  MaybeClearTransaction();

  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aParent);

  ErrorResult rv;
  RefPtr<Promise> promise = Promise::Create(global, rv);
  if(rv.Failed()) {
    return nullptr;
  }

  nsString origin;
  nsCString rpId;
  rv = GetOrigin(aParent, origin, rpId);
  if (NS_WARN_IF(rv.Failed())) {
    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(aParent, aOptions.mRpId.Value(), rpId))) {
      promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
      return promise.forget();
    }
  }

  CryptoBuffer rpIdHash;
  if (!rpIdHash.SetLength(SHA256_LENGTH, fallible)) {
    promise->MaybeReject(NS_ERROR_OUT_OF_MEMORY);
    return promise.forget();
  }

  nsresult srv;
  nsCOMPtr<nsICryptoHash> hashService =
    do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  srv = HashCString(hashService, rpId, rpIdHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    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;
  srv = AssembleClientData(origin, challenge, clientDataJSON);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  CryptoBuffer clientDataHash;
  if (!clientDataHash.SetLength(SHA256_LENGTH, fallible)) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  srv = HashCString(hashService, clientDataJSON, clientDataHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    promise->MaybeReject(NS_ERROR_DOM_SECURITY_ERR);
    return promise.forget();
  }

  // Note: we only support U2F-style authentication for now, so we effectively
  // require an AllowList.
  if (aOptions.mAllowList.Length() < 1) {
    promise->MaybeReject(NS_ERROR_DOM_NOT_ALLOWED_ERR);
    return promise.forget();
  }

  nsTArray<WebAuthnScopedCredentialDescriptor> allowList;
  for (const auto& s: aOptions.mAllowList) {
    WebAuthnScopedCredentialDescriptor c;
    CryptoBuffer cb;
    cb.Assign(s.mId);
    c.id() = cb;
    allowList.AppendElement(c);
  }

  // TODO: Add extension list building
  // 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.
  nsTArray<WebAuthnExtension> extensions;

  WebAuthnTransactionInfo info(rpIdHash,
                               clientDataHash,
                               adjustedTimeout,
                               allowList,
                               extensions);
  RefPtr<MozPromise<nsresult, nsresult, false>> p = GetOrCreateBackgroundActor();
  p->Then(GetMainThreadSerialEventTarget(), __func__,
          []() {
            WebAuthnManager* mgr = WebAuthnManager::Get();
            if (!mgr) {
              return;
            }
            mgr->StartSign();
          },
          []() {
            // This case can't actually happen, we'll have crashed if the child
            // failed to create.
          });

  // Only store off the promise if we've succeeded in sending the IPC event.
  mTransactionPromise = promise;
  mClientData = Some(clientDataJSON);
  mCurrentParent = aParent;
  mInfo = Some(info);
  ListenForVisibilityEvents(aParent, this);

  return promise.forget();
}

void
WebAuthnManager::FinishMakeCredential(nsTArray<uint8_t>& aRegBuffer)
{
  MOZ_ASSERT(mTransactionPromise);
  MOZ_ASSERT(mInfo.isSome());

  CryptoBuffer regData;
  if (NS_WARN_IF(!regData.Assign(aRegBuffer.Elements(), aRegBuffer.Length()))) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  mozilla::dom::CryptoBuffer aaguidBuf;
  if (NS_WARN_IF(!aaguidBuf.SetCapacity(16, mozilla::fallible))) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }
  // TODO: Adjust the AAGUID from all zeroes in Bug 1381575 (if needed)
  // See https://github.com/w3c/webauthn/issues/506
  for (int i=0; i<16; i++) {
    aaguidBuf.AppendElement(0x00, mozilla::fallible);
  }

  // Decompose the U2F registration packet
  CryptoBuffer pubKeyBuf;
  CryptoBuffer keyHandleBuf;
  CryptoBuffer attestationCertBuf;
  CryptoBuffer signatureBuf;

  // Only handles attestation cert chains of length=1.
  nsresult rv = U2FDecomposeRegistrationResponse(regData, pubKeyBuf, keyHandleBuf,
                                                 attestationCertBuf, signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Cancel(rv);
    return;
  }
  MOZ_ASSERT(keyHandleBuf.Length() <= 0xFFFF);

  nsAutoString keyHandleBase64Url;
  rv = keyHandleBuf.ToJwkBase64(keyHandleBase64Url);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Cancel(rv);
    return;
  }

  CryptoBuffer clientDataBuf;
  if (!clientDataBuf.Assign(mClientData.ref())) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  CryptoBuffer rpIdHashBuf;
  if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  // Construct the public key object
  CryptoBuffer pubKeyObj;
  rv = CBOREncodePublicKeyObj(pubKeyBuf, pubKeyObj);
  if (NS_FAILED(rv)) {
    Cancel(rv);
    return;
  }

  // During create credential, counter is always 0 for U2F
  // See https://github.com/w3c/webauthn/issues/507
  mozilla::dom::CryptoBuffer counterBuf;
  if (NS_WARN_IF(!counterBuf.SetCapacity(4, mozilla::fallible))) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }
  counterBuf.AppendElement(0x00, mozilla::fallible);
  counterBuf.AppendElement(0x00, mozilla::fallible);
  counterBuf.AppendElement(0x00, mozilla::fallible);
  counterBuf.AppendElement(0x00, mozilla::fallible);

  // Construct the Attestation Data, which slots into the end of the
  // Authentication Data buffer.
  CryptoBuffer attDataBuf;
  rv = AssembleAttestationData(aaguidBuf, keyHandleBuf, pubKeyObj, attDataBuf);
  if (NS_FAILED(rv)) {
    Cancel(rv);
    return;
  }

  mozilla::dom::CryptoBuffer authDataBuf;
  rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf, attDataBuf,
                                 authDataBuf);
  if (NS_FAILED(rv)) {
    Cancel(rv);
    return;
  }

  // The Authentication Data buffer gets CBOR-encoded with the Cert and
  // Signature to build the Attestation Object.
  CryptoBuffer attObj;
  rv = CBOREncodeAttestationObj(authDataBuf, attestationCertBuf, signatureBuf,
                                attObj);
  if (NS_FAILED(rv)) {
    Cancel(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(mCurrentParent);
  attestation->SetClientDataJSON(clientDataBuf);
  attestation->SetAttestationObject(attObj);

  RefPtr<PublicKeyCredential> credential = new PublicKeyCredential(mCurrentParent);
  credential->SetId(keyHandleBase64Url);
  credential->SetType(NS_LITERAL_STRING("public-key"));
  credential->SetRawId(keyHandleBuf);
  credential->SetResponse(attestation);

  mTransactionPromise->MaybeResolve(credential);
  MaybeClearTransaction();
}

void
WebAuthnManager::FinishGetAssertion(nsTArray<uint8_t>& aCredentialId,
                                    nsTArray<uint8_t>& aSigBuffer)
{
  MOZ_ASSERT(mTransactionPromise);
  MOZ_ASSERT(mInfo.isSome());

  CryptoBuffer tokenSignatureData;
  if (NS_WARN_IF(!tokenSignatureData.Assign(aSigBuffer.Elements(),
                                            aSigBuffer.Length()))) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  CryptoBuffer clientDataBuf;
  if (!clientDataBuf.Assign(mClientData.ref())) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  CryptoBuffer rpIdHashBuf;
  if (!rpIdHashBuf.Assign(mInfo.ref().RpIdHash())) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  CryptoBuffer signatureBuf;
  CryptoBuffer counterBuf;
  uint8_t flags = 0;
  nsresult rv = U2FDecomposeSignResponse(tokenSignatureData, flags, counterBuf,
                                         signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Cancel(rv);
    return;
  }

  CryptoBuffer attestationDataBuf;
  CryptoBuffer authenticatorDataBuf;
  rv = AssembleAuthenticatorData(rpIdHashBuf, FLAG_TUP, counterBuf,
                                 /* deliberately empty */ attestationDataBuf,
                                 authenticatorDataBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Cancel(rv);
    return;
  }

  CryptoBuffer credentialBuf;
  if (!credentialBuf.Assign(aCredentialId)) {
    Cancel(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  nsAutoString credentialBase64Url;
  rv = credentialBuf.ToJwkBase64(credentialBase64Url);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    Cancel(rv);
    return;
  }

  // If any authenticator returns success:

  // Create a new PublicKeyCredential object named value and populate its fields
  // with the values returned from the authenticator as well as the
  // clientDataJSON computed earlier.
  RefPtr<AuthenticatorAssertionResponse> assertion =
    new AuthenticatorAssertionResponse(mCurrentParent);
  assertion->SetClientDataJSON(clientDataBuf);
  assertion->SetAuthenticatorData(authenticatorDataBuf);
  assertion->SetSignature(signatureBuf);

  RefPtr<PublicKeyCredential> credential =
    new PublicKeyCredential(mCurrentParent);
  credential->SetId(credentialBase64Url);
  credential->SetType(NS_LITERAL_STRING("public-key"));
  credential->SetRawId(credentialBuf);
  credential->SetResponse(assertion);

  mTransactionPromise->MaybeResolve(credential);
  MaybeClearTransaction();
}

void
WebAuthnManager::Cancel(const nsresult& aError)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransactionPromise) {
    mTransactionPromise->MaybeReject(aError);
  }

  MaybeClearTransaction();
}

NS_IMETHODIMP
WebAuthnManager::HandleEvent(nsIDOMEvent* aEvent)
{
  MOZ_ASSERT(aEvent);

  nsAutoString type;
  aEvent->GetType(type);
  if (!type.Equals(kVisibilityChange)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsIDocument> doc =
    do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
  MOZ_ASSERT(doc);

  if (doc && doc->Hidden()) {
    MOZ_LOG(gWebAuthnManagerLog, LogLevel::Debug,
            ("Visibility change: WebAuthn window is hidden, cancelling job."));

    StartCancel();
    Cancel(NS_ERROR_ABORT);
  }

  return NS_OK;
}

void
WebAuthnManager::ActorCreated(PBackgroundChild* aActor)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aActor);

  if (mChild) {
    return;
  }

  RefPtr<WebAuthnTransactionChild> mgr(new WebAuthnTransactionChild());
  PWebAuthnTransactionChild* constructedMgr =
    aActor->SendPWebAuthnTransactionConstructor(mgr);

  if (NS_WARN_IF(!constructedMgr)) {
    ActorFailed();
    return;
  }
  MOZ_ASSERT(constructedMgr == mgr);
  mChild = mgr.forget();
  mPBackgroundCreationPromise.Resolve(NS_OK, __func__);
}

void
WebAuthnManager::ActorDestroyed()
{
  mChild = nullptr;
}

void
WebAuthnManager::ActorFailed()
{
  MOZ_CRASH("We shouldn't be here!");
}

}
}
