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

#include "hasht.h"
#include "nsICryptoHash.h"
#include "nsNetCID.h"
#include "U2FManager.h"
#include "U2FTransactionChild.h"
#include "U2FUtil.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PWebAuthnTransaction.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/BackgroundChild.h"

using namespace mozilla::ipc;

namespace mozilla {
namespace dom {

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

namespace {
StaticRefPtr<U2FManager> gU2FManager;
static mozilla::LazyLogModule gU2FManagerLog("u2fmanager");
}

NS_NAMED_LITERAL_STRING(kVisibilityChange, "visibilitychange");

NS_IMPL_ISUPPORTS(U2FManager, nsIDOMEventListener);

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

static void
ListenForVisibilityEvents(nsPIDOMWindowInner* aParent,
                          U2FManager* 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,
                                 U2FManager* 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));
}

static ErrorCode
ConvertNSResultToErrorCode(const nsresult& aError)
{
  if (aError == NS_ERROR_DOM_TIMEOUT_ERR) {
    return ErrorCode::TIMEOUT;
  }
  /* Emitted by U2F{Soft,HID}TokenManager when we really mean ineligible */
  if (aError == NS_ERROR_DOM_NOT_ALLOWED_ERR) {
    return ErrorCode::DEVICE_INELIGIBLE;
  }
  return ErrorCode::OTHER_ERROR;
}

/***********************************************************************
 * U2FManager Implementation
 **********************************************************************/

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

void
U2FManager::ClearTransaction()
{
  if (!NS_WARN_IF(mTransaction.isNothing())) {
    StopListeningForVisibilityEvents(mTransaction.ref().mParent, this);
  }

  mTransaction.reset();
}

void
U2FManager::RejectTransaction(const nsresult& aError)
{
  if (!NS_WARN_IF(mTransaction.isNothing())) {
    ErrorCode code = ConvertNSResultToErrorCode(aError);
    mTransaction.ref().mPromise.Reject(code, __func__);
  }

  ClearTransaction();
}

void
U2FManager::CancelTransaction(const nsresult& aError)
{
  if (!NS_WARN_IF(!mChild || mTransaction.isNothing())) {
    mChild->SendRequestCancel(mTransaction.ref().mId);
  }

  RejectTransaction(aError);
}

U2FManager::~U2FManager()
{
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransaction.isSome()) {
    RejectTransaction(NS_ERROR_ABORT);
  }

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

bool
U2FManager::MaybeCreateBackgroundActor()
{
  MOZ_ASSERT(NS_IsMainThread());

  if (mChild) {
    return true;
  }

  PBackgroundChild* actorChild = BackgroundChild::GetOrCreateForCurrentThread();
  if (NS_WARN_IF(!actorChild)) {
    return false;
  }

  RefPtr<U2FTransactionChild> mgr(new U2FTransactionChild());
  PWebAuthnTransactionChild* constructedMgr =
    actorChild->SendPWebAuthnTransactionConstructor(mgr);

  if (NS_WARN_IF(!constructedMgr)) {
    return false;
  }

  MOZ_ASSERT(constructedMgr == mgr);
  mChild = mgr.forget();

  return true;
}

//static
U2FManager*
U2FManager::GetOrCreate()
{
  MOZ_ASSERT(NS_IsMainThread());
  if (gU2FManager) {
    return gU2FManager;
  }

  gU2FManager = new U2FManager();
  ClearOnShutdown(&gU2FManager);
  return gU2FManager;
}

//static
U2FManager*
U2FManager::Get()
{
  MOZ_ASSERT(NS_IsMainThread());
  return gU2FManager;
}

