/* * Copyright (c) 2026, Luke Wilde * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include namespace WebView { static constexpr auto DATABASE_SYNCHRONIZATION_TIMER = AK::Duration::from_seconds(30); ErrorOr> HSTSStore::create(Database::Database& database) { Statements statements {}; auto create_table = TRY(database.prepare_statement("CREATE TABLE IF NOT EXISTS HSTSPolicies (" " domain TEXT PRIMARY KEY," " expiry_time INTEGER NOT NULL," " include_sub_domains BOOLEAN NOT NULL," " last_observed_time INTEGER NOT NULL" ");"sv)); database.execute_statement(create_table, {}); statements.insert_policy = TRY(database.prepare_statement("INSERT OR REPLACE INTO HSTSPolicies VALUES (?, ?, ?, ?);"sv)); statements.delete_expired = TRY(database.prepare_statement("DELETE FROM HSTSPolicies WHERE (expiry_time < ?);"sv)); statements.select_all_policies = TRY(database.prepare_statement("SELECT * FROM HSTSPolicies;"sv)); return adopt_own(*new HSTSStore { PersistedStorage { database, statements } }); } NonnullOwnPtr HSTSStore::create() { return adopt_own(*new HSTSStore { OptionalNone {} }); } HSTSStore::HSTSStore(Optional persisted_storage) : m_persisted_storage(move(persisted_storage)) { if (!m_persisted_storage.has_value()) return; auto policies = m_persisted_storage->select_all_policies(); m_transient_storage.set_policies(move(policies)); m_persisted_storage->synchronization_timer = Core::Timer::create_repeating( static_cast(DATABASE_SYNCHRONIZATION_TIMER.to_milliseconds()), [this]() { for (auto const& it : m_transient_storage.take_dirty_policies()) m_persisted_storage->insert_policy(it.key, it.value); auto now = m_transient_storage.purge_expired_policies(); m_persisted_storage->database.execute_statement(m_persisted_storage->statements.delete_expired, {}, now.milliseconds_since_epoch()); }); m_persisted_storage->synchronization_timer->start(); } HSTSStore::~HSTSStore() { if (!m_persisted_storage.has_value()) return; m_persisted_storage->synchronization_timer->stop(); m_persisted_storage->synchronization_timer->on_timeout(); } // https://www.rfc-editor.org/rfc/rfc6797#section-8.1 void HSTSStore::store_policy(String const& domain, HTTP::HSTS::ParsedHSTSPolicy const& policy) { // NB: The caller is responsible for ensuring this is only called for responses received over // secure transport, and that the host is a domain (not an IP address). auto now = UnixDateTime::now(); StoredPolicy stored_policy; // A max-age value of zero (i.e., "max-age=0") signals the UA to cease regarding the host as a Known HSTS Host, // including the includeSubDomains directive (if asserted for that HSTS Host). if (policy.max_age == AK::Duration::zero()) { stored_policy = StoredPolicy { .expiry = UnixDateTime::earliest(), .include_sub_domains = false, .last_observed_time = now, }; } else { stored_policy = StoredPolicy { .expiry = now + policy.max_age, .include_sub_domains = policy.include_sub_domains, .last_observed_time = now, }; } m_transient_storage.set_policy(domain.to_ascii_lowercase(), stored_policy); m_transient_storage.purge_expired_policies(); } // https://www.rfc-editor.org/rfc/rfc6797#section-8.2 bool HSTSStore::is_known_hsts_host(StringView domain) { m_transient_storage.purge_expired_policies(); // Compare the given domain name with the domain name of each of the UA's unexpired Known HSTS Hosts. // For each Known HSTS Host's domain name, the comparison is done with the given domain name label-by-label // (comparing only labels) using an ASCII case-insensitive comparison beginning with the rightmost label, // and continuing right-to-left. auto canonical = domain.to_ascii_lowercase_string(); // Congruent Match: If a label-for-label match between a Known HSTS Host's domain name and the given domain name // is found -- i.e., there are no further labels to compare -- then the given domain name congruently matches // this Known HSTS Host. if (auto policy = m_transient_storage.get_policy(canonical); policy.has_value()) { m_transient_storage.update_last_observed_time(canonical); return true; } // Superdomain Match: If a label-for-label match between an entire Known HSTS Host's domain name and a right-hand // portion of the given domain name is found, then this Known HSTS Host's domain name is a superdomain match for // the given domain name. There could be multiple superdomain matches for a given domain name. auto remaining = canonical.bytes_as_string_view(); while (true) { auto dot = remaining.find('.'); if (!dot.has_value()) break; remaining = remaining.substring_view(*dot + 1); if (auto policy = m_transient_storage.get_policy(remaining); policy.has_value() && policy->include_sub_domains) { m_transient_storage.update_last_observed_time(remaining); return true; } } // Otherwise, if no matches are found, the given domain name does not represent a Known HSTS Host. return false; } void HSTSStore::remove_policies_observed_since(UnixDateTime since) { m_transient_storage.remove_policies_observed_since(since); } void HSTSStore::TransientStorage::set_policies(Policies policies) { m_policies = move(policies); purge_expired_policies(); } void HSTSStore::TransientStorage::set_policy(String const& domain, StoredPolicy const& policy) { auto now = UnixDateTime::now(); if (policy.expiry < now && !m_policies.contains(domain)) return; m_policies.set(domain, policy); m_dirty_policies.set(domain, policy); } Optional HSTSStore::TransientStorage::get_policy(StringView domain) const { auto it = m_policies.find(domain); if (it == m_policies.end()) return {}; return it->value; } void HSTSStore::TransientStorage::update_last_observed_time(StringView domain) { auto it = m_policies.find(domain); if (it == m_policies.end()) return; it->value.last_observed_time = UnixDateTime::now(); m_dirty_policies.set(it->key, it->value); } void HSTSStore::TransientStorage::remove_policies_observed_since(UnixDateTime since) { for (auto& [domain, policy] : m_policies) { if (policy.last_observed_time >= since) { policy.expiry = UnixDateTime::earliest(); m_dirty_policies.set(domain, policy); } } purge_expired_policies(); } UnixDateTime HSTSStore::TransientStorage::purge_expired_policies() { // A Known HSTS Host is "expired" if its cache entry has an expiry date // in the past. The UA MUST evict all expired Known HSTS Hosts from its // cache if, at any time, an expired Known HSTS Host exists in the // cache. auto now = UnixDateTime::now(); auto is_expired = [&](auto const&, auto const& policy) { return policy.expiry < now; }; m_policies.remove_all_matching(is_expired); return now; } void HSTSStore::PersistedStorage::insert_policy(String const& domain, StoredPolicy const& policy) { database.execute_statement(statements.insert_policy, {}, domain, policy.expiry, policy.include_sub_domains, policy.last_observed_time); } HSTSStore::TransientStorage::Policies HSTSStore::PersistedStorage::select_all_policies() { TransientStorage::Policies policies; database.execute_statement(statements.select_all_policies, [&](auto row) { auto domain = database.result_column(row, 0); StoredPolicy stored_policy { .expiry = database.result_column(row, 1), .include_sub_domains = database.result_column(row, 2), .last_observed_time = database.result_column(row, 3), }; policies.set(move(domain), stored_policy); }); return policies; } }