Skip to content

Commit

Permalink
Update Private Aggregation contribution bound
Browse files Browse the repository at this point in the history
Switches the bound from being per-origin per-day per-API, to per-site
per-10 min per-API. Also adds a new per-site per-day per-API looser
bound as a backstop to prevent worst-case leakage. The budget data is
stored on disk in 1440 separate minute-long windows, replacing the 24
hour-long windows before. We do not use separate stores for the two
budgets.

This cl also bumps the storage version, which will raze any existing
stored budgets. It also updates the data deletion logic to handle the
per-site scope, retaining references to the origins that have used it
in the last day to support per-origin deletion. (Note that all data for
the site will be deleted in case of an origin match, however.)

See also
patcg-individual-drafts/private-aggregation-api#45

Bug: 1446822
Change-Id: I877a8e6f2e7a8a5b9ea308ee07eadd22112c5416
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4543883
Commit-Queue: David Bokan <bokan@chromium.org>
Reviewed-by: Nan Lin <linnan@chromium.org>
Reviewed-by: Charlie Harrison <csharrison@chromium.org>
Reviewed-by: David Bokan <bokan@chromium.org>
Commit-Queue: Alex Turner <alexmt@chromium.org>
Auto-Submit: Alex Turner <alexmt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1150638}
  • Loading branch information
alexmturner authored and Chromium LUCI CQ committed May 30, 2023
1 parent b21bdc5 commit fc2210d
Show file tree
Hide file tree
Showing 14 changed files with 865 additions and 353 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

#include "content/browser/private_aggregation/private_aggregation_budget_key.h"

#include <utility>

#include "base/check.h"
#include "base/time/time.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
Expand All @@ -24,9 +22,8 @@ base::Time FloorToDuration(base::Time time) {
}
} // namespace

PrivateAggregationBudgetKey::TimeWindow::TimeWindow(
base::Time api_invocation_time)
: start_time_(FloorToDuration(api_invocation_time)) {}
PrivateAggregationBudgetKey::TimeWindow::TimeWindow(base::Time start_time)
: start_time_(FloorToDuration(start_time)) {}