//static
nsresult
U2FManager::BuildTransactionHashes(const nsCString& aRpId,
                                   const nsCString& aClientDataJSON,
                                   /* out */ CryptoBuffer& aRpIdHash,
                                   /* out */ CryptoBuffer& aClientDataHash)
{
  nsresult srv;
  nsCOMPtr<nsICryptoHash> hashService =
    do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &srv);
  if (NS_FAILED(srv)) {
    return srv;
  }

  if (!aRpIdHash.SetLength(SHA256_LENGTH, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  srv = HashCString(hashService, aRpId, aRpIdHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    return NS_ERROR_FAILURE;
  }

  if (!aClientDataHash.SetLength(SHA256_LENGTH, fallible)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  srv = HashCString(hashService, aClientDataJSON, aClientDataHash);
  if (NS_WARN_IF(NS_FAILED(srv))) {
    return NS_ERROR_FAILURE;
  }

  if (MOZ_LOG_TEST(gU2FLog, LogLevel::Debug)) {
    nsString base64;
    Unused << NS_WARN_IF(NS_FAILED(aRpIdHash.ToJwkBase64(base64)));

    MOZ_LOG(gU2FLog, LogLevel::Debug,
            ("dom::U2FManager::RpID: %s", aRpId.get()));

    MOZ_LOG(gU2FLog, LogLevel::Debug,
            ("dom::U2FManager::Rp ID Hash (base64): %s",
              NS_ConvertUTF16toUTF8(base64).get()));

    Unused << NS_WARN_IF(NS_FAILED(aClientDataHash.ToJwkBase64(base64)));

    MOZ_LOG(gU2FLog, LogLevel::Debug,
            ("dom::U2FManager::Client Data JSON: %s", aClientDataJSON.get()));

    MOZ_LOG(gU2FLog, LogLevel::Debug,
            ("dom::U2FManager::Client Data Hash (base64): %s",
              NS_ConvertUTF16toUTF8(base64).get()));
  }

  return NS_OK;
}

already_AddRefed<U2FPromise>
U2FManager::Register(nsPIDOMWindowInner* aParent, const nsCString& aRpId,
               const nsCString& aClientDataJSON,
               const uint32_t& aTimeoutMillis,
               const nsTArray<WebAuthnScopedCredentialDescriptor>& aExcludeList)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aParent);

  if (mTransaction.isSome()) {
    CancelTransaction(NS_ERROR_ABORT);
  }

  CryptoBuffer rpIdHash, clientDataHash;
  if (NS_FAILED(BuildTransactionHashes(aRpId, aClientDataJSON,
                                       rpIdHash, clientDataHash))) {
    return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
  }

  if (!MaybeCreateBackgroundActor()) {
    return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
  }

  ListenForVisibilityEvents(aParent, this);

  // Always blank for U2F
  nsTArray<WebAuthnExtension> extensions;

  WebAuthnTransactionInfo info(rpIdHash,
                               clientDataHash,
                               aTimeoutMillis,
                               aExcludeList,
                               extensions);

  MOZ_ASSERT(mTransaction.isNothing());
  mTransaction = Some(U2FTransaction(aParent, Move(info), aClientDataJSON));

  mChild->SendRequestRegister(mTransaction.ref().mId, mTransaction.ref().mInfo);
  return mTransaction.ref().mPromise.Ensure(__func__);
}

already_AddRefed<U2FPromise>
U2FManager::Sign(nsPIDOMWindowInner* aParent,
                 const nsCString& aRpId,
                 const nsCString& aClientDataJSON,
                 const uint32_t& aTimeoutMillis,
                 const nsTArray<WebAuthnScopedCredentialDescriptor>& aAllowList)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aParent);

  if (mTransaction.isSome()) {
    CancelTransaction(NS_ERROR_ABORT);
  }

  CryptoBuffer rpIdHash, clientDataHash;
  if (NS_FAILED(BuildTransactionHashes(aRpId, aClientDataJSON,
                                       rpIdHash, clientDataHash))) {
    return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
  }

  if (!MaybeCreateBackgroundActor()) {
    return U2FPromise::CreateAndReject(ErrorCode::OTHER_ERROR, __func__).forget();
  }

  ListenForVisibilityEvents(aParent, this);

  // Always blank for U2F
  nsTArray<WebAuthnExtension> extensions;

  WebAuthnTransactionInfo info(rpIdHash,
                               clientDataHash,
                               aTimeoutMillis,
                               aAllowList,
                               extensions);

  MOZ_ASSERT(mTransaction.isNothing());
  mTransaction = Some(U2FTransaction(aParent, Move(info), aClientDataJSON));

  mChild->SendRequestSign(mTransaction.ref().mId, mTransaction.ref().mInfo);
  return mTransaction.ref().mPromise.Ensure(__func__);
}

