dom/u2f/U2FManager.cpp
author Gregory Szorc <gps@mozilla.com>
Thu, 09 Nov 2017 22:26:26 -0800
changeset 391814 1cd169ff8a0dbb2ea7d50b4275472ca9298765b3
parent 388518 cb123c84818a3c491fb9d63f00891d23f515cf6f
permissions -rw-r--r--
Bug 1416052 - Remove comment filtering; r=nalexander The auto-generated make file that we include (and the `mach environment` output that we included before that) should not contain comment lines. I think it is safe to remove the code that filters them out. It is possible a multi-line value in mozconfigs could contain lines looking like comments and this may cause problems. I'm inclined to believe that this scenario doesn't exist. If someone complains and we need to bring back support, we could certainly do that. MozReview-Commit-ID: 8kKw91HH4ms

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