chromium/chrome/browser/ash/crosapi/cert_database_ash.cc

// Copyright 2020 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/crosapi/cert_database_ash.h"

#include <algorithm>

#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "base/system/sys_info.h"
#include "chrome/browser/ash/kcer/kcer_factory_ash.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/certificate_provider/certificate_provider_service.h"
#include "chrome/browser/certificate_provider/certificate_provider_service_factory.h"
#include "chrome/browser/net/nss_service.h"
#include "chrome/browser/net/nss_service_factory.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "chromeos/ash/components/tpm/tpm_token_info_getter.h"
#include "chromeos/crosapi/mojom/cert_database.mojom.h"
#include "components/account_id/account_id.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "components/user_manager/user_names.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "crypto/nss_util_internal.h"
#include "net/cert/cert_database.h"
#include "net/cert/nss_cert_database.h"

namespace {
using GotDbCallback =
    base::OnceCallback<void(unsigned long private_slot_id,
                            std::optional<unsigned long> system_slot_id)>;

void GotCertDbOnIOThread(GotDbCallback ui_callback,
                         net::NSSCertDatabase* cert_db) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);
  DCHECK(cert_db);

  // Technically `PK11_GetSlotID` returns CK_SLOT_ID, but it is guaranteed by
  // PKCS #11 standard to be unsigned long and it is more convenient to use here
  // because it will be sent through mojo later.
  unsigned long private_slot_id =
      PK11_GetSlotID(cert_db->GetPrivateSlot().get());

  std::optional<unsigned long> system_slot_id;
  crypto::ScopedPK11Slot system_slot = cert_db->GetSystemSlot();
  if (system_slot)
    system_slot_id = PK11_GetSlotID(system_slot.get());

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(std::move(ui_callback), private_slot_id, system_slot_id));
}

void GetCertDbOnIOThread(GotDbCallback ui_callback,
                         NssCertDatabaseGetter database_getter) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::IO);

  auto split_callback = base::SplitOnceCallback(
      base::BindOnce(&GotCertDbOnIOThread, std::move(ui_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
  // |did_get_cert_db_callback| has not been called. Call it explicitly.
  if (cert_db)
    std::move(split_callback.second).Run(cert_db);
}

}  // namespace

namespace crosapi {

CertDatabaseAsh::CertDatabaseAsh() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(ash::LoginState::IsInitialized());
  ash::LoginState::Get()->AddObserver(this);
}

CertDatabaseAsh::~CertDatabaseAsh() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  ash::LoginState::Get()->RemoveObserver(this);
}

void CertDatabaseAsh::BindReceiver(
    mojo::PendingReceiver<mojom::CertDatabase> pending_receiver) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  receivers_.Add(this, std::move(pending_receiver));
}

void CertDatabaseAsh::GetCertDatabaseInfo(
    GetCertDatabaseInfoCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // TODO(crbug.com/40156265): For now Lacros-Chrome will initialize certificate
  // database only in session. Revisit later to decide what to do on the login
  // screen.
  if (!ash::LoginState::Get()->IsUserLoggedIn()) {
    LOG(ERROR) << "Not implemented";
    std::move(callback).Run(nullptr);
    return;
  }

  if (!is_cert_database_ready_.has_value()) {
    WaitForCertDatabaseReady(std::move(callback));
    return;
  }

  const user_manager::User* user =
      user_manager::UserManager::Get()->GetPrimaryUser();

  // If user is not available or the database was previously attempted to be
  // loaded, and failed, don't retry, just return an empty result that indicates
  // error.
  if (!user || !is_cert_database_ready_.value()) {
    std::move(callback).Run(nullptr);
    return;
  }

  // Guest users should not have access to certs.
  const bool is_guest = user->GetAccountId() == user_manager::GuestAccountId();

  // Otherwise, if the TPM was already loaded previously, let the
  // caller know.
  mojom::GetCertDatabaseInfoResultPtr result =
      mojom::GetCertDatabaseInfoResult::New();
  result->should_load_chaps = !is_guest && base::SysInfo::IsRunningOnChromeOS();
  result->private_slot_id = private_slot_id_;
  result->enable_system_slot = system_slot_id_.has_value();
  result->system_slot_id =
      result->enable_system_slot ? system_slot_id_.value() : 0;

  std::move(callback).Run(std::move(result));
}