void
U2FManager::FinishRegister(const uint64_t& aTransactionId,
                           nsTArray<uint8_t>& aRegBuffer)
{
  MOZ_ASSERT(NS_IsMainThread());

  // Check for a valid transaction.
  if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
    return;
  }

  CryptoBuffer clientDataBuf;
  if (NS_WARN_IF(!clientDataBuf.Assign(mTransaction.ref().mClientData))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  CryptoBuffer regBuf;
  if (NS_WARN_IF(!regBuf.Assign(aRegBuffer))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  nsString clientDataBase64;
  nsString registrationDataBase64;
  nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
  nsresult rvRegistrationData = regBuf.ToJwkBase64(registrationDataBase64);

  if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
      NS_WARN_IF(NS_FAILED(rvRegistrationData))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  // Assemble a response object to return
  RegisterResponse response;
  response.mVersion.Construct(kRequiredU2FVersion);
  response.mClientData.Construct(clientDataBase64);
  response.mRegistrationData.Construct(registrationDataBase64);
  response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));

  nsString responseStr;
  if (NS_WARN_IF(!response.ToJSON(responseStr))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  mTransaction.ref().mPromise.Resolve(responseStr, __func__);
  ClearTransaction();
}

void
U2FManager::FinishSign(const uint64_t& aTransactionId,
                       nsTArray<uint8_t>& aCredentialId,
                       nsTArray<uint8_t>& aSigBuffer)
{
  MOZ_ASSERT(NS_IsMainThread());

  // Check for a valid transaction.
  if (mTransaction.isNothing() || mTransaction.ref().mId != aTransactionId) {
    return;
  }

  CryptoBuffer clientDataBuf;
  if (NS_WARN_IF(!clientDataBuf.Assign(mTransaction.ref().mClientData))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  CryptoBuffer credBuf;
  if (NS_WARN_IF(!credBuf.Assign(aCredentialId))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  CryptoBuffer sigBuf;
  if (NS_WARN_IF(!sigBuf.Assign(aSigBuffer))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  // Assemble a response object to return
  nsString clientDataBase64;
  nsString signatureDataBase64;
  nsString keyHandleBase64;
  nsresult rvClientData = clientDataBuf.ToJwkBase64(clientDataBase64);
  nsresult rvSignatureData = sigBuf.ToJwkBase64(signatureDataBase64);
  nsresult rvKeyHandle = credBuf.ToJwkBase64(keyHandleBase64);
  if (NS_WARN_IF(NS_FAILED(rvClientData)) ||
      NS_WARN_IF(NS_FAILED(rvSignatureData) ||
      NS_WARN_IF(NS_FAILED(rvKeyHandle)))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  SignResponse response;
  response.mKeyHandle.Construct(keyHandleBase64);
  response.mClientData.Construct(clientDataBase64);
  response.mSignatureData.Construct(signatureDataBase64);
  response.mErrorCode.Construct(static_cast<uint32_t>(ErrorCode::OK));

  nsString responseStr;
  if (NS_WARN_IF(!response.ToJSON(responseStr))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  mTransaction.ref().mPromise.Resolve(responseStr, __func__);
  ClearTransaction();
}

void
U2FManager::RequestAborted(const uint64_t& aTransactionId,
                           const nsresult& aError)
{
  MOZ_ASSERT(NS_IsMainThread());

  if (mTransaction.isSome() && mTransaction.ref().mId == aTransactionId) {
    RejectTransaction(aError);
  }
}

NS_IMETHODIMP
U2FManager::HandleEvent(nsIDOMEvent* aEvent)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aEvent);

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

  nsCOMPtr<nsIDocument> doc =
    do_QueryInterface(aEvent->InternalDOMEvent()->GetTarget());
  if (NS_WARN_IF(!doc)) {
    return NS_ERROR_FAILURE;
  }

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

    CancelTransaction(NS_ERROR_ABORT);
  }

  return NS_OK;
}

void
U2FManager::ActorDestroyed()
{
  MOZ_ASSERT(NS_IsMainThread());
  mChild = nullptr;
}

} // namespace dom
} // namespace mozilla
