Blob Blame History Raw
/* -*- 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/U2F.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/dom/WebAuthnTransactionChild.h"
#include "mozilla/dom/WebAuthnUtil.h"
#include "nsContentUtils.h"
#include "nsICryptoHash.h"
#include "nsIEffectiveTLDService.h"
#include "nsNetCID.h"
#include "nsNetUtil.h"
#include "nsURLParsers.h"
#include "U2FUtil.h"
#include "hasht.h"

using namespace mozilla::ipc;

// Forward decl because of nsHTMLDocument.h's complex dependency on
// /layout/style
class nsHTMLDocument {
 public:
  bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString,
                                            const nsACString& aOrigHost);
};

namespace mozilla {
namespace dom {

static mozilla::LazyLogModule gU2FLog("u2fmanager");

NS_NAMED_LITERAL_STRING(kFinishEnrollment, "navigator.id.finishEnrollment");
NS_NAMED_LITERAL_STRING(kGetAssertion, "navigator.id.getAssertion");

// Bug #1436078 - Permit Google Accounts. Remove in Bug #1436085 in Jan 2023.
NS_NAMED_LITERAL_STRING(kGoogleAccountsAppId1,
                        "https://www.gstatic.com/securitykey/origins.json");
NS_NAMED_LITERAL_STRING(
    kGoogleAccountsAppId2,
    "https://www.gstatic.com/securitykey/a/google.com/origins.json");

NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(U2F)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
  NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener)
NS_INTERFACE_MAP_END

NS_IMPL_CYCLE_COLLECTING_ADDREF(U2F)
NS_IMPL_CYCLE_COLLECTING_RELEASE(U2F)

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(U2F, mParent)

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

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_INVALID_STATE_ERR) {
    return ErrorCode::DEVICE_INELIGIBLE;
  }
  return ErrorCode::OTHER_ERROR;
}

static uint32_t AdjustedTimeoutMillis(
    const Optional<Nullable<int32_t>>& opt_aSeconds) {
  uint32_t adjustedTimeoutMillis = 30000u;
  if (opt_aSeconds.WasPassed() && !opt_aSeconds.Value().IsNull()) {
    adjustedTimeoutMillis = opt_aSeconds.Value().Value() * 1000u;
    adjustedTimeoutMillis = std::max(15000u, adjustedTimeoutMillis);
    adjustedTimeoutMillis = std::min(120000u, adjustedTimeoutMillis);
  }
  return adjustedTimeoutMillis;
}

static nsresult AssembleClientData(const nsAString& aOrigin,
                                   const nsAString& aTyp,
                                   const nsAString& aChallenge,
                                   /* out */ nsString& aClientData) {
  MOZ_ASSERT(NS_IsMainThread());
  U2FClientData clientDataObject;
  clientDataObject.mTyp.Construct(aTyp);  // "Typ" from the U2F specification
  clientDataObject.mChallenge.Construct(aChallenge);
  clientDataObject.mOrigin.Construct(aOrigin);

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

  return NS_OK;
}

static void RegisteredKeysToScopedCredentialList(
    const nsAString& aAppId, const nsTArray<RegisteredKey>& aKeys,
    nsTArray<WebAuthnScopedCredential>& aList) {
  for (const RegisteredKey& key : aKeys) {
    // Check for required attributes
    if (!key.mVersion.WasPassed() || !key.mKeyHandle.WasPassed() ||
        key.mVersion.Value() != kRequiredU2FVersion) {
      continue;
    }

    // If this key's mAppId doesn't match the invocation, we can't handle it.
    if (key.mAppId.WasPassed() && !key.mAppId.Value().Equals(aAppId)) {
      continue;
    }

    CryptoBuffer keyHandle;
    nsresult rv = keyHandle.FromJwkBase64(key.mKeyHandle.Value());
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }

    WebAuthnScopedCredential c;
    c.id() = keyHandle;
    aList.AppendElement(c);
  }
}

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

/***********************************************************************
 * U2F JavaScript API Implementation
 **********************************************************************/

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

  if (mTransaction.isSome()) {
    ClearTransaction();
  }

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

void U2F::Init(ErrorResult& aRv) {
  MOZ_ASSERT(mParent);

  nsCOMPtr<nsIDocument> doc = mParent->GetDoc();
  MOZ_ASSERT(doc);
  if (!doc) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }

  nsIPrincipal* principal = doc->NodePrincipal();
  aRv = nsContentUtils::GetUTFOrigin(principal, mOrigin);
  if (NS_WARN_IF(aRv.Failed())) {
    return;
  }

  if (NS_WARN_IF(mOrigin.IsEmpty())) {
    aRv.Throw(NS_ERROR_FAILURE);
    return;
  }
}

