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

#include "mozilla/dom/U2FHIDTokenManager.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "mozilla/ipc/BackgroundParent.h"
#include "mozilla/StaticMutex.h"

namespace mozilla {
namespace dom {

static StaticMutex gInstanceMutex;
static U2FHIDTokenManager* gInstance;
static nsIThread* gPBackgroundThread;

static void
u2f_register_callback(uint64_t aTransactionId, rust_u2f_result* aResult)
{
  UniquePtr<U2FResult> rv = MakeUnique<U2FResult>(aTransactionId, aResult);

  StaticMutexAutoLock lock(gInstanceMutex);
  if (!gInstance || NS_WARN_IF(!gPBackgroundThread)) {
    return;
  }

  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<UniquePtr<U2FResult>&&>(
      "U2FHIDTokenManager::HandleRegisterResult", gInstance,
      &U2FHIDTokenManager::HandleRegisterResult, std::move(rv)));

  MOZ_ALWAYS_SUCCEEDS(gPBackgroundThread->Dispatch(r.forget(),
                                                   NS_DISPATCH_NORMAL));
}

static void
u2f_sign_callback(uint64_t aTransactionId, rust_u2f_result* aResult)
{
  UniquePtr<U2FResult> rv = MakeUnique<U2FResult>(aTransactionId, aResult);

  StaticMutexAutoLock lock(gInstanceMutex);
  if (!gInstance || NS_WARN_IF(!gPBackgroundThread)) {
    return;
  }

  nsCOMPtr<nsIRunnable> r(NewRunnableMethod<UniquePtr<U2FResult>&&>(
      "U2FHIDTokenManager::HandleSignResult", gInstance,
      &U2FHIDTokenManager::HandleSignResult, std::move(rv)));

  MOZ_ALWAYS_SUCCEEDS(gPBackgroundThread->Dispatch(r.forget(),
                                                   NS_DISPATCH_NORMAL));
}

U2FHIDTokenManager::U2FHIDTokenManager()
{
  StaticMutexAutoLock lock(gInstanceMutex);
  mozilla::ipc::AssertIsOnBackgroundThread();
  MOZ_ASSERT(XRE_IsParentProcess());
  MOZ_ASSERT(!gInstance);

  mU2FManager = rust_u2f_mgr_new();
  gPBackgroundThread = NS_GetCurrentThread();
  MOZ_ASSERT(gPBackgroundThread, "This should never be null!");
  gInstance = this;
}

void
U2FHIDTokenManager::Drop()
{
  {
    StaticMutexAutoLock lock(gInstanceMutex);
    mozilla::ipc::AssertIsOnBackgroundThread();

    mRegisterPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    mSignPromise.RejectIfExists(NS_ERROR_DOM_UNKNOWN_ERR, __func__);

    gInstance = nullptr;
  }

  // Release gInstanceMutex before we call U2FManager::drop(). It will wait
  // for the work queue thread to join, and that requires the
  // u2f_{register,sign}_callback to lock and return.
  rust_u2f_mgr_free(mU2FManager);
  mU2FManager = nullptr;

  // Reset transaction ID so that queued runnables exit early.
  mTransaction.reset();
}

// A U2F Register operation causes a new key pair to be generated by the token.
// The token then returns the public key of the key pair, and a handle to the
// private key, which is a fancy way of saying "key wrapped private key", as
// well as the generated attestation certificate and a signature using that
// certificate's private key.
//
// The KeyHandleFromPrivateKey and PrivateKeyFromKeyHandle methods perform
// the actual key wrap/unwrap operations.
//
// The format of the return registration data is as follows:
//
// Bytes  Value
// 1      0x05
// 65     public key
// 1      key handle length
// *      key handle
// ASN.1  attestation certificate
// *      attestation signature
//
RefPtr<U2FRegisterPromise>
U2FHIDTokenManager::Register(const WebAuthnMakeCredentialInfo& aInfo,
                             bool aForceNoneAttestation)
{
  mozilla::ipc::AssertIsOnBackgroundThread();

  uint64_t registerFlags = 0;

  if (aInfo.Extra().type() != WebAuthnMaybeMakeCredentialExtraInfo::Tnull_t) {
    const auto& extra = aInfo.Extra().get_WebAuthnMakeCredentialExtraInfo();
    const WebAuthnAuthenticatorSelection& sel = extra.AuthenticatorSelection();

    // Set flags for credential creation.
    if (sel.requireResidentKey()) {
      registerFlags |= U2F_FLAG_REQUIRE_RESIDENT_KEY;
    }
    if (sel.requireUserVerification()) {
      registerFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION;
    }
    if (sel.requirePlatformAttachment()) {
      registerFlags |= U2F_FLAG_REQUIRE_PLATFORM_ATTACHMENT;
    }
  }

  CryptoBuffer rpIdHash, clientDataHash;
  NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
  nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(),
                                       rpIdHash, clientDataHash);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  ClearPromises();
  mTransaction.reset();
  uint64_t tid = rust_u2f_mgr_register(mU2FManager,
                                       registerFlags,
                                       (uint64_t)aInfo.TimeoutMS(),
                                       u2f_register_callback,
                                       clientDataHash.Elements(),
                                       clientDataHash.Length(),
                                       rpIdHash.Elements(),
                                       rpIdHash.Length(),
                                       U2FKeyHandles(aInfo.ExcludeList()).Get());