PrivateAggregationBudgetKey::PrivateAggregationBudgetKey(
url::Origin origin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,25 @@
namespace content {

// Represents all information needed to record the budget usage against the
// right counter. Note that the budget limits are enforced against not per-key,
// but per-origin per-day per-API. That is, they are enforced against a set of
// budget keys with contiguous time windows spanning one 24-hour period (and
// identical `origin` and `api` fields). See
// `PrivateAggregationBudgeter::kBudgetScopeDuration`.
// right counter. Note that the budget limits are not enforced per-key, but
// instead per-site per-API per-10 min and per-site per-API per-day. That is,
// they are enforced against two sets of budget keys with contiguous time
// windows -- one spanning a 10 min period and one spanning a 24 hour period
// (both with identical `site` and `api` fields). See
// `PrivateAggregationBudgeter::kSmallerScopeValues.budget_scope_duration` and
// `PrivateAggregationBudgeter::kLargerScopeValues.budget_scope_duration`.
class CONTENT_EXPORT PrivateAggregationBudgetKey {
public:
enum class Api { kProtectedAudience, kSharedStorage };

// Represents a period of time for which budget usage is recorded. This
// interval includes the `start_time()` instant but excludes the end time
// (`start_time() + kDuration`) instant. (But note the `base::Time::Min()`
// `start_time()` caveat below.) No instant is included in multiple time
// windows.
// Represents the smallest period of time for which budget usage is recorded.
// This interval includes the `start_time()` instant, but excludes the end
// time (`start_time() + kDuration`) instant. (But note the
// `base::Time::Min()` start time caveat below.) No instant is included in
// multiple time windows.
class CONTENT_EXPORT TimeWindow {
public:
static constexpr base::TimeDelta kDuration = base::Hours(1);
static constexpr base::TimeDelta kDuration = base::Minutes(1);

// Constructs the window that the `api_invocation_time` lies within.
// `base::Time::Max()` is disallowed.
Expand All @@ -41,8 +43,9 @@ class CONTENT_EXPORT PrivateAggregationBudgetKey {
base::Time start_time() const { return start_time_; }

private:
// Must be 'on the hour' in UTC, or `base::Time::Min()` for the window that
// includes `base::Time::Min()` (as its start time cannot be represented.)
// Must be 'on the minute' in UTC, or `base::Time::Min()` for the window
// that includes `base::Time::Min()` (as its start time cannot be
// represented).
base::Time start_time_;

// When adding new members, the corresponding `operator==()` definition in
Expand Down Expand Up @@ -74,7 +77,8 @@ class CONTENT_EXPORT PrivateAggregationBudgetKey {
base::Time api_invocation_time,
Api api);

// `origin_` must be potentially trustworthy.
// `origin_` must be potentially trustworthy. Even though the budget is scoped
// per-site, we store the origin to support deleting the data by origin later.
url::Origin origin_;
TimeWindow time_window_;
Api api_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ namespace {
// TODO(alexmt): Consider making FromJavaTime() constexpr.
const base::Time kExampleTime = base::Time::FromJavaTime(1652984901234);

// `kExampleTime` floored to an hour boundary.
const base::Time kExampleHourBoundary = base::Time::FromJavaTime(1652983200000);
// `kExampleTime` floored to a minute boundary.
const base::Time kExampleMinuteBoundary =
base::Time::FromJavaTime(1652984880000);

constexpr char kExampleOriginUrl[] = "https://origin.example";

Expand All @@ -35,7 +36,7 @@ TEST(PrivateAggregationBudgetKeyTest, Fields_MatchInputs) {
ASSERT_TRUE(protected_audience_key.has_value());
EXPECT_EQ(protected_audience_key->origin(), example_origin);
EXPECT_EQ(protected_audience_key->time_window().start_time(),
kExampleHourBoundary);
kExampleMinuteBoundary);
EXPECT_EQ(protected_audience_key->api(),
PrivateAggregationBudgetKey::Api::kProtectedAudience);

Expand All @@ -46,12 +47,12 @@ TEST(PrivateAggregationBudgetKeyTest, Fields_MatchInputs) {
ASSERT_TRUE(shared_storage_key.has_value());
EXPECT_EQ(shared_storage_key->origin(), example_origin);
EXPECT_EQ(shared_storage_key->time_window().start_time(),
kExampleHourBoundary);
kExampleMinuteBoundary);
EXPECT_EQ(shared_storage_key->api(),
PrivateAggregationBudgetKey::Api::kSharedStorage);
}

TEST(PrivateAggregationBudgetKeyTest, StartTimes_FlooredToTheHour) {
TEST(PrivateAggregationBudgetKeyTest, StartTimes_FlooredToTheMinute) {
const url::Origin example_origin =
url::Origin::Create(GURL(kExampleOriginUrl));

Expand All @@ -60,32 +61,34 @@ TEST(PrivateAggregationBudgetKeyTest, StartTimes_FlooredToTheHour) {
example_origin, /*api_invocation_time=*/kExampleTime,
PrivateAggregationBudgetKey::Api::kProtectedAudience);
ASSERT_TRUE(example_key.has_value());
EXPECT_EQ(example_key->time_window().start_time(), kExampleHourBoundary);
EXPECT_EQ(example_key->time_window().start_time(), kExampleMinuteBoundary);

absl::optional<PrivateAggregationBudgetKey> on_the_hour =
absl::optional<PrivateAggregationBudgetKey> on_the_minute =
PrivateAggregationBudgetKey::Create(
example_origin, /*api_invocation_time=*/kExampleHourBoundary,
example_origin, /*api_invocation_time=*/kExampleMinuteBoundary,
PrivateAggregationBudgetKey::Api::kProtectedAudience);
ASSERT_TRUE(on_the_hour.has_value());
EXPECT_EQ(on_the_hour->time_window().start_time(), kExampleHourBoundary);
ASSERT_TRUE(on_the_minute.has_value());
EXPECT_EQ(on_the_minute->time_window().start_time(), kExampleMinuteBoundary);

absl::optional<PrivateAggregationBudgetKey> just_after_the_hour =
absl::optional<PrivateAggregationBudgetKey> just_after_the_minute =
PrivateAggregationBudgetKey::Create(
example_origin,
/*api_invocation_time=*/kExampleHourBoundary + base::Microseconds(1),
/*api_invocation_time=*/kExampleMinuteBoundary +
base::Microseconds(1),
PrivateAggregationBudgetKey::Api::kProtectedAudience);
ASSERT_TRUE(just_after_the_hour.has_value());
EXPECT_EQ(just_after_the_hour->time_window().start_time(),
kExampleHourBoundary);
ASSERT_TRUE(just_after_the_minute.has_value());
EXPECT_EQ(just_after_the_minute->time_window().start_time(),
kExampleMinuteBoundary);

absl::optional<PrivateAggregationBudgetKey> just_before_the_hour =
absl::optional<PrivateAggregationBudgetKey> just_before_the_minute =
PrivateAggregationBudgetKey::Create(
example_origin,
/*api_invocation_time=*/kExampleHourBoundary - base::Microseconds(1),
/*api_invocation_time=*/kExampleMinuteBoundary -
base::Microseconds(1),
PrivateAggregationBudgetKey::Api::kProtectedAudience);
ASSERT_TRUE(just_before_the_hour.has_value());
EXPECT_EQ(just_before_the_hour->time_window().start_time(),
kExampleHourBoundary - base::Hours(1));
ASSERT_TRUE(just_before_the_minute.has_value());
EXPECT_EQ(just_before_the_minute->time_window().start_time(),
kExampleMinuteBoundary - base::Minutes(1));
}

TEST(PrivateAggregationBudgetKeyTest, ExtremeStartTimes_HandledCorrectly) {
Expand All @@ -99,7 +102,8 @@ TEST(PrivateAggregationBudgetKeyTest, ExtremeStartTimes_HandledCorrectly) {
.start_time(),
base::Time::Min());

// The second earliest window should report a start time 'on the hour' again.
// The second earliest window should report a start time 'on the minute'
// again.
PrivateAggregationBudgetKey::TimeWindow second_earliest_window(
base::Time::Min() + PrivateAggregationBudgetKey::TimeWindow::kDuration);
EXPECT_NE(second_earliest_window.start_time(), base::Time::Min());
Expand All @@ -108,7 +112,7 @@ TEST(PrivateAggregationBudgetKeyTest, ExtremeStartTimes_HandledCorrectly) {
base::Time::Min() + PrivateAggregationBudgetKey::TimeWindow::kDuration);
EXPECT_EQ(
second_earliest_window.start_time().since_origin().InMicroseconds() %
base::Time::kMicrosecondsPerHour,
base::Time::kMicrosecondsPerMinute,
0);

// `base::Time::Max()` is disallowed, but otherwise the last window should
Expand All @@ -117,7 +121,7 @@ TEST(PrivateAggregationBudgetKeyTest, ExtremeStartTimes_HandledCorrectly) {
base::Microseconds(1));
EXPECT_LT(last_window.start_time(), base::Time::Max());
EXPECT_EQ(last_window.start_time().since_origin().InMicroseconds() %
base::Time::kMicrosecondsPerHour,
base::Time::kMicrosecondsPerMinute,
0);
EXPECT_LE(base::Time::Max() - last_window.start_time(),
PrivateAggregationBudgetKey::TimeWindow::kDuration);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ constexpr char kBudgetsTableName[] = "private_aggregation_api_budgets";
// When updating the database's schema, please increment the schema version.
// This will raze the database. This is not necessary for backwards-compatible
// updates to the proto format.
constexpr int kCurrentSchemaVersion = 1;
constexpr int kCurrentSchemaVersion = 2;

void RecordInitializationStatus(
PrivateAggregationBudgetStorage::InitStatus status) {
Expand Down

0 comments on commit fc2210d

Please sign in to comment.