/* virtual */ JSObject* U2F::WrapObject(JSContext* aCx,
                                        JS::Handle<JSObject*> aGivenProto) {
  return U2FBinding::Wrap(aCx, this, aGivenProto);
}

template <typename T, typename C>
void U2F::ExecuteCallback(T& aResp, nsMainThreadPtrHandle<C>& aCb) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aCb);

  // Assert that mTransaction was cleared before before we were called to allow
  // reentrancy from microtask checkpoints.
  MOZ_ASSERT(mTransaction.isNothing());

  ErrorResult error;
  aCb->Call(aResp, error);
  NS_WARNING_ASSERTION(!error.Failed(), "dom::U2F::Promise callback failed");
  error.SuppressException();  // Useful exceptions already emitted
}

void U2F::Register(const nsAString& aAppId,
                   const Sequence<RegisterRequest>& aRegisterRequests,
                   const Sequence<RegisteredKey>& aRegisteredKeys,
                   U2FRegisterCallback& aCallback,
                   const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
                   ErrorResult& aRv) {
  MOZ_ASSERT(NS_IsMainThread());

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

  nsMainThreadPtrHandle<U2FRegisterCallback> callback(
      new nsMainThreadPtrHolder<U2FRegisterCallback>("U2F::Register::callback",
                                                     &aCallback));

  // Ensure we have a callback.
  if (NS_WARN_IF(!callback)) {
    return;
  }

  // Evaluate the AppID
  nsString adjustedAppId(aAppId);
  if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Register, adjustedAppId)) {
    RegisterResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
    ExecuteCallback(response, callback);
    return;
  }

  // Produce the AppParam from the current AppID
  nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);

  nsAutoString clientDataJSON;

  // Pick the first valid RegisterRequest; we can only work with one.
  for (const RegisterRequest& req : aRegisterRequests) {
    if (!req.mChallenge.WasPassed() || !req.mVersion.WasPassed() ||
        req.mVersion.Value() != kRequiredU2FVersion) {
      continue;
    }

    nsresult rv = AssembleClientData(mOrigin, kFinishEnrollment,
                                     req.mChallenge.Value(), clientDataJSON);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }
  }

  // Did we not get a valid RegisterRequest? Abort.
  if (clientDataJSON.IsEmpty()) {
    RegisterResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
    ExecuteCallback(response, callback);
    return;
  }

  // Build the exclusion list, if any
  nsTArray<WebAuthnScopedCredential> excludeList;
  RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
                                       excludeList);

  auto clientData = NS_ConvertUTF16toUTF8(clientDataJSON);

  CryptoBuffer rpIdHash, clientDataHash;
  if (NS_FAILED(BuildTransactionHashes(cAppId, clientData, rpIdHash,
                                       clientDataHash))) {
    RegisterResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
    ExecuteCallback(response, callback);
    return;
  }

  if (!MaybeCreateBackgroundActor()) {
    RegisterResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
    ExecuteCallback(response, callback);
    return;
  }

  ListenForVisibilityEvents();

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

  // Default values for U2F.
  WebAuthnAuthenticatorSelection authSelection(
      false /* requireResidentKey */, false /* requireUserVerification */,
      false /* requirePlatformAttachment */);

  uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);

  WebAuthnMakeCredentialInfo info(
      mOrigin, rpIdHash, clientDataHash, adjustedTimeoutMillis, excludeList,
      extensions, authSelection, false /* RequestDirectAttestation */);

  MOZ_ASSERT(mTransaction.isNothing());
  mTransaction = Some(U2FTransaction(clientData, Move(AsVariant(callback))));
  mChild->SendRequestRegister(mTransaction.ref().mId, info);
}

void U2F::FinishMakeCredential(const uint64_t& aTransactionId,
                               const WebAuthnMakeCredentialResult& aResult) {
  MOZ_ASSERT(NS_IsMainThread());

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

  if (NS_WARN_IF(!mTransaction.ref().HasRegisterCallback())) {
    RejectTransaction(NS_ERROR_ABORT);
    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(aResult.RegBuffer()))) {
    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));

  // Keep the callback pointer alive.
  nsMainThreadPtrHandle<U2FRegisterCallback> callback(
      mTransaction.ref().GetRegisterCallback());

  ClearTransaction();
  ExecuteCallback(response, callback);
}