  if (tid == 0) {
    return U2FRegisterPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  mTransaction = Some(Transaction(tid, rpIdHash, aInfo.ClientDataJSON(),
                                  aForceNoneAttestation));

  return mRegisterPromise.Ensure(__func__);
}

// A U2F Sign operation creates a signature over the "param" arguments (plus
// some other stuff) using the private key indicated in the key handle argument.
//
// The format of the signed data is as follows:
//
//  32    Application parameter
//  1     User presence (0x01)
//  4     Counter
//  32    Challenge parameter
//
// The format of the signature data is as follows:
//
//  1     User presence
//  4     Counter
//  *     Signature
//
RefPtr<U2FSignPromise>
U2FHIDTokenManager::Sign(const WebAuthnGetAssertionInfo& aInfo)
{
  mozilla::ipc::AssertIsOnBackgroundThread();

  CryptoBuffer rpIdHash, clientDataHash;
  NS_ConvertUTF16toUTF8 rpId(aInfo.RpId());
  nsresult rv = BuildTransactionHashes(rpId, aInfo.ClientDataJSON(),
                                       rpIdHash, clientDataHash);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  uint64_t signFlags = 0;
  nsTArray<nsTArray<uint8_t>> appIds;
  appIds.AppendElement(rpIdHash);

  if (aInfo.Extra().type() != WebAuthnMaybeGetAssertionExtraInfo::Tnull_t) {
    const auto& extra = aInfo.Extra().get_WebAuthnGetAssertionExtraInfo();

    // Set flags for credential requests.
    if (extra.RequireUserVerification()) {
      signFlags |= U2F_FLAG_REQUIRE_USER_VERIFICATION;
    }

    // Process extensions.
    for (const WebAuthnExtension& ext: extra.Extensions()) {
      if (ext.type() == WebAuthnExtension::TWebAuthnExtensionAppId) {
        appIds.AppendElement(ext.get_WebAuthnExtensionAppId().AppId());
      }
    }
  }

  ClearPromises();
  mTransaction.reset();
  uint64_t tid = rust_u2f_mgr_sign(mU2FManager,
                                   signFlags,
                                   (uint64_t)aInfo.TimeoutMS(),
                                   u2f_sign_callback,
                                   clientDataHash.Elements(),
                                   clientDataHash.Length(),
                                   U2FAppIds(appIds).Get(),
                                   U2FKeyHandles(aInfo.AllowList()).Get());
  if (tid == 0) {
    return U2FSignPromise::CreateAndReject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
  }

  mTransaction = Some(Transaction(tid, rpIdHash, aInfo.ClientDataJSON()));

  return mSignPromise.Ensure(__func__);
}

void
U2FHIDTokenManager::Cancel()
{
  mozilla::ipc::AssertIsOnBackgroundThread();

  ClearPromises();
  rust_u2f_mgr_cancel(mU2FManager);
  mTransaction.reset();
}

void
U2FHIDTokenManager::HandleRegisterResult(UniquePtr<U2FResult>&& aResult)
{
  mozilla::ipc::AssertIsOnBackgroundThread();

  if (mTransaction.isNothing() ||
      aResult->GetTransactionId() != mTransaction.ref().mId) {
    return;
  }

  MOZ_ASSERT(!mRegisterPromise.IsEmpty());

  if (aResult->IsError()) {
    mRegisterPromise.Reject(aResult->GetError(), __func__);
    return;
  }

  nsTArray<uint8_t> registration;
  if (!aResult->CopyRegistration(registration)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

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

  CryptoBuffer regData;
  regData.Assign(registration);

  // Only handles attestation cert chains of length=1.
  nsresult rv = U2FDecomposeRegistrationResponse(regData, pubKeyBuf, keyHandle,
                                                 attestationCertBuf, signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer rpIdHashBuf;
  if (!rpIdHashBuf.Assign(mTransaction.ref().mRpIdHash)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer attObj;
  rv = AssembleAttestationObject(rpIdHashBuf, pubKeyBuf, keyHandle,
                                 attestationCertBuf, signatureBuf,
                                 mTransaction.ref().mForceNoneAttestation,
                                 attObj);
  if (NS_FAILED(rv)) {
    mRegisterPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  WebAuthnMakeCredentialResult result(mTransaction.ref().mClientDataJSON,
                                      attObj, keyHandle, regData);
  mRegisterPromise.Resolve(std::move(result), __func__);
}

void
U2FHIDTokenManager::HandleSignResult(UniquePtr<U2FResult>&& aResult)
{
  mozilla::ipc::AssertIsOnBackgroundThread();

  if (mTransaction.isNothing() ||
      aResult->GetTransactionId() != mTransaction.ref().mId) {
    return;
  }

  MOZ_ASSERT(!mSignPromise.IsEmpty());

  if (aResult->IsError()) {
    mSignPromise.Reject(aResult->GetError(), __func__);
    return;
  }

  nsTArray<uint8_t> appId;
  if (!aResult->CopyAppId(appId)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<uint8_t> keyHandle;
  if (!aResult->CopyKeyHandle(keyHandle)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<uint8_t> signature;
  if (!aResult->CopySignature(signature)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer rawSignatureBuf;
  if (!rawSignatureBuf.Assign(signature)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  nsTArray<WebAuthnExtensionResult> extensions;

  if (appId != mTransaction.ref().mRpIdHash) {
    // Indicate to the RP that we used the FIDO appId.
    extensions.AppendElement(WebAuthnExtensionResultAppId(true));
  }

  CryptoBuffer signatureBuf;
  CryptoBuffer counterBuf;
  uint8_t flags = 0;
  nsresult rv = U2FDecomposeSignResponse(rawSignatureBuf, flags, counterBuf,
                                         signatureBuf);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  CryptoBuffer chosenAppIdBuf;
  if (!chosenAppIdBuf.Assign(appId)) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  // Preserve the two LSBs of the flags byte, UP and RFU1.
  // See <https://github.com/fido-alliance/fido-2-specs/pull/519>
  flags &= 0b11;

  CryptoBuffer emptyAttestationData;
  CryptoBuffer authenticatorData;
  rv = AssembleAuthenticatorData(chosenAppIdBuf, flags, counterBuf,
                                 emptyAttestationData, authenticatorData);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    mSignPromise.Reject(NS_ERROR_DOM_UNKNOWN_ERR, __func__);
    return;
  }

  WebAuthnGetAssertionResult result(mTransaction.ref().mClientDataJSON,
                                    keyHandle, signatureBuf, authenticatorData,
                                    extensions, rawSignatureBuf);
  mSignPromise.Resolve(std::move(result), __func__);
}

}
}
