| // Copyright 2021 The Chromium Authors. All rights reserved. |
| // 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 <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/check.h" |
| #include "base/check_op.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/sequence_checker.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/meta_table.h" |
| #include "sql/statement.h" |
| #include "sql/transaction.h" |
| #include "third_party/abseil-cpp/absl/numeric/int128.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "third_party/blink/public/common/storage_key/storage_key.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| constexpr base::FilePath::CharType kDatabasePath[] = |
| FILE_PATH_LITERAL("AggregationService"); |
| |
| // 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) |
| constexpr int kCurrentVersionNumber = 2; |
| |
| // Earliest version which can use a `kCurrentVersionNumber` database |
| // without failing. |
| constexpr int kCompatibleVersionNumber = 2; |
| |
| // Latest version of the database that cannot be upgraded to |
| // `kCurrentVersionNumber` without razing the database. |
| constexpr int kDeprecatedVersionNumber = 0; |
| |
| // 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 char 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 char 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 char kCreationTimeIndexSql[] = |
| "CREATE INDEX IF NOT EXISTS creation_time_idx ON " |
| "report_requests(creation_time)"; |
| |
| bool UpgradeAggregationServiceStorageSqlSchema(sql::Database& db, |
| sql::MetaTable& meta_table) { |
| if (meta_table.GetVersionNumber() != 1) |
| return false; // Only one migration is supported. |
| |
| // == Migrate from version 1 to 2 == |
| // Simply create the new empty table. |
| |
| sql::Transaction transaction(&db); |
| if (!transaction.Begin()) |
| return false; |
| |
| if (!db.Execute(kReportRequestsCreateTableSql)) |
| return false; |
| |
| if (!db.Execute(kReportTimeIndexSql)) |
| return false; |
| |
| if (!db.Execute(kCreationTimeIndexSql)) |
| return false; |
| |
| meta_table.SetVersionNumber(kCurrentVersionNumber); |
| return 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) |
| : run_in_memory_(run_in_memory), |
| path_to_database_(run_in_memory_ |
| ? base::FilePath() |
| : path_to_database.Append(kDatabasePath)), |
| clock_(*clock), |
| db_(sql::DatabaseOptions{.exclusive_locking = true, |
| .page_size = 4096, |
| .cache_size = 32}) { |
| DETACH_FROM_SEQUENCE(sequence_checker_); |
| DCHECK(clock); |
| |
| db_.set_histogram_tag("AggregationService"); |
| |
| // 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_); |
| DCHECK(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_); |
| DCHECK(network::IsUrlPotentiallyTrustworthy(url)); |
| DCHECK_LE(keyset.keys.size(), PublicKeyset::kMaxNumberKeys); |
| |
| // TODO(crbug.com/1231703): 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_); |
| DCHECK(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) { |
| DCHECK(!delete_begin.is_null()); |
| DCHECK(!delete_end.is_null()); |
| DCHECK(!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_); |
| DCHECK(!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) { |
| DCHECK(!keyset.fetch_time.is_null()); |
| DCHECK(!keyset.expiry_time.is_null()); |
| DCHECK(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) { |
| DCHECK_LE(key.id.size(), PublicKey::kMaxIdSize); |
| DCHECK_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) { |
| DCHECK(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) { |
| DCHECK(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(); |
| } |
| |
| 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 public keys. |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kCreateIfAbsent)) |
| 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)); |
| |
| const AggregatableReportSharedInfo& shared_info = request.shared_info(); |
| |
| store_request_statement.BindTime(0, shared_info.scheduled_report_time); |
| store_request_statement.BindTime(1, clock_.Now()); |
| store_request_statement.BindString(2, |
| shared_info.reporting_origin.Serialize()); |
| |
| 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. |
| DCHECK(!serialized_request.empty()); |
| store_request_statement.BindBlob(3, serialized_request); |
| |
| store_request_statement.Run(); |
| } |
| |
| 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(); |
| } |
| |
| absl::optional<base::Time> AggregationServiceStorageSql::NextReportTimeAfter( |
| base::Time strictly_after_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return absl::nullopt; |
| |
| 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 absl::nullopt; |
| } |
| |
| std::vector<AggregationServiceStorage::RequestAndId> |
| AggregationServiceStorageSql::GetRequestsReportingOnOrBefore( |
| base::Time not_after_time) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!EnsureDatabaseOpen(DbCreationPolicy::kFailIfAbsent)) |
| return {}; |
| |
| static constexpr char kGetRequestsSql[] = |
| "SELECT request_id,report_time,request_proto FROM report_requests " |
| "WHERE report_time<=? ORDER BY report_time"; |
| |
| sql::Statement get_requests_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, kGetRequestsSql)); |
| get_requests_statement.BindTime(0, not_after_time); |
| |
| // Partial results are not returned in case of any error. |
| // TODO(crbug.com/1340046): Limit the total number of results that can be |
| // returned in one query. |
| std::vector<AggregationServiceStorage::RequestAndId> result; |
| while (get_requests_statement.Step()) { |
| absl::optional<AggregatableReportRequest> parsed_request = |
| AggregatableReportRequest::Deserialize( |
| get_requests_statement.ColumnBlob(2)); |
| if (!parsed_request) |
| return {}; |
| |
| result.push_back(AggregationServiceStorage::RequestAndId{ |
| .request = std::move(parsed_request.value()), |
| .id = AggregationServiceStorage::RequestId( |
| get_requests_statement.ColumnInt64(0))}); |
| } |
| |
| if (!get_requests_statement.Succeeded()) |
| return {}; |
| |
| return result; |
| } |
| |
| 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) { |
| DCHECK(!delete_begin.is_null()); |
| DCHECK(!delete_end.is_null()); |
| DCHECK(!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.ColumnString(1))); |
| if (filter.is_null() || filter.Run(blink::StorageKey(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); |
| db_init_status_ = DbStatus::kClosed; |
| } |
| |
| bool AggregationServiceStorageSql::EnsureDatabaseOpen( |
| DbCreationPolicy creation_policy) { |
| if (!db_init_status_) { |
| if (run_in_memory_) { |
| db_init_status_ = DbStatus::kDeferringCreation; |
| } else { |
| db_init_status_ = base::PathExists(path_to_database_) |
| ? DbStatus::kDeferringOpen |
| : DbStatus::kDeferringCreation; |
| } |
| } |
| |
| switch (*db_init_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::kClosed: |
| return false; |
| case DbStatus::kOpen: |
| return true; |
| } |
| |
| 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_init_status_ == DbStatus::kDeferringCreation)) { |
| HandleInitializationFailure(InitStatus::kFailedToInitializeSchema); |
| return false; |
| } |
| |
| db_init_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 (!meta_table_.Init(&db_, kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| return false; |
| } |
| |
| base::UmaHistogramMediumTimes( |
| "PrivacySandbox.AggregationService.Storage.Sql.CreationTime", |
| timer.Elapsed()); |
| |
| return transaction.Commit(); |
| } |
| |
| void AggregationServiceStorageSql::DatabaseErrorCallback(int extended_error, |
| sql::Statement* stmt) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| // The default handling is to assert on debug and to ignore on release. |
| if (!sql::Database::IsExpectedSqliteError(extended_error) && |
| !ignore_errors_for_testing_) |
| DLOG(FATAL) << db_.GetErrorMessage(); |
| |
| // Consider the database closed to avoid further errors. |
| db_init_status_ = DbStatus::kClosed; |
| } |
| |
| } // namespace content |