void U2F::Sign(const nsAString& aAppId, const nsAString& aChallenge,
               const Sequence<RegisteredKey>& aRegisteredKeys,
               U2FSignCallback& aCallback,
               const Optional<Nullable<int32_t>>& opt_aTimeoutSeconds,
               ErrorResult& aRv) {
  MOZ_ASSERT(NS_IsMainThread());

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

  nsMainThreadPtrHandle<U2FSignCallback> callback(
      new nsMainThreadPtrHolder<U2FSignCallback>("U2F::Sign::callback",
                                                 &aCallback));

  // Ensure we have a callback.
  if (NS_WARN_IF(!callback)) {
    return;
  }

  // Evaluate the AppID
  nsString adjustedAppId(aAppId);
  if (!EvaluateAppID(mParent, mOrigin, U2FOperation::Sign, adjustedAppId)) {
    SignResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
    ExecuteCallback(response, callback);
    return;
  }

  // Produce the AppParam from the current AppID
  nsCString cAppId = NS_ConvertUTF16toUTF8(adjustedAppId);

  nsAutoString clientDataJSON;
  nsresult rv =
      AssembleClientData(mOrigin, kGetAssertion, aChallenge, clientDataJSON);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    SignResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::BAD_REQUEST));
    ExecuteCallback(response, callback);
    return;
  }

  // Build the key list, if any
  nsTArray<WebAuthnScopedCredential> permittedList;
  RegisteredKeysToScopedCredentialList(adjustedAppId, aRegisteredKeys,
                                       permittedList);

  auto clientData = NS_ConvertUTF16toUTF8(clientDataJSON);

  CryptoBuffer rpIdHash, clientDataHash;
  if (NS_FAILED(BuildTransactionHashes(cAppId, clientData, rpIdHash,
                                       clientDataHash))) {
    SignResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
    ExecuteCallback(response, callback);
    return;
  }

  if (!MaybeCreateBackgroundActor()) {
    SignResponse response;
    response.mErrorCode.Construct(
        static_cast<uint32_t>(ErrorCode::OTHER_ERROR));
    ExecuteCallback(response, callback);
    return;
  }

  ListenForVisibilityEvents();

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

  uint32_t adjustedTimeoutMillis = AdjustedTimeoutMillis(opt_aTimeoutSeconds);

  WebAuthnGetAssertionInfo info(mOrigin, rpIdHash, clientDataHash,
                                adjustedTimeoutMillis, permittedList,
                                false, /* requireUserVerification */
                                extensions);

  MOZ_ASSERT(mTransaction.isNothing());
  mTransaction = Some(U2FTransaction(clientData, Move(AsVariant(callback))));
  mChild->SendRequestSign(mTransaction.ref().mId, info);
}

void U2F::FinishGetAssertion(const uint64_t& aTransactionId,
                             const WebAuthnGetAssertionResult& aResult) {
  MOZ_ASSERT(NS_IsMainThread());

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

  if (NS_WARN_IF(!mTransaction.ref().HasSignCallback())) {
    RejectTransaction(NS_ERROR_ABORT);
    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(aResult.CredentialID()))) {
    RejectTransaction(NS_ERROR_ABORT);
    return;
  }

  CryptoBuffer sigBuf;
  if (NS_WARN_IF(!sigBuf.Assign(aResult.SigBuffer()))) {
    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));

  // Keep the callback pointer alive.
  nsMainThreadPtrHandle<U2FSignCallback> callback(
      mTransaction.ref().GetSignCallback());

  ClearTransaction();
  ExecuteCallback(response, callback);
}

void U2F::ClearTransaction() {
  if (!NS_WARN_IF(mTransaction.isNothing())) {
    StopListeningForVisibilityEvents();
  }

  mTransaction.reset();
}

void U2F::RejectTransaction(const nsresult& aError) {
  if (NS_WARN_IF(mTransaction.isNothing())) {
    return;
  }

  StopListeningForVisibilityEvents();

  // Clear out mTransaction before calling ExecuteCallback() below to allow
  // reentrancy from microtask checkpoints.
  Maybe<U2FTransaction> maybeTransaction(Move(mTransaction));
  MOZ_ASSERT(mTransaction.isNothing() && maybeTransaction.isSome());

  U2FTransaction& transaction = maybeTransaction.ref();
  ErrorCode code = ConvertNSResultToErrorCode(aError);

  if (transaction.HasRegisterCallback()) {
    RegisterResponse response;
    response.mErrorCode.Construct(static_cast<uint32_t>(code));
    ExecuteCallback(response, transaction.GetRegisterCallback());
  }

  if (transaction.HasSignCallback()) {
    SignResponse response;
    response.mErrorCode.Construct(static_cast<uint32_t>(code));
    ExecuteCallback(response, transaction.GetSignCallback());
  }
}

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

  RejectTransaction(aError);
}

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

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

}  // namespace dom
}  // namespace mozilla