serenity/Userland/Applications/CertificateSettings/CertificateStoreWidget.cpp
Timothy Flynn 928287b782 LibCrypto: Store ASN1 certificate timestamps as UnixDateTime
We are currently using Core::DateTime, which is meant to represent local
time. However, we are doing no conversion between the parsed time in UTC
and local time, so we end up comparing time stamps from different time
zones.

Instead, store the parsed times as UnixDateTime, which is UTC. Then we
can always compare the parsed times against the current UTC time.

This also lets us store parsed milliseconds.
2024-03-08 00:41:23 +01:00

192 lines
6.2 KiB
C++

/*
* Copyright (c) 2023, Fabian Dellwing <fabian@dellwing.net>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "CertificateStoreWidget.h"
#include <AK/String.h>
#include <LibCore/DateTime.h>
#include <LibCrypto/ASN1/PEM.h>
#include <LibFileSystem/FileSystem.h>
#include <LibFileSystemAccessClient/Client.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/SortingProxyModel.h>
namespace CertificateSettings {
NonnullRefPtr<CertificateStoreModel> CertificateStoreModel::create()
{
return adopt_ref(*new CertificateStoreModel);
}
void CertificateStoreProxyModel::sort(int column, GUI::SortOrder sort_order)
{
SortingProxyModel::sort(column, sort_order);
m_parent_table_view->set_column_width(CertificateStoreModel::Column::IssuedTo, 150);
m_parent_table_view->set_column_width(CertificateStoreModel::Column::IssuedBy, 150);
}
ErrorOr<void> CertificateStoreModel::load()
{
m_certificates = TRY(DefaultRootCACertificates::load_certificates());
return {};
}
ErrorOr<String> CertificateStoreModel::column_name(int column) const
{
switch (column) {
case Column::IssuedTo:
return "Issued To"_string;
case Column::IssuedBy:
return "Issued By"_string;
case Column::Expire:
return "Expiration Date"_string;
default:
VERIFY_NOT_REACHED();
}
}
GUI::Variant CertificateStoreModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const
{
if (role != GUI::ModelRole::Display)
return {};
if (m_certificates.is_empty())
return {};
auto cert = m_certificates.at(index.row());
switch (index.column()) {
case Column::IssuedTo: {
auto issued_to = cert.subject.common_name();
if (issued_to.is_empty()) {
issued_to = cert.subject.organizational_unit();
}
return issued_to;
}
case Column::IssuedBy: {
auto issued_by = cert.issuer.common_name();
if (issued_by.is_empty()) {
issued_by = cert.issuer.organizational_unit();
}
return issued_by;
}
case Column::Expire:
return Core::DateTime::from_timestamp(cert.validity.not_after.seconds_since_epoch()).to_byte_string("%Y-%m-%d"sv);
default:
VERIFY_NOT_REACHED();
}
return {};
}
ErrorOr<size_t> CertificateStoreModel::add(Vector<Certificate> const& certificates)
{
auto size = m_certificates.size();
TRY(m_certificates.try_extend(certificates));
return m_certificates.size() - size;
}
ErrorOr<void> CertificateStoreWidget::import_pem()
{
FileSystemAccessClient::OpenFileOptions options {
.window_title = "Import"sv,
.allowed_file_types = Vector {
GUI::FileTypeFilter { "Certificate Files", { { "pem", "crt" } } },
},
};
auto fsac_result = FileSystemAccessClient::Client::the().open_file(window(), options);
if (fsac_result.is_error())
return {};
auto fsac_file = fsac_result.release_value();
auto data = TRY(fsac_file.release_stream()->read_until_eof());
auto count = TRY(m_root_ca_model->add(TRY(DefaultRootCACertificates::parse_pem_root_certificate_authorities(data))));
if (count == 0) {
return Error::from_string_view("No valid CA found to import."sv);
}
auto cert_file = TRY(Core::File::open(TRY(String::formatted("{}/.config/certs.pem", Core::StandardPaths::home_directory())), Core::File::OpenMode::Write | Core::File::OpenMode::Append));
TRY(cert_file->write_until_depleted(data.bytes()));
cert_file->close();
m_root_ca_model->invalidate();
m_root_ca_tableview->set_column_width(CertificateStoreModel::Column::IssuedTo, 150);
m_root_ca_tableview->set_column_width(CertificateStoreModel::Column::IssuedBy, 150);
GUI::MessageBox::show(window(), TRY(String::formatted("Successfully imported {} CAs.", count)), "Success"sv);
return {};
}
Certificate CertificateStoreModel::get(int index)
{
return m_certificates.at(index);
}
ErrorOr<void> CertificateStoreWidget::export_pem()
{
auto index = m_root_ca_tableview->selection().first();
auto real_index = m_root_ca_proxy_model->map_to_source(index);
auto cert = m_root_ca_model->get(real_index.row());
String filename = cert.subject.common_name();
if (filename.is_empty()) {
filename = cert.subject.organizational_unit();
}
filename = TRY(filename.replace(" "sv, "_"sv, ReplaceMode::All));
auto file = FileSystemAccessClient::Client::the().save_file(window(), filename.to_byte_string(), "pem"sv);
if (file.is_error())
return {};
auto data = TRY(Crypto::encode_pem(cert.original_asn1));
TRY(file.release_value().release_stream()->write_until_depleted(data));
return {};
}
ErrorOr<void> CertificateStoreWidget::initialize()
{
m_root_ca_tableview = find_descendant_of_type_named<GUI::TableView>("root_ca_tableview");
m_root_ca_tableview->set_highlight_selected_rows(true);
m_root_ca_tableview->set_alternating_row_colors(false);
m_root_ca_model = CertificateStoreModel::create();
m_root_ca_proxy_model = TRY(CertificateStoreProxyModel::create(*m_root_ca_model, *m_root_ca_tableview));
m_root_ca_proxy_model->set_sort_role(GUI::ModelRole::Display);
TRY(m_root_ca_model->load());
m_root_ca_tableview->set_model(m_root_ca_proxy_model);
m_root_ca_tableview->set_column_width(CertificateStoreModel::Column::IssuedTo, 150);
m_root_ca_tableview->set_column_width(CertificateStoreModel::Column::IssuedBy, 150);
m_root_ca_tableview->on_selection_change = [&]() {
m_export_ca_button->set_enabled(m_root_ca_tableview->selection().size() == 1);
};
m_import_ca_button = find_descendant_of_type_named<GUI::Button>("import_button");
m_import_ca_button->on_click = [&](auto) {
auto import_result = import_pem();
if (import_result.is_error()) {
GUI::MessageBox::show_error(window(), ByteString::formatted("{}", import_result.release_error()));
}
};
m_export_ca_button = find_descendant_of_type_named<GUI::Button>("export_button");
m_export_ca_button->on_click = [&](auto) {
auto export_result = export_pem();
if (export_result.is_error()) {
GUI::MessageBox::show_error(window(), ByteString::formatted("{}", export_result.release_error()));
}
};
return {};
}
}