| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/browser/aggregation_service/aggregation_service_storage_sql.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/containers/span.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/sequence_checker.h" |
| #include "base/strings/cstring_view.h" |
| #include "base/time/clock.h" |
| #include "base/time/time.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "content/browser/aggregation_service/aggregatable_report.h" |
| #include "content/browser/aggregation_service/aggregation_service_storage.h" |
| #include "content/browser/aggregation_service/proto/aggregatable_report.pb.h" |
| #include "content/browser/aggregation_service/public_key.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "services/network/public/cpp/is_potentially_trustworthy.h" |
| #include "sql/database.h" |
| #include "sql/error_delegate_util.h" |
| #include "sql/meta_table.h" |
| #include "sql/sqlite_result_code.h" |
| #include "sql/statement.h" |
| #include "sql/statement_id.h" |
| #include "sql/transaction.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| // Version number of the database. |
| // |
| // Version 1 - https://crrev.com/c/3038364 |
| // https://crrev.com/c/3462368 |
| // Version 2 - https://crrev.com/c/3733377 (adding report_requests table) |
| // Version 3 - https://crrev.com/c/3842459 (adding reporting_origin index) |
| constexpr int AggregationServiceStorageSql::kCurrentVersionNumber = 3; |
| |
| // Earliest version which can use a `kCurrentVersionNumber` database |
| // without failing. |
| constexpr int AggregationServiceStorageSql::kCompatibleVersionNumber = 3; |
| |
| // Latest version of the database that cannot be upgraded to |
| // `kCurrentVersionNumber` without razing the database. |
| constexpr int AggregationServiceStorageSql::kDeprecatedVersionNumber = 0; |
| |
| namespace { |
| |
| constexpr base::FilePath::CharType kDatabasePath[] = |
| FILE_PATH_LITERAL("AggregationService"); |
| |
| // All columns in this table except `report_time` are designed to be "const". |
| // `request_id` uses AUTOINCREMENT to ensure that IDs aren't reused over the |
| // lifetime of the DB. |
| // `report_time` is when the request should be assembled and sent |
| // `creation_time` is when the request was stored in the database and will be |
| // used for data deletion. |
| // `reporting_origin` should match the corresponding proto field, but is |
| // maintained separately for data deletion. |
| // `request_proto` is a serialized AggregatableReportRequest proto. |
| static constexpr base::cstring_view kReportRequestsCreateTableSql = |
| // clang-format off |
| "CREATE TABLE IF NOT EXISTS report_requests(" |
| "request_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," |
| "report_time INTEGER NOT NULL," |
| "creation_time INTEGER NOT NULL," |
| "reporting_origin TEXT NOT NULL," |
| "request_proto BLOB NOT NULL)"; |
| // clang-format on |
| |
| // Used to optimize report request lookup by report_time. |
| static constexpr base::cstring_view kReportTimeIndexSql = |
| "CREATE INDEX IF NOT EXISTS report_time_idx ON " |
| "report_requests(report_time)"; |
| |
| // Will be used to optimize report request lookup by creation_time for data |
| // clearing, see crbug.com/1340053. |
| static constexpr base::cstring_view kCreationTimeIndexSql = |
| "CREATE INDEX IF NOT EXISTS creation_time_idx ON " |
| "report_requests(creation_time)"; |
| |
| // Used to optimize checking whether there is capacity for the reporting origin. |
| static constexpr base::cstring_view kReportingOriginIndexSql = |
| "CREATE INDEX IF NOT EXISTS reporting_origin_idx ON " |
| "report_requests(reporting_origin)"; |
| |
| bool UpgradeAggregationServiceStorageSqlSchema(sql::Database& db, |
| sql::MetaTable& meta_table) { |
| if (meta_table.GetVersionNumber() != 1 && meta_table.GetVersionNumber() != 2) |
| return false; // Migration is not supported. |
| |
| sql::Transaction transaction(&db); |
| |
| if (!transaction.Begin()) |
| return false; |
| |
| if (meta_table.GetVersionNumber() == 1) { |
| // == Migrate from version 1 to 2 == |
| // Create the new empty table. |
| |
| if (!db.Execute(kReportRequestsCreateTableSql)) |
| return false; |
| |
| if (!db.Execute(kReportTimeIndexSql)) |
| return false; |
| |
| if (!db.Execute(kCreationTimeIndexSql)) |
| return false; |
| } |
| |
| // == Migrate from version 2 to 3 == |
| // Add the new index. |
| if (!db.Execute(kReportingOriginIndexSql)) |
| return false; |
| |
| return meta_table.SetVersionNumber( |
| AggregationServiceStorageSql::kCurrentVersionNumber) && |
| transaction.Commit(); |
| } |
| |
| void RecordInitializationStatus( |
| const AggregationServiceStorageSql::InitStatus status) { |
| base::UmaHistogramEnumeration( |
| "PrivacySandbox.AggregationService.Storage.Sql.InitStatus", status); |
| } |
| |
| } // namespace |
| |
| AggregationServiceStorageSql::AggregationServiceStorageSql( |
| bool run_in_memory, |
| const base::FilePath& path_to_database, |
| const base::Clock* clock, |
| int max_stored_requests_per_reporting_origin) |
| : run_in_memory_(run_in_memory), |
| path_to_database_(run_in_memory_ |
| ? base::FilePath() |
| : path_to_database.Append(kDatabasePath)), |
| clock_(*clock), |
| max_stored_requests_per_reporting_origin_( |
| max_stored_requests_per_reporting_origin), |
| db_(sql::DatabaseOptions().set_page_size(4096).set_cache_size(32), |
| /*tag=*/"AggregationService") { |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| CHECK(clock); |
| |
| // base::Unretained is safe here because the callback will only be called |
| // while the sql::Database in `db_` is alive, and this instance owns `db_`. |
| db_.set_error_callback( |
| base::BindRepeating(&AggregationServiceStorageSql::DatabaseErrorCallback, |
| base::Unretained(this))); |
| } |
| |
| AggregationServiceStorageSql::~AggregationServiceStorageSql() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| std::vector<PublicKey> AggregationServiceStorageSql::GetPublicKeys( |
| const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(network::IsUrlPotentiallyTrustworthy(url)); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return {}; |
| |
| static constexpr char kGetUrlIdSql[] = |
| "SELECT url_id FROM urls WHERE url = ? AND expiry_time > ?"; |
| sql::Statement get_url_id_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetUrlIdSql)); |
| get_url_id_statement.BindString(0, url.spec()); |
| get_url_id_statement.BindTime(1, clock_->Now()); |
| if (!get_url_id_statement.Step()) |
| return {}; |
| |
| int64_t url_id = get_url_id_statement.ColumnInt64(0); |
| |
| static constexpr char kGetKeysSql[] = |
| "SELECT key_id, key FROM keys WHERE url_id = ? ORDER BY url_id"; |
| |
| sql::Statement get_keys_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetKeysSql)); |
| get_keys_statement.BindInt64(0, url_id); |
| |
| // Partial results are not returned in case of any error. |
| std::vector<PublicKey> result; |
| while (get_keys_statement.Step()) { |
| if (result.size() >= PublicKeyset::kMaxNumberKeys) |
| return {}; |
| |
| std::string id = get_keys_statement.ColumnString(0); |
| |
| std::vector<uint8_t> key; |
| get_keys_statement.ColumnBlobAsVector(1, &key); |
| |
| if (id.size() > PublicKey::kMaxIdSize || |
| key.size() != PublicKey::kKeyByteLength) { |
| return {}; |
| } |
| |
| result.emplace_back(std::move(id), std::move(key)); |
| } |
| |
| if (!get_keys_statement.Succeeded()) |
| return {}; |
| |
| return result; |
| } |
| |
| void AggregationServiceStorageSql::SetPublicKeys(const GURL& url, |
| const PublicKeyset& keyset) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(network::IsUrlPotentiallyTrustworthy(url)); |
| CHECK_LE(keyset.keys.size(), PublicKeyset::kMaxNumberKeys); |
| |
| // TODO(crbug.com/40190806): Add an allowlist for helper server urls and |
| // validate the url. |
| |
| // Force the creation of the database if it doesn't exist, as we need to |
| // persist the public keys. |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kCreateIfAbsent)) |
| return; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| // Replace the public keys for the url. Deleting the existing rows and |
| // inserting new ones to reduce the complexity. |
| if (!ClearPublicKeysImpl(url)) |
| return; |
| |
| if (!InsertPublicKeysImpl(url, keyset)) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::ClearPublicKeys(const GURL& url) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(network::IsUrlPotentiallyTrustworthy(url)); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| ClearPublicKeysImpl(url); |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::ClearPublicKeysFetchedBetween( |
| base::Time delete_begin, |
| base::Time delete_end) { |
| CHECK(!delete_begin.is_null()); |
| CHECK(!delete_end.is_null()); |
| CHECK(!delete_begin.is_min() || !delete_end.is_max()); |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| static constexpr char kDeleteCandidateData[] = |
| "DELETE FROM urls WHERE fetch_time BETWEEN ? AND ? " |
| "RETURNING url_id"; |
| sql::Statement statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteCandidateData)); |
| statement.BindTime(0, delete_begin); |
| statement.BindTime(1, delete_end); |
| |
| while (statement.Step()) { |
| if (!ClearPublicKeysByUrlId(/*url_id=*/statement.ColumnInt64(0))) { |
| return; |
| } |
| } |
| |
| if (!statement.Succeeded()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::ClearPublicKeysExpiredBy( |
| base::Time delete_end) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!delete_end.is_null()); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| static constexpr char kDeleteUrlRangeSql[] = |
| "DELETE FROM urls WHERE expiry_time <= ? " |
| "RETURNING url_id"; |
| sql::Statement delete_urls_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteUrlRangeSql)); |
| delete_urls_statement.BindTime(0, delete_end); |
| |
| while (delete_urls_statement.Step()) { |
| if (!ClearPublicKeysByUrlId( |
| /*url_id=*/delete_urls_statement.ColumnInt64(0))) { |
| return; |
| } |
| } |
| |
| if (!delete_urls_statement.Succeeded()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| bool AggregationServiceStorageSql::InsertPublicKeysImpl( |
| const GURL& url, |
| const PublicKeyset& keyset) { |
| CHECK(!keyset.fetch_time.is_null()); |
| CHECK(!keyset.expiry_time.is_null()); |
| CHECK(db_.HasActiveTransactions()); |
| |
| static constexpr char kInsertUrlSql[] = |
| "INSERT INTO urls(url, fetch_time, expiry_time) VALUES (?,?,?)"; |
| |
| sql::Statement insert_url_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kInsertUrlSql)); |
| insert_url_statement.BindString(0, url.spec()); |
| insert_url_statement.BindTime(1, keyset.fetch_time); |
| insert_url_statement.BindTime(2, keyset.expiry_time); |
| |
| if (!insert_url_statement.Run()) |
| return false; |
| |
| int64_t url_id = db_.GetLastInsertRowId(); |
| |
| static constexpr char kInsertKeySql[] = |
| "INSERT INTO keys(url_id, key_id, key) VALUES (?,?,?)"; |
| sql::Statement insert_key_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kInsertKeySql)); |
| |
| for (const PublicKey& key : keyset.keys) { |
| CHECK_LE(key.id.size(), PublicKey::kMaxIdSize); |
| CHECK_EQ(key.key.size(), PublicKey::kKeyByteLength); |
| |
| insert_key_statement.Reset(/*clear_bound_vars=*/true); |
| insert_key_statement.BindInt64(0, url_id); |
| insert_key_statement.BindString(1, key.id); |
| insert_key_statement.BindBlob(2, key.key); |
| |
| if (!insert_key_statement.Run()) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool AggregationServiceStorageSql::ClearPublicKeysImpl(const GURL& url) { |
| CHECK(db_.HasActiveTransactions()); |
| |
| static constexpr char kDeleteUrlSql[] = |
| "DELETE FROM urls WHERE url = ? " |
| "RETURNING url_id"; |
| sql::Statement delete_url_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteUrlSql)); |
| delete_url_statement.BindString(0, url.spec()); |
| |
| bool has_matched_url = delete_url_statement.Step(); |
| |
| if (!delete_url_statement.Succeeded()) |
| return false; |
| |
| if (!has_matched_url) |
| return true; |
| |
| return ClearPublicKeysByUrlId( |
| /*url_id=*/delete_url_statement.ColumnInt64(0)); |
| } |
| |
| bool AggregationServiceStorageSql::ClearPublicKeysByUrlId(int64_t url_id) { |
| CHECK(db_.HasActiveTransactions()); |
| |
| static constexpr char kDeleteKeysSql[] = "DELETE FROM keys WHERE url_id = ?"; |
| sql::Statement delete_keys_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteKeysSql)); |
| delete_keys_statement.BindInt64(0, url_id); |
| return delete_keys_statement.Run(); |
| } |
| |
| void AggregationServiceStorageSql::ClearAllPublicKeys() { |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| static constexpr char kDeleteAllUrlsSql[] = "DELETE FROM urls"; |
| sql::Statement delete_all_urls_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllUrlsSql)); |
| if (!delete_all_urls_statement.Run()) |
| return; |
| |
| static constexpr char kDeleteAllKeysSql[] = "DELETE FROM keys"; |
| sql::Statement delete_all_keys_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteAllKeysSql)); |
| if (!delete_all_keys_statement.Run()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| bool AggregationServiceStorageSql::ReportingOriginHasCapacity( |
| std::string_view serialized_reporting_origin) { |
| static constexpr char kCountRequestSql[] = |
| "SELECT COUNT(*)FROM report_requests WHERE reporting_origin = ?"; |
| sql::Statement count_request_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kCountRequestSql)); |
| count_request_statement.BindString(0, serialized_reporting_origin); |
| |
| if (!count_request_statement.Step()) |
| return false; |
| |
| int64_t count = count_request_statement.ColumnInt64(0); |
| |
| // Goes above 1000 to ensure the limit is being applied correctly. |
| base::UmaHistogramCustomCounts( |
| "PrivacySandbox.AggregationService.Storage.Sql." |
| "StoredRequestsPerReportingOrigin", |
| count, /*min=*/1, /*exclusive_max=*/2000, /*buckets=*/50); |
| |
| return count < max_stored_requests_per_reporting_origin_; |
| } |
| |
| void AggregationServiceStorageSql::UpdateReportForSendFailure( |
| AggregationServiceStorage::RequestId request_id, |
| base::Time new_report_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kCreateIfAbsent)) |
| return; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| static constexpr char kGetRequestProtoSql[] = |
| "SELECT request_proto FROM report_requests WHERE request_id=?"; |
| sql::Statement get_request_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetRequestProtoSql)); |
| get_request_statement.BindInt64(0, request_id.value()); |
| |
| if (!get_request_statement.Step()) |
| return; |
| |
| base::span<const uint8_t> blob = get_request_statement.ColumnBlob(0); |
| proto::AggregatableReportRequest request_proto; |
| if (!request_proto.ParseFromArray(blob.data(), blob.size())) |
| return; |
| |
| if (request_proto.failed_send_attempts() < 0) |
| return; |
| |
| request_proto.set_failed_send_attempts(request_proto.failed_send_attempts() + |
| 1); |
| |
| size_t size = request_proto.ByteSizeLong(); |
| std::vector<uint8_t> serialized_proto(size); |
| if (!request_proto.SerializeToArray(serialized_proto.data(), size)) |
| return; |
| |
| static constexpr char kUpdateRequestSql[] = |
| "UPDATE report_requests SET report_time=?,request_proto=? " |
| "WHERE request_id=?"; |
| |
| sql::Statement update_request_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kUpdateRequestSql)); |
| |
| update_request_statement.BindTime(0, new_report_time); |
| update_request_statement.BindBlob(1, serialized_proto); |
| update_request_statement.BindInt64(2, request_id.value()); |
| |
| if (!update_request_statement.Run()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::StoreRequest( |
| AggregatableReportRequest request) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| // Force the creation of the database if it doesn't exist, as we need to |
| // persist the request. |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kCreateIfAbsent)) |
| return; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| const AggregatableReportSharedInfo& shared_info = request.shared_info(); |
| std::string serialized_reporting_origin = |
| shared_info.reporting_origin.Serialize(); |
| |
| bool reporting_origin_has_capacity = |
| ReportingOriginHasCapacity(serialized_reporting_origin); |
| base::UmaHistogramBoolean( |
| "PrivacySandbox.AggregationService.Storage.Sql.StoreRequestHasCapacity", |
| reporting_origin_has_capacity); |
| |
| if (!reporting_origin_has_capacity) |
| return; |
| |
| static constexpr char kStoreRequestSql[] = |
| "INSERT INTO report_requests(" |
| "report_time,creation_time,reporting_origin,request_proto) " |
| "VALUES(?,?,?,?)"; |
| |
| sql::Statement store_request_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kStoreRequestSql)); |
| |
| store_request_statement.BindTime(0, shared_info.scheduled_report_time); |
| store_request_statement.BindTime(1, clock_->Now()); |
| store_request_statement.BindString(2, serialized_reporting_origin); |
| |
| std::vector<uint8_t> serialized_request = request.Serialize(); |
| |
| // While an empty vector can be a valid proto serialization, report requests |
| // should always be non-empty. |
| CHECK(!serialized_request.empty()); |
| store_request_statement.BindBlob(3, serialized_request); |
| |
| if (!store_request_statement.Run()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::DeleteRequest( |
| AggregationServiceStorage::RequestId request_id) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return; |
| |
| DeleteRequestImpl(request_id); |
| } |
| |
| bool AggregationServiceStorageSql::DeleteRequestImpl(RequestId request_id) { |
| static constexpr char kDeleteRequestSql[] = |
| "DELETE FROM report_requests WHERE request_id=?"; |
| |
| sql::Statement delete_request_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kDeleteRequestSql)); |
| |
| delete_request_statement.BindInt64(0, request_id.value()); |
| |
| return delete_request_statement.Run(); |
| } |
| |
| std::optional<base::Time> AggregationServiceStorageSql::NextReportTimeAfter( |
| base::Time strictly_after_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return std::nullopt; |
| |
| return NextReportTimeAfterImpl(strictly_after_time); |
| } |
| |
| std::optional<base::Time> AggregationServiceStorageSql::NextReportTimeAfterImpl( |
| base::Time strictly_after_time) { |
| static constexpr char kGetRequestsSql[] = |
| "SELECT MIN(report_time) FROM report_requests WHERE report_time>?"; |
| |
| sql::Statement get_requests_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetRequestsSql)); |
| |
| get_requests_statement.BindTime(0, strictly_after_time); |
| |
| if (get_requests_statement.Step() && |
| get_requests_statement.GetColumnType(0) != sql::ColumnType::kNull) { |
| return get_requests_statement.ColumnTime(0); |
| } |
| return std::nullopt; |
| } |
| |
| std::vector<AggregationServiceStorage::RequestAndId> |
| AggregationServiceStorageSql::GetRequestsReportingOnOrBefore( |
| base::Time not_after_time, |
| std::optional<int> limit) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| CHECK(!limit.has_value() || limit.value() > 0); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return {}; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) { |
| return {}; |
| } |
| |
| static constexpr char kGetRequestsSql[] = |
| "SELECT request_id,report_time,request_proto FROM report_requests " |
| "WHERE report_time<=? ORDER BY report_time LIMIT ?"; |
| |
| sql::Statement get_requests_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetRequestsSql)); |
| get_requests_statement.BindTime(0, not_after_time); |
| // Negative number indicates no limit. |
| // See https://www.sqlite.org/lang_select.html. |
| get_requests_statement.BindInt(1, limit.value_or(-1)); |
| |
| // TODO(crbug.com/40230192): Limit the total number of results that can be |
| // returned in one query. |
| std::vector<AggregationServiceStorage::RequestAndId> result; |
| std::vector<AggregationServiceStorage::RequestId> failures; |
| while (get_requests_statement.Step()) { |
| AggregationServiceStorage::RequestId request_id{ |
| get_requests_statement.ColumnInt64(0)}; |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize( |
| get_requests_statement.ColumnBlob(2)); |
| if (!parsed_request) { |
| failures.push_back(request_id); |
| continue; |
| } |
| |
| // Exclude internals page requests |
| if (!not_after_time.is_max()) { |
| base::UmaHistogramCustomTimes( |
| "PrivacySandbox.AggregationService.Storage.Sql." |
| "RequestDelayFromUpdatedReportTime2", |
| not_after_time - get_requests_statement.ColumnTime(1), |
| /*min=*/base::Milliseconds(1), |
| /*max=*/base::Days(24), |
| /*buckets=*/50); |
| } |
| |
| result.push_back(AggregationServiceStorage::RequestAndId{ |
| .request = std::move(parsed_request.value()), .id = request_id}); |
| } |
| |
| if (!get_requests_statement.Succeeded()) |
| return {}; |
| |
| // In case of deserialization failures, remove the request from storage. This |
| // could occur if the coordinator chosen is no longer on the allowlist. It is |
| // also possible in case of database corruption. |
| for (AggregationServiceStorage::RequestId request_id : failures) { |
| if (!DeleteRequestImpl(request_id)) { |
| return {}; |
| } |
| } |
| |
| if (!transaction.Commit()) { |
| return {}; |
| } |
| |
| return result; |
| } |
| |
| std::vector<AggregationServiceStorage::RequestAndId> |
| AggregationServiceStorageSql::GetRequests( |
| const std::vector<AggregationServiceStorage::RequestId>& ids) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return {}; |
| |
| static constexpr char kGetRequestSql[] = |
| "SELECT request_id,request_proto FROM report_requests " |
| "WHERE request_id=?"; |
| sql::Statement statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetRequestSql)); |
| |
| std::vector<AggregationServiceStorage::RequestAndId> result; |
| for (AggregationServiceStorage::RequestId id : ids) { |
| statement.Reset(/*clear_bound_vars=*/true); |
| statement.BindInt64(0, *id); |
| if (!statement.Step()) |
| continue; |
| std::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize(statement.ColumnBlob(1)); |
| if (!parsed_request) |
| continue; |
| result.push_back(AggregationServiceStorage::RequestAndId{ |
| .request = std::move(*parsed_request), |
| .id = id, |
| }); |
| } |
| return result; |
| } |
| |
| std::optional<base::Time> |
| AggregationServiceStorageSql::AdjustOfflineReportTimes( |
| base::Time now, |
| base::TimeDelta min_delay, |
| base::TimeDelta max_delay) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| CHECK_GE(min_delay, base::TimeDelta()); |
| CHECK_GE(max_delay, base::TimeDelta()); |
| CHECK_LE(min_delay, max_delay); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return std::nullopt; |
| |
| // Set the report time for all reports that should have been sent before `now` |
| // to `now` + a random number of microseconds between `min_delay` and |
| // `max_delay`, both inclusive. We use RANDOM, instead of a C++ method to |
| // avoid having to pull all reports into memory and update them one by one. We |
| // use ABS because RANDOM may return a negative integer. We add 1 to the |
| // difference between `max_delay` and `min_delay` to ensure that the range of |
| // generated values is inclusive. If `max_delay == min_delay`, we take the |
| // remainder modulo 1, which is always 0. |
| static constexpr char kAdjustOfflineReportTimesSql[] = |
| "UPDATE report_requests SET report_time=?+ABS(RANDOM()%?)" |
| "WHERE report_time<?"; |
| |
| sql::Statement statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kAdjustOfflineReportTimesSql)); |
| statement.BindTime(0, now + min_delay); |
| statement.BindInt64(1, 1 + (max_delay - min_delay).InMicroseconds()); |
| statement.BindTime(2, now); |
| |
| statement.Run(); |
| |
| return NextReportTimeAfterImpl(base::Time::Min()); |
| } |
| |
| std::set<url::Origin> |
| AggregationServiceStorageSql::GetReportRequestReportingOrigins() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) { |
| return {}; |
| } |
| |
| std::set<url::Origin> origins; |
| static constexpr char kSelectRequestReportingOrigins[] = |
| "SELECT reporting_origin FROM report_requests"; |
| sql::Statement statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kSelectRequestReportingOrigins)); |
| |
| while (statement.Step()) { |
| url::Origin reporting_origin = |
| url::Origin::Create(GURL(statement.ColumnStringView(0))); |
| if (reporting_origin.opaque()) { |
| continue; |
| } |
| origins.insert(std::move(reporting_origin)); |
| } |
| |
| return origins; |
| } |
| |
| void AggregationServiceStorageSql::ClearDataBetween( |
| base::Time delete_begin, |
| base::Time delete_end, |
| StoragePartition::StorageKeyMatcherFunction filter) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return; |
| |
| // Treat null times as unbounded lower or upper range. This is used by |
| // browsing data remover. |
| if (delete_begin.is_null()) |
| delete_begin = base::Time::Min(); |
| |
| if (delete_end.is_null()) |
| delete_end = base::Time::Max(); |
| |
| if (delete_begin.is_min() && delete_end.is_max()) { |
| ClearAllPublicKeys(); |
| |
| if (filter.is_null()) { |
| ClearAllRequests(); |
| return; |
| } |
| } else { |
| ClearPublicKeysFetchedBetween(delete_begin, delete_end); |
| } |
| |
| ClearRequestsStoredBetween(delete_begin, delete_end, filter); |
| } |
| |
| void AggregationServiceStorageSql::ClearRequestsStoredBetween( |
| base::Time delete_begin, |
| base::Time delete_end, |
| StoragePartition::StorageKeyMatcherFunction filter) { |
| CHECK(!delete_begin.is_null()); |
| CHECK(!delete_end.is_null()); |
| CHECK(!delete_begin.is_min() || !delete_end.is_max() || !filter.is_null()); |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return; |
| |
| static constexpr char kSelectRequestsToDeleteSql[] = |
| "SELECT request_id,reporting_origin FROM report_requests " |
| "WHERE creation_time BETWEEN ? AND ?"; |
| sql::Statement select_requests_to_delete_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kSelectRequestsToDeleteSql)); |
| select_requests_to_delete_statement.BindTime(0, delete_begin); |
| select_requests_to_delete_statement.BindTime(1, delete_end); |
| |
| while (select_requests_to_delete_statement.Step()) { |
| url::Origin reporting_origin = url::Origin::Create( |
| GURL(select_requests_to_delete_statement.ColumnStringView(1))); |
| if (filter.is_null() || |
| filter.Run(blink::StorageKey::CreateFirstParty(reporting_origin))) { |
| if (!DeleteRequestImpl( |
| RequestId(select_requests_to_delete_statement.ColumnInt64(0)))) { |
| return; |
| } |
| } |
| } |
| |
| if (!select_requests_to_delete_statement.Succeeded()) |
| return; |
| |
| transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::ClearAllRequests() { |
| static constexpr char kClearAllRequests[] = "DELETE FROM report_requests"; |
| sql::Statement select_requests_to_delete_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kClearAllRequests)); |
| |
| select_requests_to_delete_statement.Run(); |
| } |
| |
| void AggregationServiceStorageSql::HandleInitializationFailure( |
| const InitStatus status) { |
| RecordInitializationStatus(status); |
| |
| meta_table_.Reset(); |
| db_.Close(); |
| |
| // It's possible that `db_status_` was set by `DatabaseErrorCallback()` during |
| // a call to `sql::Database::Open()`. Some databases attempt recovery at this |
| // point, but we opt to delete the database from disk. Recovery can always |
| // result in partial data loss, even when it appears to succeed. SQLite's |
| // documentation discusses how some use cases can tolerate partial data loss, |
| // while others cannot: <https://www.sqlite.org/recovery.html>. |
| if (db_status_ == DbStatus::kClosedDueToCatastrophicError) { |
| const bool delete_ok = sql::Database::Delete(path_to_database_); |
| LOG_IF(WARNING, !delete_ok) |
| << "Failed to delete database after catastrophic SQLite error"; |
| } |
| |
| db_status_ = DbStatus::kClosed; |
| } |
| |
| bool AggregationServiceStorageSql::EnsureDatabaseOpen( |
| DbCreationPolicy creation_policy) { |
| if (!db_status_) { |
| if (run_in_memory_) { |
| db_status_ = DbStatus::kDeferringCreation; |
| } else { |
| db_status_ = base::PathExists(path_to_database_) |
| ? DbStatus::kDeferringOpen |
| : DbStatus::kDeferringCreation; |
| } |
| } |
| |
| switch (*db_status_) { |
| // If the database file has not been created, we defer creation until |
| // storage needs to be used for an operation which needs to operate even on |
| // an empty database. |
| case DbStatus::kDeferringCreation: |
| if (creation_policy == DbCreationPolicy::kFailIfAbsent) |
| return false; |
| break; |
| case DbStatus::kDeferringOpen: |
| break; |
| case DbStatus::kOpen: |
| return true; |
| case DbStatus::kClosed: |
| case DbStatus::kClosedDueToCatastrophicError: |
| return false; |
| } |
| |
| if (run_in_memory_) { |
| if (!db_.OpenInMemory()) { |
| HandleInitializationFailure(InitStatus::kFailedToOpenDbInMemory); |
| return false; |
| } |
| } else { |
| const base::FilePath& dir = path_to_database_.DirName(); |
| const bool dir_exists_or_was_created = |
| base::DirectoryExists(dir) || base::CreateDirectory(dir); |
| if (!dir_exists_or_was_created) { |
| DLOG(ERROR) |
| << "Failed to create directory for AggregationService database"; |
| HandleInitializationFailure(InitStatus::kFailedToCreateDir); |
| return false; |
| } |
| if (!db_.Open(path_to_database_)) { |
| HandleInitializationFailure(InitStatus::kFailedToOpenDbFile); |
| return false; |
| } |
| } |
| |
| if (!InitializeSchema(db_status_ == DbStatus::kDeferringCreation)) { |
| HandleInitializationFailure(InitStatus::kFailedToInitializeSchema); |
| return false; |
| } |
| |
| db_status_ = DbStatus::kOpen; |
| RecordInitializationStatus(InitStatus::kSuccess); |
| return true; |
| } |
| |
| bool AggregationServiceStorageSql::InitializeSchema(bool db_empty) { |
| if (db_empty) |
| return CreateSchema(); |
| |
| if (!meta_table_.Init(&db_, kCurrentVersionNumber, kCompatibleVersionNumber)) |
| return false; |
| |
| int current_version = meta_table_.GetVersionNumber(); |
| if (current_version == kCurrentVersionNumber) |
| return true; |
| |
| if (current_version <= kDeprecatedVersionNumber || |
| meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber || |
| !UpgradeAggregationServiceStorageSqlSchema(db_, meta_table_)) { |
| // The database version is either deprecated, the version is too new to be |
| // used, or the attempt to upgrade failed. In the second case (version too |
| // new), the DB will never work until Chrome is re-upgraded. Assume the user |
| // will continue using this Chrome version and raze the DB to get |
| // aggregation service storage working. |
| db_.Raze(); |
| meta_table_.Reset(); |
| return CreateSchema(); |
| } |
| |
| return true; // Upgrade was successful. |
| } |
| |
| bool AggregationServiceStorageSql::CreateSchema() { |
| base::ElapsedThreadTimer timer; |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) |
| return false; |
| |
| // All of the columns in this table are designed to be "const". |
| // `url` is the helper server url. |
| // `fetch_time` is when the key is fetched and inserted into database, and |
| // will be used for data deletion. |
| // `expiry_time` is when the key becomes invalid and will be used for data |
| // pruning. |
| static constexpr char kUrlsTableSql[] = |
| "CREATE TABLE IF NOT EXISTS urls(" |
| " url_id INTEGER PRIMARY KEY NOT NULL," |
| " url TEXT NOT NULL," |
| " fetch_time INTEGER NOT NULL," |
| " expiry_time INTEGER NOT NULL)"; |
| if (!db_.Execute(kUrlsTableSql)) |
| return false; |
| |
| static constexpr char kUrlsByUrlIndexSql[] = |
| "CREATE UNIQUE INDEX IF NOT EXISTS urls_by_url_idx " |
| " ON urls(url)"; |
| if (!db_.Execute(kUrlsByUrlIndexSql)) |
| return false; |
| |
| // Will be used to optimize key lookup by fetch time for data clearing (see |
| // crbug.com/1231689). |
| static constexpr char kFetchTimeIndexSql[] = |
| "CREATE INDEX IF NOT EXISTS fetch_time_idx ON urls(fetch_time)"; |
| if (!db_.Execute(kFetchTimeIndexSql)) |
| return false; |
| |
| // Will be used to optimize key lookup by expiry time for data pruning (see |
| // crbug.com/1231696). |
| static constexpr char kExpiryTimeIndexSql[] = |
| "CREATE INDEX IF NOT EXISTS expiry_time_idx ON urls(expiry_time)"; |
| if (!db_.Execute(kExpiryTimeIndexSql)) |
| return false; |
| |
| // All of the columns in this table are designed to be "const". |
| // `url_id` is the primary key of a row in the `urls` table. |
| // `key_id` is an arbitrary string identifying the key which is set by helper |
| // servers and not required to be unique, but is required to be unique per |
| // url. |
| // `key` is the public key as a sequence of bytes. |
| static constexpr char kKeysTableSql[] = |
| "CREATE TABLE IF NOT EXISTS keys(" |
| " url_id INTEGER NOT NULL," |
| " key_id TEXT NOT NULL," |
| " key BLOB NOT NULL," |
| " PRIMARY KEY(url_id, key_id)) WITHOUT ROWID"; |
| if (!db_.Execute(kKeysTableSql)) |
| return false; |
| |
| // See constant definitions above for documentation. |
| if (!db_.Execute(kReportRequestsCreateTableSql)) |
| return false; |
| |
| if (!db_.Execute(kReportTimeIndexSql)) |
| return false; |
| |
| if (!db_.Execute(kCreationTimeIndexSql)) |
| return false; |
| |
| if (!db_.Execute(kReportingOriginIndexSql)) |
| return false; |
| |
| if (!meta_table_.Init(&db_, kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| return false; |
| } |
| |
| if (timer.is_supported()) { |
| base::UmaHistogramMediumTimes( |
| "PrivacySandbox.AggregationService.Storage.Sql.CreationTime2", |
| timer.Elapsed()); |
| } |
| |
| return transaction.Commit(); |
| } |
| |
| // The interaction between this error callback and `sql::Database` is complex. |
| // Here are just a few of the sharp edges: |
| // |
| // 1. This callback would become reentrant if it called a `sql::Database` method |
| // that could encounter an error. |
| // |
| // 2. This callback may be invoked multiple times by a single call to a |
| // `sql::Database` method. |
| // |
| // 3. This callback may see phantom errors that do not otherwise bubble up via |
| // return values. This can happen because `sql::Database` runs the error |
| // callback eagerly despite the fact that some of its methods ignore certain |
| // errors. |
| // |
| // A concrete example: opening the database may run the error callback *and* |
| // return true if `sql::Database::Open()` encounters a transient error, but |
| // opens the database successfully on the second try. |
| // |
| // Reducing this complexity will likely require a redesign of `sql::Database`'s |
| // error handling interface. See <https://crbug.com/40199997>. |
| void AggregationServiceStorageSql::DatabaseErrorCallback(int extended_error, |
| sql::Statement* stmt) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // Inform the test framework that we encountered this error. |
| std::ignore = sql::Database::IsExpectedSqliteError(extended_error); |
| |
| if (ignore_errors_for_testing_) { |
| return; |
| } |
| |
| // Consider the database closed to avoid further errors. Note that the value |
| // we write to `db_status_` may be subsequently overwritten elsewhere if |
| // `sql::Database` ignores the error (see sharp edge #3 above). |
| if (sql::IsErrorCatastrophic(extended_error)) { |
| db_status_ = DbStatus::kClosedDueToCatastrophicError; |
| } else { |
| db_status_ = DbStatus::kClosed; |
| } |
| |
| // Prevent future uses of `db_` from having any effect until we unpoison it |
| // with `db_.Close()`. |
| if (db_.is_open()) { |
| db_.Poison(); |
| } |
| |
| base::UmaHistogramEnumeration( |
| "PrivacySandbox.AggregationService.Storage.Sql.Error", |
| sql::ToSqliteLoggedResultCode(extended_error)); |
| } |
| |
| } // namespace content |