// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/arc/net/cert_manager_impl.h"

#include <pk11priv.h>
#include <pk11pub.h>

#include <optional>
#include <utility>

#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/net/nss_service.h"
#include "chrome/browser/net/nss_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "components/device_event_log/device_event_log.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/nss_key_util.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/scoped_nss_types.h"
#include "net/cert/x509_util_nss.h"
#include "third_party/boringssl/src/pki/pem.h"

namespace {

void GetCertDBOnIOThread(
    NssCertDatabaseGetter database_getter,
    base::OnceCallback<void(net::NSSCertDatabase*)> callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  auto split_callback = base::SplitOnceCallback(std::move(callback));
  net::NSSCertDatabase* cert_db =
      std::move(database_getter).Run(std::move(split_callback.first));
  // If the NSS database was already available, |cert_db| is non-null and
  // the callback has not been called. Explicitly call the callback.
  if (cert_db) {
    std::move(split_callback.second).Run(cert_db);
  }
}

net::ScopedCERTCertificate TranslatePEMToCert(const std::string& cert_pem) {
  bssl::PEMTokenizer tokenizer(cert_pem, {arc::kCertificatePEMHeader});
  if (!tokenizer.GetNext()) {
    NET_LOG(ERROR) << "Failed to get certificate data";
    return nullptr;
  }

  std::vector<uint8_t> cert_der(tokenizer.data().begin(),
                                tokenizer.data().end());
  return net::x509_util::CreateCERTCertificateFromBytes(cert_der);
}

// Check the status of delete operation for cert and key, then runs |callback|.
void DeleteCertAndKeyCallback(arc::CertManagerImpl::DeleteCertCallback callback,
                              bool status) {
  if (!status) {
    NET_LOG(ERROR) << "Failed to delete certificate";
  }
  // Run the callback regardless of the certificate and key deletion status.
  std::move(callback).Run();
}

}  // namespace

