gecko-dev/dom/u2f/U2FManager.cpp

491 строка
13 KiB
C++

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