blob: 9c0dff89e9a5b9e3cad79ad94b17ff9e35cfce06 [file] [log] [blame]
// 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/aggregatable_report.h"
#include <stddef.h>
#include <stdint.h>
#include <limits>
#include <optional>
#include <ostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "base/base64.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/containers/flat_map.h"
#include "base/containers/span.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/json/json_writer.h"
#include "base/ranges/algorithm.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/time/time.h"
#include "base/uuid.h"
#include "base/values.h"
#include "components/aggregation_service/aggregation_coordinator_utils.h"
#include "components/aggregation_service/features.h"
#include "components/aggregation_service/parsing_utils.h"
#include "components/cbor/values.h"
#include "components/cbor/writer.h"
#include "content/browser/aggregation_service/aggregation_service_features.h"
#include "content/browser/aggregation_service/proto/aggregatable_report.pb.h"
#include "content/browser/aggregation_service/public_key.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "third_party/abseil-cpp/absl/numeric/int128.h"
#include "third_party/blink/public/mojom/private_aggregation/aggregatable_report.mojom.h"
#include "third_party/boringssl/src/include/openssl/hpke.h"
#include "third_party/distributed_point_functions/dpf/distributed_point_function.pb.h"
#include "third_party/distributed_point_functions/shim/distributed_point_function_shim.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using DpfKey = distributed_point_functions::DpfKey;
using DpfParameters = distributed_point_functions::DpfParameters;
// Payload contents:
constexpr char kHistogramValue[] = "histogram";
constexpr char kOperationKey[] = "operation";
std::vector<GURL> GetDefaultProcessingUrls(
blink::mojom::AggregationServiceMode aggregation_mode,
const std::optional<url::Origin>& aggregation_coordinator_origin) {
switch (aggregation_mode) {
case blink::mojom::AggregationServiceMode::kTeeBased:
if (base::FeatureList::IsEnabled(
aggregation_service::kAggregationServiceMultipleCloudProviders)) {
if (!aggregation_coordinator_origin.has_value()) {
return {GetAggregationServiceProcessingUrl(
::aggregation_service::GetDefaultAggregationCoordinatorOrigin())};
}
if (!::aggregation_service::IsAggregationCoordinatorOriginAllowed(
*aggregation_coordinator_origin)) {
return {};
}
return {GetAggregationServiceProcessingUrl(
*aggregation_coordinator_origin)};
} else {
return {GURL(
kPrivacySandboxAggregationServiceTrustedServerUrlAwsParam.Get())};
}
case blink::mojom::AggregationServiceMode::kExperimentalPoplar:
// TODO(crbug.com/1295705): Update default processing urls.
return {GURL("https://server1.example"), GURL("https://server2.example")};
}
}
// Returns parameters that support each possible prefix length in
// `[1, kBucketDomainBitLength]` with the same element_bitsize of
// `kValueDomainBitLength`.
std::vector<DpfParameters> ConstructDpfParameters() {
std::vector<DpfParameters> parameters(
AggregatableReport::kBucketDomainBitLength);
for (size_t i = 0; i < AggregatableReport::kBucketDomainBitLength; i++) {
parameters[i].set_log_domain_size(i + 1);
parameters[i].mutable_value_type()->mutable_integer()->set_bitsize(
AggregatableReport::kValueDomainBitLength);
}
return parameters;
}
// Returns empty vector in case of error.
std::vector<DpfKey> GenerateDpfKeys(
const AggregationServicePayloadContents& contents) {
DCHECK_EQ(contents.operation,
AggregationServicePayloadContents::Operation::kHistogram);
DCHECK_EQ(contents.aggregation_mode,
blink::mojom::AggregationServiceMode::kExperimentalPoplar);
DCHECK_EQ(contents.contributions.size(), 1u);
std::optional<std::pair<DpfKey, DpfKey>> maybe_dpf_keys =
distributed_point_functions::GenerateKeysIncremental(
ConstructDpfParameters(),
/*alpha=*/contents.contributions[0].bucket,
// We want the same beta, no matter which prefix length is used.
/*beta=*/
std::vector<absl::uint128>(AggregatableReport::kBucketDomainBitLength,
contents.contributions[0].value));
if (!maybe_dpf_keys.has_value()) {
return {};
}
std::vector<DpfKey> dpf_keys;
dpf_keys.push_back(std::move(maybe_dpf_keys->first));
dpf_keys.push_back(std::move(maybe_dpf_keys->second));
return dpf_keys;
}
// Returns a vector with a serialized CBOR map for each processing url. See
// the AggregatableReport documentation for more detail on the expected format.
// Returns an empty vector in case of error.
std::vector<std::vector<uint8_t>>
ConstructUnencryptedExperimentalPoplarPayloads(
const AggregationServicePayloadContents& payload_contents) {
std::vector<DpfKey> dpf_keys = GenerateDpfKeys(payload_contents);
if (dpf_keys.empty()) {
return {};
}
DCHECK_EQ(dpf_keys.size(), 2u);
std::vector<std::vector<uint8_t>> unencrypted_payloads;
for (const DpfKey& dpf_key : dpf_keys) {
std::vector<uint8_t> serialized_key(dpf_key.ByteSizeLong());
bool succeeded =
dpf_key.SerializeToArray(serialized_key.data(), serialized_key.size());
DCHECK(succeeded);
cbor::Value::MapValue value;
value.emplace(kOperationKey, kHistogramValue);
value.emplace("dpf_key", std::move(serialized_key));
std::optional<std::vector<uint8_t>> unencrypted_payload =
cbor::Writer::Write(cbor::Value(std::move(value)));
if (!unencrypted_payload.has_value()) {
return {};
}
unencrypted_payloads.push_back(std::move(unencrypted_payload.value()));
}
return unencrypted_payloads;
}
// TODO(crbug.com/1298196): Replace with `base::WriteBigEndian()` when available
template <typename T>
std::vector<uint8_t> EncodeIntegerForPayload(T integer) {
static_assert(sizeof(T) <= sizeof(absl::uint128),
"sizeof(T) <= sizeof(absl::uint128)");
static_assert(!std::numeric_limits<T>::is_signed,
"!std::numeric_limits<T>::is_signed");
static_assert(std::is_integral_v<T> || std::is_same_v<T, absl::uint128>,
"std::is_integral_v<T> || std::is_same_v<T, absl::uint128>");
std::vector<uint8_t> byte_string(sizeof(T));
// Construct the vector in reverse to ensure network byte (big-endian) order.
for (auto it = byte_string.rbegin(); it != byte_string.rend(); ++it) {
*it = static_cast<uint8_t>(integer & 0xFF);
integer >>= 8;
}
DCHECK_EQ(integer, 0u);
return byte_string;
}
void AppendEncodedContributionToCborArray(
cbor::Value::ArrayValue& array,
const blink::mojom::AggregatableReportHistogramContribution& contribution) {
cbor::Value::MapValue map;
map.emplace("bucket",
EncodeIntegerForPayload<absl::uint128>(contribution.bucket));
map.emplace("value", EncodeIntegerForPayload<uint32_t>(contribution.value));
array.emplace_back(std::move(map));
}
// Returns a vector with a serialized CBOR map. See the AggregatableReport
// documentation for more detail on the expected format. Returns an empty
// vector in case of error.
// Note that a vector is returned to match the `kExperimentalPoplar` case.
std::vector<std::vector<uint8_t>> ConstructUnencryptedTeeBasedPayload(
const AggregationServicePayloadContents& payload_contents) {
cbor::Value::MapValue value;
value.emplace(kOperationKey, kHistogramValue);
cbor::Value::ArrayValue data;
base::ranges::for_each(
payload_contents.contributions,
[&data](const blink::mojom::AggregatableReportHistogramContribution&
contribution) {
AppendEncodedContributionToCborArray(data, contribution);
});
int number_of_null_contributions_to_add = 0;
if (base::FeatureList::IsEnabled(
kPrivacySandboxAggregationServiceReportPadding)) {
number_of_null_contributions_to_add =
payload_contents.max_contributions_allowed -
payload_contents.contributions.size();
} else if (payload_contents.contributions.empty()) {
number_of_null_contributions_to_add = 1;
}
CHECK_GE(number_of_null_contributions_to_add, 0);
for (int i = 0; i < number_of_null_contributions_to_add; ++i) {
AppendEncodedContributionToCborArray(
data, blink::mojom::AggregatableReportHistogramContribution(
/*bucket=*/0, /*value=*/0));
}
value.emplace("data", std::move(data));
std::optional<std::vector<uint8_t>> unencrypted_payload =
cbor::Writer::Write(cbor::Value(std::move(value)));
if (!unencrypted_payload.has_value()) {
return {};
}
return {std::move(unencrypted_payload.value())};
}
// Encrypts the `plaintext` with HPKE using the processing url's
// `public_key`. Returns empty vector if the encryption fails.
std::vector<uint8_t> EncryptWithHpke(
base::span<const uint8_t> plaintext,
base::span<const uint8_t> public_key,
base::span<const uint8_t> authenticated_info) {
bssl::ScopedEVP_HPKE_CTX sender_context;
// This vector will hold the encapsulated shared secret "enc" followed by the
// symmetrically encrypted ciphertext "ct".
std::vector<uint8_t> payload(EVP_HPKE_MAX_ENC_LENGTH);
size_t encapsulated_shared_secret_len;
DCHECK_EQ(public_key.size(), PublicKey::kKeyByteLength);
if (!EVP_HPKE_CTX_setup_sender(
/*ctx=*/sender_context.get(),
/*out_enc=*/payload.data(),
/*out_enc_len=*/&encapsulated_shared_secret_len,
/*max_enc=*/payload.size(),
/*kem=*/EVP_hpke_x25519_hkdf_sha256(), /*kdf=*/EVP_hpke_hkdf_sha256(),
/*aead=*/EVP_hpke_chacha20_poly1305(),
/*peer_public_key=*/public_key.data(),
/*peer_public_key_len=*/public_key.size(),
/*info=*/authenticated_info.data(),
/*info_len=*/authenticated_info.size())) {
return {};
}
payload.resize(encapsulated_shared_secret_len + plaintext.size() +
EVP_HPKE_CTX_max_overhead(sender_context.get()));
base::span<uint8_t> ciphertext =
base::make_span(payload).subspan(encapsulated_shared_secret_len);
size_t ciphertext_len;
if (!EVP_HPKE_CTX_seal(
/*ctx=*/sender_context.get(), /*out=*/ciphertext.data(),
/*out_len=*/&ciphertext_len,
/*max_out_len=*/ciphertext.size(), /*in=*/plaintext.data(),
/*in_len*/ plaintext.size(),
/*ad=*/nullptr,
/*ad_len=*/0)) {
return {};
}
payload.resize(encapsulated_shared_secret_len + ciphertext_len);
return payload;
}
std::optional<AggregationServicePayloadContents>
ConvertPayloadContentsFromProto(
const proto::AggregationServicePayloadContents& proto) {
if (proto.operation() !=
proto::AggregationServicePayloadContents_Operation_HISTOGRAM) {
return std::nullopt;
}
AggregationServicePayloadContents::Operation operation(
AggregationServicePayloadContents::Operation::kHistogram);
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions;
for (const proto::AggregatableReportHistogramContribution&
contribution_proto : proto.contributions()) {
contributions.emplace_back(
/*bucket=*/absl::MakeUint128(contribution_proto.bucket_high(),
contribution_proto.bucket_low()),
/*value=*/contribution_proto.value());
}
blink::mojom::AggregationServiceMode aggregation_mode =
blink::mojom::AggregationServiceMode::kTeeBased;
switch (proto.aggregation_mode()) {
case proto::AggregationServiceMode::TEE_BASED:
break;
case proto::AggregationServiceMode::EXPERIMENTAL_POPLAR:
aggregation_mode =
blink::mojom::AggregationServiceMode::kExperimentalPoplar;
break;
default:
return std::nullopt;
}
std::optional<url::Origin> aggregation_coordinator_origin;
if (proto.has_aggregation_coordinator_origin()) {
aggregation_coordinator_origin =
url::Origin::Create(GURL(proto.aggregation_coordinator_origin()));
}
int max_contributions_allowed = proto.max_contributions_allowed();
if (max_contributions_allowed < 0) {
return std::nullopt;
} else if (max_contributions_allowed == 0) {
// Don't pad reports stored before padding was implemented.
max_contributions_allowed = contributions.size();
}
// Report storage doesn't support multiple aggregation coordinators.
return AggregationServicePayloadContents(
operation, std::move(contributions), aggregation_mode,
std::move(aggregation_coordinator_origin), max_contributions_allowed);
}
std::optional<AggregatableReportSharedInfo> ConvertSharedInfoFromProto(
const proto::AggregatableReportSharedInfo& proto) {
base::Time scheduled_report_time = base::Time::FromDeltaSinceWindowsEpoch(
base::Microseconds(proto.scheduled_report_time()));
base::Uuid report_id = base::Uuid::ParseLowercase(proto.report_id());
url::Origin reporting_origin =
url::Origin::Create(GURL(proto.reporting_origin()));
AggregatableReportSharedInfo::DebugMode debug_mode =
AggregatableReportSharedInfo::DebugMode::kDisabled;
switch (proto.debug_mode()) {
case proto::AggregatableReportSharedInfo_DebugMode_DISABLED:
break;
case proto::AggregatableReportSharedInfo_DebugMode_ENABLED:
debug_mode = AggregatableReportSharedInfo::DebugMode::kEnabled;
break;
default:
return std::nullopt;
}
std::string api_version = proto.api_version();
std::string api_identifier = proto.api_identifier();
return AggregatableReportSharedInfo(
scheduled_report_time, std::move(report_id), std::move(reporting_origin),
debug_mode,
// TODO(alexmt): Persist additional_fields when it becomes necessary.
/*additional_fields=*/base::Value::Dict(),
// TODO(crbug.com/1340296): Add mechanism to upgrade stored requests from
// older to newer versions.
std::move(api_version), std::move(api_identifier));
}
std::optional<AggregatableReportRequest> ConvertReportRequestFromProto(
proto::AggregatableReportRequest request_proto) {
std::optional<AggregationServicePayloadContents> payload_contents(
ConvertPayloadContentsFromProto(request_proto.payload_contents()));
if (!payload_contents.has_value()) {
return std::nullopt;
}
std::optional<AggregatableReportSharedInfo> shared_info(
ConvertSharedInfoFromProto(request_proto.shared_info()));
if (!shared_info.has_value()) {
return std::nullopt;
}
std::optional<uint64_t> debug_key;
if (request_proto.has_debug_key()) {
debug_key = request_proto.debug_key();
}
base::flat_map<std::string, std::string> additional_fields;
for (auto& elem : request_proto.additional_fields()) {
additional_fields.emplace(std::move(elem));
}
return AggregatableReportRequest::Create(
std::move(payload_contents.value()), std::move(shared_info.value()),
std::move(*request_proto.mutable_reporting_path()), debug_key,
std::move(additional_fields), request_proto.failed_send_attempts());
}
void ConvertPayloadContentsToProto(
const AggregationServicePayloadContents& payload_contents,
proto::AggregationServicePayloadContents* out) {
switch (payload_contents.operation) {
case AggregationServicePayloadContents::Operation::kHistogram:
out->set_operation(
proto::AggregationServicePayloadContents_Operation_HISTOGRAM);
}
for (const blink::mojom::AggregatableReportHistogramContribution&
contribution : payload_contents.contributions) {
proto::AggregatableReportHistogramContribution* contribution_proto =
out->add_contributions();
contribution_proto->set_bucket_high(
absl::Uint128High64(contribution.bucket));
contribution_proto->set_bucket_low(absl::Uint128Low64(contribution.bucket));
contribution_proto->set_value(contribution.value);
}
switch (payload_contents.aggregation_mode) {
case blink::mojom::AggregationServiceMode::kTeeBased:
out->set_aggregation_mode(proto::AggregationServiceMode::TEE_BASED);
break;
case blink::mojom::AggregationServiceMode::kExperimentalPoplar:
out->set_aggregation_mode(
proto::AggregationServiceMode::EXPERIMENTAL_POPLAR);
break;
}
if (base::FeatureList::IsEnabled(
aggregation_service::kAggregationServiceMultipleCloudProviders) &&
payload_contents.aggregation_coordinator_origin.has_value()) {
out->set_aggregation_coordinator_origin(
payload_contents.aggregation_coordinator_origin->Serialize());
}
out->set_max_contributions_allowed(
payload_contents.max_contributions_allowed);
}
void ConvertSharedInfoToProto(const AggregatableReportSharedInfo& shared_info,
proto::AggregatableReportSharedInfo* out) {
out->set_scheduled_report_time(
shared_info.scheduled_report_time.ToDeltaSinceWindowsEpoch()
.InMicroseconds());
out->set_report_id(shared_info.report_id.AsLowercaseString());
out->set_reporting_origin(shared_info.reporting_origin.Serialize());
switch (shared_info.debug_mode) {
case AggregatableReportSharedInfo::DebugMode::kDisabled:
out->set_debug_mode(
proto::AggregatableReportSharedInfo_DebugMode_DISABLED);
break;
case AggregatableReportSharedInfo::DebugMode::kEnabled:
out->set_debug_mode(
proto::AggregatableReportSharedInfo_DebugMode_ENABLED);
break;
}
DCHECK(shared_info.additional_fields.empty());
out->set_api_version(shared_info.api_version);
out->set_api_identifier(shared_info.api_identifier);
}
proto::AggregatableReportRequest ConvertReportRequestToProto(
const AggregatableReportRequest& request) {
proto::AggregatableReportRequest request_proto;
ConvertPayloadContentsToProto(
request.payload_contents(),
/*out=*/request_proto.mutable_payload_contents());
ConvertSharedInfoToProto(request.shared_info(),
/*out=*/request_proto.mutable_shared_info());
*request_proto.mutable_reporting_path() = request.reporting_path();
if (request.debug_key().has_value()) {
request_proto.set_debug_key(request.debug_key().value());
}
request_proto.set_failed_send_attempts(request.failed_send_attempts());
for (auto& elem : request.additional_fields()) {
(*request_proto.mutable_additional_fields())[elem.first] = elem.second;
}
return request_proto;
}
void MaybeVerifyPayloadLength(size_t max_contributions_allowed,
size_t payload_length) {
// TODO(alexmt): Replace with a more general method to ensure that the payload
// length is deterministic.
// Note that the 747 byte expectation derives from the following:
// 27 (baseline size with no contributions) + 20 * 36 (size per contribution)
if (max_contributions_allowed == 20 && payload_length != 747) {
base::debug::DumpWithoutCrashing();
}
}
} // namespace
GURL GetAggregationServiceProcessingUrl(const url::Origin& origin) {
GURL::Replacements replacements;
static constexpr char kEndpointPath[] =
".well-known/aggregation-service/v1/public-keys";
replacements.SetPathStr(kEndpointPath);
return origin.GetURL().ReplaceComponents(replacements);
}
AggregationServicePayloadContents::AggregationServicePayloadContents(
Operation operation,
std::vector<blink::mojom::AggregatableReportHistogramContribution>
contributions,
blink::mojom::AggregationServiceMode aggregation_mode,
std::optional<url::Origin> aggregation_coordinator_origin,
int max_contributions_allowed)
: operation(operation),
contributions(std::move(contributions)),
aggregation_mode(aggregation_mode),
aggregation_coordinator_origin(std::move(aggregation_coordinator_origin)),
max_contributions_allowed(max_contributions_allowed) {}
AggregationServicePayloadContents::AggregationServicePayloadContents(
const AggregationServicePayloadContents& other) = default;
AggregationServicePayloadContents& AggregationServicePayloadContents::operator=(
const AggregationServicePayloadContents& other) = default;
AggregationServicePayloadContents::AggregationServicePayloadContents(
AggregationServicePayloadContents&& other) = default;
AggregationServicePayloadContents& AggregationServicePayloadContents::operator=(
AggregationServicePayloadContents&& other) = default;
AggregationServicePayloadContents::~AggregationServicePayloadContents() =
default;
AggregatableReportSharedInfo::AggregatableReportSharedInfo(
base::Time scheduled_report_time,
base::Uuid report_id,
url::Origin reporting_origin,
DebugMode debug_mode,
base::Value::Dict additional_fields,
std::string api_version,
std::string api_identifier)
: scheduled_report_time(scheduled_report_time),
report_id(std::move(report_id)),
reporting_origin(std::move(reporting_origin)),
debug_mode(debug_mode),
additional_fields(std::move(additional_fields)),
api_version(std::move(api_version)),
api_identifier(std::move(api_identifier)) {}
AggregatableReportSharedInfo::AggregatableReportSharedInfo(
AggregatableReportSharedInfo&& other) = default;
AggregatableReportSharedInfo& AggregatableReportSharedInfo::operator=(
AggregatableReportSharedInfo&& other) = default;
AggregatableReportSharedInfo::~AggregatableReportSharedInfo() = default;
AggregatableReportSharedInfo AggregatableReportSharedInfo::Clone() const {
return AggregatableReportSharedInfo(
scheduled_report_time, report_id, reporting_origin, debug_mode,
additional_fields.Clone(), api_version, api_identifier);
}
std::string AggregatableReportSharedInfo::SerializeAsJson() const {
base::Value::Dict value;
DCHECK(report_id.is_valid());
value.Set("report_id", report_id.AsLowercaseString());
value.Set("reporting_origin", reporting_origin.Serialize());
// Encoded as the number of seconds since the Unix epoch, ignoring leap
// seconds and rounded down.
DCHECK(!scheduled_report_time.is_null());
DCHECK(!scheduled_report_time.is_inf());
value.Set("scheduled_report_time",
base::NumberToString(
scheduled_report_time.InMillisecondsSinceUnixEpoch() /
base::Time::kMillisecondsPerSecond));
value.Set("version", api_version);
value.Set("api", api_identifier);
// Only include the field if enabled.
if (debug_mode == DebugMode::kEnabled) {
value.Set("debug_mode", "enabled");
}
DCHECK(base::ranges::none_of(additional_fields, [&value](const auto& e) {
return value.contains(e.first);
})) << "Additional fields in shared_info cannot duplicate existing fields";
value.Merge(additional_fields.Clone());
std::string serialized_value;
bool succeeded = base::JSONWriter::Write(value, &serialized_value);
DCHECK(succeeded);
return serialized_value;
}
// static
std::optional<AggregatableReportRequest> AggregatableReportRequest::Create(
AggregationServicePayloadContents payload_contents,
AggregatableReportSharedInfo shared_info,
std::string reporting_path,
std::optional<uint64_t> debug_key,
base::flat_map<std::string, std::string> additional_fields,
int failed_send_attempts) {
std::vector<GURL> processing_urls =
GetDefaultProcessingUrls(payload_contents.aggregation_mode,
payload_contents.aggregation_coordinator_origin);
return CreateInternal(std::move(processing_urls), std::move(payload_contents),
std::move(shared_info), std::move(reporting_path),
debug_key, std::move(additional_fields),
failed_send_attempts);
}
// static
std::optional<AggregatableReportRequest>
AggregatableReportRequest::CreateForTesting(
std::vector<GURL> processing_urls,
AggregationServicePayloadContents payload_contents,
AggregatableReportSharedInfo shared_info,
std::string reporting_path,
std::optional<uint64_t> debug_key,
base::flat_map<std::string, std::string> additional_fields,
int failed_send_attempts) {
return CreateInternal(std::move(processing_urls), std::move(payload_contents),
std::move(shared_info), std::move(reporting_path),
debug_key, std::move(additional_fields),
failed_send_attempts);
}
// static
std::optional<AggregatableReportRequest>
AggregatableReportRequest::CreateInternal(
std::vector<GURL> processing_urls,
AggregationServicePayloadContents payload_contents,
AggregatableReportSharedInfo shared_info,
std::string reporting_path,
std::optional<uint64_t> debug_key,
base::flat_map<std::string, std::string> additional_fields,
int failed_send_attempts) {
if (!AggregatableReport::IsNumberOfProcessingUrlsValid(
processing_urls.size(), payload_contents.aggregation_mode)) {
return std::nullopt;
}
if (!base::ranges::all_of(processing_urls,
network::IsUrlPotentiallyTrustworthy)) {
return std::nullopt;
}
if (!AggregatableReport::IsNumberOfHistogramContributionsValid(
payload_contents.contributions.size(),
payload_contents.aggregation_mode)) {
return std::nullopt;
}
if (base::ranges::any_of(
payload_contents.contributions,
[](const blink::mojom::AggregatableReportHistogramContribution&
contribution) { return contribution.value < 0; })) {
return std::nullopt;
}
if (!shared_info.report_id.is_valid()) {
return std::nullopt;
}
if (debug_key.has_value() &&
shared_info.debug_mode ==
AggregatableReportSharedInfo::DebugMode::kDisabled) {
return std::nullopt;
}
if (failed_send_attempts < 0) {
return std::nullopt;
}
if (payload_contents.max_contributions_allowed <
static_cast<int>(payload_contents.contributions.size())) {
return std::nullopt;
}
// Ensure the ordering of urls is deterministic. This is required for
// AggregatableReport construction later.
base::ranges::sort(processing_urls);
return AggregatableReportRequest(
std::move(processing_urls), std::move(payload_contents),
std::move(shared_info), std::move(reporting_path), debug_key,
std::move(additional_fields), failed_send_attempts);
}
AggregatableReportRequest::AggregatableReportRequest(
std::vector<GURL> processing_urls,
AggregationServicePayloadContents payload_contents,
AggregatableReportSharedInfo shared_info,
std::string reporting_path,
std::optional<uint64_t> debug_key,
base::flat_map<std::string, std::string> additional_fields,
int failed_send_attempts)
: processing_urls_(std::move(processing_urls)),
payload_contents_(std::move(payload_contents)),
shared_info_(std::move(shared_info)),
reporting_path_(std::move(reporting_path)),
debug_key_(debug_key),
additional_fields_(std::move(additional_fields)),
failed_send_attempts_(failed_send_attempts) {}
AggregatableReportRequest::AggregatableReportRequest(
AggregatableReportRequest&& other) = default;
AggregatableReportRequest& AggregatableReportRequest::operator=(
AggregatableReportRequest&& other) = default;
AggregatableReportRequest::~AggregatableReportRequest() = default;
GURL AggregatableReportRequest::GetReportingUrl() const {
if (reporting_path_.empty()) {
return GURL();
}
return shared_info().reporting_origin.GetURL().Resolve(reporting_path_);
}
std::optional<AggregatableReportRequest> AggregatableReportRequest::Deserialize(
base::span<const uint8_t> serialized_proto) {
proto::AggregatableReportRequest request_proto;
if (!request_proto.ParseFromArray(serialized_proto.data(),
serialized_proto.size())) {
return std::nullopt;
}
return ConvertReportRequestFromProto(std::move(request_proto));
}
std::vector<uint8_t> AggregatableReportRequest::Serialize() {
proto::AggregatableReportRequest request_proto =
ConvertReportRequestToProto(*this);
size_t size = request_proto.ByteSizeLong();
std::vector<uint8_t> serialized_proto(size);
if (!request_proto.SerializeToArray(serialized_proto.data(), size)) {
return {};
}
return serialized_proto;
}
AggregatableReport::AggregationServicePayload::AggregationServicePayload(
std::vector<uint8_t> payload,
std::string key_id,
std::optional<std::vector<uint8_t>> debug_cleartext_payload)
: payload(std::move(payload)),
key_id(std::move(key_id)),
debug_cleartext_payload(std::move(debug_cleartext_payload)) {}
AggregatableReport::AggregationServicePayload::AggregationServicePayload(
const AggregatableReport::AggregationServicePayload& other) = default;
AggregatableReport::AggregationServicePayload&
AggregatableReport::AggregationServicePayload::operator=(
const AggregatableReport::AggregationServicePayload& other) = default;
AggregatableReport::AggregationServicePayload::AggregationServicePayload(
AggregatableReport::AggregationServicePayload&& other) = default;
AggregatableReport::AggregationServicePayload&
AggregatableReport::AggregationServicePayload::operator=(
AggregatableReport::AggregationServicePayload&& other) = default;
AggregatableReport::AggregationServicePayload::~AggregationServicePayload() =
default;
AggregatableReport::AggregatableReport(
std::vector<AggregationServicePayload> payloads,
std::string shared_info,
std::optional<uint64_t> debug_key,
base::flat_map<std::string, std::string> additional_fields,
std::optional<url::Origin> aggregation_coordinator_origin)
: payloads_(std::move(payloads)),
shared_info_(std::move(shared_info)),
debug_key_(debug_key),
additional_fields_(std::move(additional_fields)),
aggregation_coordinator_origin_(
std::move(aggregation_coordinator_origin)) {}
AggregatableReport::AggregatableReport(const AggregatableReport& other) =
default;
AggregatableReport& AggregatableReport::operator=(
const AggregatableReport& other) = default;
AggregatableReport::AggregatableReport(AggregatableReport&& other) = default;
AggregatableReport& AggregatableReport::operator=(AggregatableReport&& other) =
default;
AggregatableReport::~AggregatableReport() = default;
// static
bool AggregatableReport::Provider::g_disable_encryption_for_testing_tool_ =
false;
// static
void AggregatableReport::Provider::SetDisableEncryptionForTestingTool(
bool should_disable) {
g_disable_encryption_for_testing_tool_ = should_disable;
}
AggregatableReport::Provider::~Provider() = default;
std::optional<AggregatableReport>
AggregatableReport::Provider::CreateFromRequestAndPublicKeys(
const AggregatableReportRequest& report_request,
std::vector<PublicKey> public_keys) const {
const size_t num_processing_urls = public_keys.size();
DCHECK_EQ(num_processing_urls, report_request.processing_urls().size());
// The urls must be sorted so we can ensure the ordering (and assignment of
// DpfKey parties for the `kExperimentalPoplar` aggregation mode) is
// deterministic.
DCHECK(base::ranges::is_sorted(report_request.processing_urls()));
std::vector<std::vector<uint8_t>> unencrypted_payloads;
switch (report_request.payload_contents().aggregation_mode) {
case blink::mojom::AggregationServiceMode::kTeeBased: {
unencrypted_payloads = ConstructUnencryptedTeeBasedPayload(
report_request.payload_contents());
if (base::FeatureList::IsEnabled(
kPrivacySandboxAggregationServiceReportPadding)) {
MaybeVerifyPayloadLength(
report_request.payload_contents().max_contributions_allowed,
/*payload_length=*/unencrypted_payloads[0].size());
}
break;
}
case blink::mojom::AggregationServiceMode::kExperimentalPoplar: {
unencrypted_payloads = ConstructUnencryptedExperimentalPoplarPayloads(
report_request.payload_contents());
break;
}
}
if (unencrypted_payloads.empty()) {
return std::nullopt;
}
std::string encoded_shared_info =
report_request.shared_info().SerializeAsJson();
std::string authenticated_info_str =
base::StrCat({kDomainSeparationPrefix, encoded_shared_info});
base::span<const uint8_t> authenticated_info =
base::as_bytes(base::make_span(authenticated_info_str));
std::vector<AggregatableReport::AggregationServicePayload> encrypted_payloads;
DCHECK_EQ(unencrypted_payloads.size(), num_processing_urls);
for (size_t i = 0; i < num_processing_urls; ++i) {
std::vector<uint8_t> encrypted_payload =
g_disable_encryption_for_testing_tool_
? unencrypted_payloads[i]
: EncryptWithHpke(
/*plaintext=*/unencrypted_payloads[i],
/*public_key=*/public_keys[i].key,
/*authenticated_info=*/authenticated_info);
if (encrypted_payload.empty()) {
return std::nullopt;
}
std::optional<std::vector<uint8_t>> debug_cleartext_payload;
if (report_request.shared_info().debug_mode ==
AggregatableReportSharedInfo::DebugMode::kEnabled) {
debug_cleartext_payload = std::move(unencrypted_payloads[i]);
}
encrypted_payloads.emplace_back(std::move(encrypted_payload),
std::move(public_keys[i]).id,
std::move(debug_cleartext_payload));
}
return AggregatableReport(
std::move(encrypted_payloads), std::move(encoded_shared_info),
report_request.debug_key(), report_request.additional_fields(),
report_request.payload_contents().aggregation_coordinator_origin);
}
base::Value::Dict AggregatableReport::GetAsJson() const {
base::Value::Dict value;
value.Set("shared_info", shared_info_);
// When invoked for reports being shown in the WebUI, `payloads_` may be empty
// prior to assembly or if assembly failed.
if (!payloads_.empty()) {
base::Value::List payloads_list_value;
for (const AggregationServicePayload& payload : payloads_) {
base::Value::Dict payload_dict_value;
payload_dict_value.Set("payload", base::Base64Encode(payload.payload));
payload_dict_value.Set("key_id", payload.key_id);
if (payload.debug_cleartext_payload.has_value()) {
payload_dict_value.Set(
"debug_cleartext_payload",
base::Base64Encode(payload.debug_cleartext_payload.value()));
}
payloads_list_value.Append(std::move(payload_dict_value));
}
value.Set("aggregation_service_payloads", std::move(payloads_list_value));
}
if (debug_key_.has_value()) {
value.Set("debug_key", base::NumberToString(debug_key_.value()));
}
if (base::FeatureList::IsEnabled(
aggregation_service::kAggregationServiceMultipleCloudProviders)) {
value.Set(
"aggregation_coordinator_origin",
aggregation_coordinator_origin_
.value_or(
::aggregation_service::GetDefaultAggregationCoordinatorOrigin())
.Serialize());
}
for (const auto& item : additional_fields_) {
CHECK(!value.contains(item.first))
<< "Additional field duplicates existing field: " << item.first;
value.Set(item.first, item.second);
}
return value;
}
// static
bool AggregatableReport::IsNumberOfProcessingUrlsValid(
size_t number,
blink::mojom::AggregationServiceMode aggregation_mode) {
switch (aggregation_mode) {
case blink::mojom::AggregationServiceMode::kTeeBased:
return number == 1u;
case blink::mojom::AggregationServiceMode::kExperimentalPoplar:
return number == 2u;
}
}
// static
bool AggregatableReport::IsNumberOfHistogramContributionsValid(
size_t number,
blink::mojom::AggregationServiceMode aggregation_mode) {
// Note: APIs using the aggregation service may impose their own limits.
switch (aggregation_mode) {
case blink::mojom::AggregationServiceMode::kTeeBased:
return true;
case blink::mojom::AggregationServiceMode::kExperimentalPoplar:
return number == 1u;
}
}
} // namespace content