namespace arc {

CertManagerImpl::CertManagerImpl(Profile* profile) : profile_(profile) {}

CertManagerImpl::~CertManagerImpl() = default;

std::string CertManagerImpl::ImportPrivateKey(const std::string& key_pem,
                                              net::NSSCertDatabase* database) {
  if (!database) {
    NET_LOG(ERROR) << "Certificate database is not initialized";
    return std::string();
  }

  bssl::PEMTokenizer tokenizer(key_pem, {kPrivateKeyPEMHeader});
  if (!tokenizer.GetNext()) {
    NET_LOG(ERROR) << "Failed to get private key data";
    return std::string();
  }
  std::vector<uint8_t> key_der(tokenizer.data().begin(),
                               tokenizer.data().end());

  crypto::ScopedPK11Slot private_slot = database->GetPrivateSlot();
  if (!private_slot) {
    NET_LOG(ERROR) << "Failed to get PK11 slot";
    return std::string();
  }

  crypto::ScopedSECKEYPrivateKey key(crypto::ImportNSSKeyFromPrivateKeyInfo(
      private_slot.get(), key_der, true /* permanent */));
  if (!key) {
    NET_LOG(ERROR) << "Failed to import private key";
    return std::string();
  }

  crypto::ScopedSECItem sec_item(PK11_GetLowLevelKeyIDForPrivateKey(key.get()));
  if (!sec_item) {
    NET_LOG(ERROR) << "Failed to get private key ID";
    return std::string();
  }
  return base::HexEncode(net::x509_util::SECItemAsSpan(*sec_item));
}

std::string CertManagerImpl::ImportUserCert(const std::string& cert_pem,
                                            net::NSSCertDatabase* database) {
  if (!database) {
    NET_LOG(ERROR) << "Certificate database is not initialized";
    return std::string();
  }

  net::ScopedCERTCertificate cert = TranslatePEMToCert(cert_pem);
  if (!cert) {
    NET_LOG(ERROR) << "Failed to translate PEM to certificate object";
    return std::string();
  }

  int status = database->ImportUserCert(cert.get());
  if (status != net::OK) {
    NET_LOG(ERROR) << "Failed to import user certificate with status code "
                   << status;
    return std::string();
  }

  crypto::ScopedSECItem sec_item(
      PK11_GetLowLevelKeyIDForCert(nullptr, cert.get(), nullptr));
  if (!sec_item) {
    NET_LOG(ERROR) << "Failed to get certificate ID";
    return std::string();
  }

  return base::HexEncode(net::x509_util::SECItemAsSpan(*sec_item));
}

void CertManagerImpl::DeleteCertAndKeyAsync(const std::string& cert_pem,
                                            net::NSSCertDatabase* database,
                                            DeleteCertCallback callback) {
  if (!database) {
    NET_LOG(ERROR) << "Certificate database is not initialized";
    return;
  }

  net::ScopedCERTCertificate cert = TranslatePEMToCert(cert_pem);
  if (!cert) {
    NET_LOG(ERROR) << "Failed to translate PEM to certificate object";
    return;
  }
  database->DeleteCertAndKeyAsync(
      std::move(cert),
      base::BindOnce(&DeleteCertAndKeyCallback, std::move(callback)));
}

int CertManagerImpl::GetSlotID(net::NSSCertDatabase* database) {
  if (!database) {
    NET_LOG(ERROR) << "Certificate database is not initialized";
    return -1;
  }

  crypto::ScopedPK11Slot private_slot = database->GetPrivateSlot();
  if (!private_slot) {
    NET_LOG(ERROR) << "Failed to get PK11 slot";
    return -1;
  }

  return PK11_GetSlotID(private_slot.get());
}

void CertManagerImpl::DeleteAndImportPrivateKeyAndCertWithDB(
    const std::string& key_pem,
    const std::string& cert_pem,
    ImportPrivateKeyAndCertCallback callback,
    net::NSSCertDatabase* database) {
  // Attempt to delete the key and certificate first. This is important for an
  // edge case below.
  // 1. The user removes a set of passpoint credentials. This causes shill to
  // delete the key and certificate from chaps.
  // 2. The user re-adds passpoint credentials with the same key and
  // certificate.
  // If there is no Chrome restart between (1) and (2), NSS caches are not
  // updated with the result of (1), making (2) fail.
  // Deleting the key from NSS ensures that (2) succeeds even in this case.
  DeleteCertAndKeyAsync(
      cert_pem, database,
      base::BindOnce(&CertManagerImpl::ImportPrivateKeyAndCertWithDB,
                     weak_factory_.GetWeakPtr(), key_pem, cert_pem,
                     std::move(callback), database));
}

void CertManagerImpl::ImportPrivateKeyAndCertWithDB(
    const std::string& key_pem,
    const std::string& cert_pem,
    ImportPrivateKeyAndCertCallback callback,
    net::NSSCertDatabase* database) {
  std::string key_id = ImportPrivateKey(key_pem, database);
  if (key_id.empty()) {
    NET_LOG(ERROR) << "Failed to import private key";
    std::move(callback).Run(/*cert_id=*/std::nullopt,
                            /*slot_id=*/std::nullopt);
    return;
  }
  // Both DeleteCertAndKey parse the passed certificate into a CERTCertificate.
  // This is unfortunate but reusing the same CERTCertificate resulted in
  // PK11_GetLowLevelKeyIDForCert failing after the DeleteCertAndKey call.
  std::string cert_id = ImportUserCert(cert_pem, database);
  if (cert_id.empty()) {
    NET_LOG(ERROR) << "Failed to import client certificate";
    std::move(callback).Run(/*cert_id=*/std::nullopt,
                            /*slot_id=*/std::nullopt);
    return;
  }
  int slot_id = GetSlotID(database);

  // The ID of imported user certificate and private key is the same, use one
  // of them.
  DCHECK(key_id == cert_id);
  std::move(callback).Run(cert_id, slot_id);
}

void CertManagerImpl::ImportPrivateKeyAndCert(
    const std::string& key_pem,
    const std::string& cert_pem,
    ImportPrivateKeyAndCertCallback callback) {
  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(
          &GetCertDBOnIOThread,
          NssServiceFactory::GetForContext(profile_)
              ->CreateNSSCertDatabaseGetterForIOThread(),
          base::BindPostTaskToCurrentDefault(base::BindOnce(
              &CertManagerImpl::DeleteAndImportPrivateKeyAndCertWithDB,
              weak_factory_.GetWeakPtr(), key_pem, cert_pem,
              std::move(callback)))));
}

}  // namespace arc