void CertDatabaseAsh::WaitForCertDatabaseReady(
    GetCertDatabaseInfoCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  DCHECK(profile);

  auto got_db_callback =
      base::BindOnce(&CertDatabaseAsh::OnCertDatabaseReady,
                     weak_factory_.GetWeakPtr(), std::move(callback));

  content::GetIOThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&GetCertDbOnIOThread, std::move(got_db_callback),
                     NssServiceFactory::GetForContext(profile)
                         ->CreateNSSCertDatabaseGetterForIOThread()));
}

void CertDatabaseAsh::OnCertDatabaseReady(
    GetCertDatabaseInfoCallback callback,
    unsigned long private_slot_id,
    std::optional<unsigned long> system_slot_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  is_cert_database_ready_ = true;
  private_slot_id_ = private_slot_id;
  system_slot_id_ = system_slot_id;

  // Calling the initial method again. Since |is_tpm_token_ready_| is not empty
  // this time, it will return some result via mojo.
  GetCertDatabaseInfo(std::move(callback));
}

void CertDatabaseAsh::LoggedInStateChanged() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // Cached result is valid only within one session and should be reset on
  // sign out. Currently it is not necessary to reset it on sign in, but doesn't
  // hurt.
  is_cert_database_ready_.reset();
}

void CertDatabaseAsh::OnCertsChangedInLacros(
    mojom::CertDatabaseChangeType change_type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  switch (change_type) {
    case mojom::CertDatabaseChangeType::kUnknown:
      net::CertDatabase::GetInstance()->NotifyObserversTrustStoreChanged();
      net::CertDatabase::GetInstance()->NotifyObserversClientCertStoreChanged();
      break;
    case mojom::CertDatabaseChangeType::kTrustStore:
      net::CertDatabase::GetInstance()->NotifyObserversTrustStoreChanged();
      break;
    case mojom::CertDatabaseChangeType::kClientCertStore:
      net::CertDatabase::GetInstance()->NotifyObserversClientCertStoreChanged();
      break;
  }
}

void CertDatabaseAsh::AddAshCertDatabaseObserver(
    mojo::PendingRemote<mojom::AshCertDatabaseObserver> observer) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  observers_.Add(
      mojo::Remote<mojom::AshCertDatabaseObserver>(std::move(observer)));
}

void CertDatabaseAsh::SetCertsProvidedByExtension(
    const std::string& extension_id,
    const chromeos::certificate_provider::CertificateInfoList&
        certificate_infos) {
  // Some certificates could've failed to parse, which is represented by
  // nullptr. We ignore such certificates to avoid closing the mojo pipe.
  chromeos::certificate_provider::CertificateInfoList
      filtered_certificate_infos;
  base::ranges::copy_if(certificate_infos,
                        std::back_inserter(filtered_certificate_infos),
                        [&](const auto& certificate_info) {
                          return certificate_info.certificate != nullptr;
                        });
  Profile* profile = ProfileManager::GetPrimaryUserProfile();
  chromeos::CertificateProviderService* certificate_provider_service =
      chromeos::CertificateProviderServiceFactory::GetForBrowserContext(
          profile);
  certificate_provider_service->SetCertificatesProvidedByExtension(
      extension_id, filtered_certificate_infos);
}

void CertDatabaseAsh::NotifyCertsChangedInAsh(
    mojom::CertDatabaseChangeType change_type) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  for (const auto& observer : observers_) {
    observer->OnCertsChangedInAsh(change_type);
  }
}

void CertDatabaseAsh::OnPkcs12CertDualWritten() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  kcer::KcerFactoryAsh::RecordPkcs12CertDualWritten();
}

}  // namespace crosapi