// 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 <string>
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/webui/certificate_provisioning_ui_handler.h"
#include "base/check_is_test.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/common/net/x509_certificate_model.h"
#include "chrome/common/net/x509_certificate_model_nss.h"
#include "chrome/grit/generated_resources.h"
#include "components/policy/core/browser/cloud/message_util.h"
#include "components/policy/core/common/cloud/cloud_policy_constants.h"
#include "content/public/browser/web_ui.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/time_format.h"
#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/profiles/profile.h"
#include "chromeos/lacros/lacros_service.h"
#include "chromeos/startup/browser_params_proxy.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/ash/crosapi/cert_provisioning_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_ash.h"
#include "chrome/browser/ash/crosapi/crosapi_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#endif // #if BUILDFLAG(IS_CHROMEOS_ASH)
using crosapi::mojom::CertProvisioningProcessState;
namespace chromeos::cert_provisioning {
namespace {
crosapi::mojom::CertProvisioning* GetCertProvisioningInterface(
Profile* profile) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
chromeos::LacrosService* service = chromeos::LacrosService::Get();
if (!profile->IsMainProfile() || !service ||
!service->IsAvailable<crosapi::mojom::CertProvisioning>()) {
return nullptr;
}
return service->GetRemote<crosapi::mojom::CertProvisioning>().get();
#endif // BUILDFLAG(IS_CHROMEOS_LACROS)
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (!ash::ProfileHelper::IsPrimaryProfile(profile)) {
return nullptr;
}
return crosapi::CrosapiManager::Get()->crosapi_ash()->cert_provisioning_ash();
#endif // #if BUILDFLAG(IS_CHROMEOS_ASH)
}
// Performs common crosapi validation. Returns void in case of success.
// Returns a string error message in case of a mismatch.
// |min_version| is the minimum version of the ash implementation
// of CertificateProvisioning necessary to support this
// operation.
base::expected<void, std::string> ValidateCrosapi(int min_version) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
if (BrowserParamsProxy::Get()->IsCrosapiDisabledForTesting()) {
CHECK_IS_TEST();
// Use the crosapi even though it's disabled - the test installs a fake.
return {};
}
chromeos::LacrosService* service = chromeos::LacrosService::Get();
int current_version =
service->GetInterfaceVersion<crosapi::mojom::CertProvisioning>();
if (current_version < min_version) {
return base::unexpected(base::StringPrintf(
"validate crosapi error: min_version:%i current_version:%i",
min_version, current_version));
}
#endif // #if BUILDFLAG(IS_CHROME_LACROS)
return {};
}
// Returns localized representation for the state of a certificate provisioning
// process.
std::u16string StateToText(CertProvisioningProcessState state) {
switch (state) {
case CertProvisioningProcessState ::kInitState:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR);
case CertProvisioningProcessState ::kKeypairGenerated:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING);
case CertProvisioningProcessState::kStartCsrResponseReceived:
// Intentional fall-through.
case CertProvisioningProcessState::kVaChallengeFinished:
// Intentional fall-through.
case CertProvisioningProcessState::kKeyRegistered:
// Intentional fall-through.
case CertProvisioningProcessState::kKeypairMarked:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR);
case CertProvisioningProcessState::kSignCsrFinished:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING);
case CertProvisioningProcessState::kFinishCsrResponseReceived:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_WAITING_FOR_CA);
case CertProvisioningProcessState::kSucceeded:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_SUCCESS);
case CertProvisioningProcessState::kFailed:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_FAILURE);
case CertProvisioningProcessState::kInconsistentDataError:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PREPARING_CSR_WAITING);
case CertProvisioningProcessState::kCanceled:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_CANCELED);
case CertProvisioningProcessState::kReadyForNextOperation:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_READY_FOR_NEXT_OPERATION);
case CertProvisioningProcessState::kAuthorizeInstructionReceived:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_AUTHORIZE_INSTRUCTION_RECEIVED);
case CertProvisioningProcessState::kProofOfPossessionInstructionReceived:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_PROOF_OF_POSSESSION_INSTRUCTION_RECEIVED);
case CertProvisioningProcessState::kImportCertificateInstructionReceived:
return l10n_util::GetStringUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_STATUS_IMPORT_CERTIFICATE_INSTRUCTION_RECEIVED);
}
NOTREACHED_IN_MIGRATION();
}
// Returns the status message of the process.
// The status message is expanded by the failure message if the process failed
// and the error message is non-empty.
std::u16string MakeStatusMessage(
bool did_fail,
CertProvisioningProcessState state,
const std::optional<std::string>& failure_message) {
if (!did_fail) {
return StateToText(state);
}
std::u16string status_message =
StateToText(CertProvisioningProcessState::kFailed);
if (failure_message.has_value()) {
status_message += base::UTF8ToUTF16(": " + failure_message.value());
}
return status_message;
}
// Returns a localized representation of the last update time as a delay (e.g.
// "5 minutes ago".
std::u16string GetTimeSinceLastUpdate(base::Time last_update_time) {
const base::Time now = base::Time::NowFromSystemTime();
if (last_update_time.is_null() || last_update_time > now)
return std::u16string();
const base::TimeDelta elapsed_time = now - last_update_time;
return ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_ELAPSED,
ui::TimeFormat::LENGTH_SHORT, elapsed_time);
}
std::u16string GetMessageFromBackendError(
const crosapi::mojom::CertProvisioningBackendServerErrorPtr& call_info) {
if (!call_info)
return std::u16string();
std::u16string time_u16 =
base::UTF8ToUTF16(base::TimeFormatHTTP(call_info->time));
// FormatDeviceManagementStatus will return "Unknown error" if the value after
// cast is not actually an existing enum value.
auto status =
static_cast<policy::DeviceManagementStatus>(call_info->status_code);
return l10n_util::GetStringFUTF16(
IDS_SETTINGS_CERTIFICATE_MANAGER_PROVISIONING_DMSERVER_ERROR_MESSAGE,
policy::FormatDeviceManagementStatus(status), time_u16);
}
} // namespace
// static
std::unique_ptr<CertificateProvisioningUiHandler>
CertificateProvisioningUiHandler::CreateForProfile(Profile* user_profile) {
return std::make_unique<CertificateProvisioningUiHandler>(
GetCertProvisioningInterface(user_profile));
}
CertificateProvisioningUiHandler::CertificateProvisioningUiHandler(
crosapi::mojom::CertProvisioning* cert_provisioning_interface)
: cert_provisioning_interface_(cert_provisioning_interface) {
if (cert_provisioning_interface_) {
cert_provisioning_interface->AddObserver(
receiver_.BindNewPipeAndPassRemote());
}
}
CertificateProvisioningUiHandler::~CertificateProvisioningUiHandler() = default;
void CertificateProvisioningUiHandler::RegisterMessages() {
// Passing base::Unretained(this) to
// web_ui()->RegisterMessageCallback is fine because in chrome Web
// UI, web_ui() has acquired ownership of |this| and maintains the life time
// of |this| accordingly.
web_ui()->RegisterMessageCallback(
"refreshCertificateProvisioningProcessses",
base::BindRepeating(&CertificateProvisioningUiHandler::
HandleRefreshCertificateProvisioningProcesses,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"triggerCertificateProvisioningProcessUpdate",
base::BindRepeating(&CertificateProvisioningUiHandler::
HandleTriggerCertificateProvisioningProcessUpdate,
base::Unretained(this)));
web_ui()->RegisterMessageCallback(
"triggerCertificateProvisioningProcessReset",
base::BindRepeating(&CertificateProvisioningUiHandler::
HandleTriggerCertificateProvisioningProcessReset,
base::Unretained(this)));
}
void CertificateProvisioningUiHandler::OnStateChanged() {
// If Javascript is not allowed yet, the UI will request a refresh during its
// first message to the handler.
if (!IsJavascriptAllowed())
return;
RefreshCertificateProvisioningProcesses();
}
unsigned int
CertificateProvisioningUiHandler::ReadAndResetUiRefreshCountForTesting() {
unsigned int value = ui_refresh_count_for_testing_;
ui_refresh_count_for_testing_ = 0;
return value;
}
void CertificateProvisioningUiHandler::
HandleRefreshCertificateProvisioningProcesses(
const base::Value::List& args) {
CHECK_EQ(0U, args.size());
AllowJavascript();
RefreshCertificateProvisioningProcesses();
}
void CertificateProvisioningUiHandler::
HandleTriggerCertificateProvisioningProcessUpdate(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
const base::Value& cert_profile_id = args[0];
if (!cert_profile_id.is_string()) {
return;
}
if (cert_provisioning_interface_) {
cert_provisioning_interface_->UpdateOneProcess(cert_profile_id.GetString());
}
}
void CertificateProvisioningUiHandler::
HandleTriggerCertificateProvisioningProcessReset(
const base::Value::List& args) {
CHECK_EQ(1U, args.size());
const base::Value& cert_profile_id = args[0];
if (!cert_profile_id.is_string()) {
return;
}
if (cert_provisioning_interface_) {
base::expected<void, std::string> success = ValidateCrosapi(
crosapi::mojom::CertProvisioning::kResetOneProcessMinVersion);
if (success.has_value()) {
cert_provisioning_interface_->ResetOneProcess(
cert_profile_id.GetString());
} else {
LOG(ERROR) << "cert-prov cros_api validation error: " << success.error();
}
}
}
void CertificateProvisioningUiHandler::
RefreshCertificateProvisioningProcesses() {
if (cert_provisioning_interface_) {
cert_provisioning_interface_->GetStatus(
base::BindOnce(&CertificateProvisioningUiHandler::GotStatus,
weak_ptr_factory_.GetWeakPtr()));
}
}
void CertificateProvisioningUiHandler::GotStatus(
std::vector<crosapi::mojom::CertProvisioningProcessStatusPtr> status) {
base::Value::List all_processes;
for (auto& process : status) {
base::Value::Dict entry;
entry.Set("certProfileId", std::move(process->cert_profile_id));
entry.Set("certProfileName", std::move(process->cert_profile_name));
entry.Set("isDeviceWide", process->is_device_wide);
entry.Set("timeSinceLastUpdate",
GetTimeSinceLastUpdate(process->last_update_time));
entry.Set("lastUnsuccessfulMessage",
GetMessageFromBackendError(process->last_backend_server_error));
entry.Set("stateId", static_cast<int>(process->state));
entry.Set("status", MakeStatusMessage(process->did_fail, process->state,
process->failure_message));
entry.Set("publicKey",
x509_certificate_model::ProcessRawSubjectPublicKeyInfo(
process->public_key));
all_processes.Append(std::move(entry));
}
++ui_refresh_count_for_testing_;
FireWebUIListener("certificate-provisioning-processes-changed",
all_processes);
}
} // namespace chromeos::cert_provisioning