1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
|
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/policy/messaging_layer/util/upload_response_parser.h"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
#include "base/base64.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/token.h"
#include "base/types/expected.h"
#include "base/types/expected_macros.h"
#include "base/uuid.h"
#include "base/values.h"
#include "chrome/browser/policy/messaging_layer/upload/record_upload_request_builder.h"
#include "components/reporting/proto/synced/configuration_file.pb.h"
#include "components/reporting/util/encrypted_reporting_json_keys.h"
#include "components/reporting/util/reporting_errors.h"
#include "components/reporting/util/status.h"
#include "components/reporting/util/status_macros.h"
#include "components/reporting/util/statusor.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
namespace reporting {
namespace {
// Priority could come back as an int or as a std::string, this function handles
// both situations.
std::optional<Priority> GetPriorityProtoFromSequenceInformationValue(
const base::Value::Dict& sequence_information) {
const std::optional<int> int_priority_result =
sequence_information.FindInt(json_keys::kPriority);
if (int_priority_result.has_value()) {
return Priority(int_priority_result.value());
}
const std::string* str_priority_result =
sequence_information.FindString(json_keys::kPriority);
if (!str_priority_result) {
LOG(ERROR) << "Field priority is missing from SequenceInformation: "
<< sequence_information;
return std::nullopt;
}
Priority priority;
if (!Priority_Parse(*str_priority_result, &priority)) {
LOG(ERROR) << "Unable to parse field priority in SequenceInformation: "
<< sequence_information;
return std::nullopt;
}
return priority;
}
// Returns a tuple of <SequencingId, GenerationId> if `sequencing_id` and
// `generation_id` can be parsed as numbers. Returns error status otherwise.
StatusOr<std::tuple<int64_t, int64_t>> ParseSequencingIdAndGenerationId(
const std::string* sequencing_id,
const std::string* generation_id) {
int64_t seq_id;
int64_t gen_id;
if (!base::StringToInt64(*sequencing_id, &seq_id)) {
return base::unexpected(
Status(error::INVALID_ARGUMENT, "Could not parse sequencing id."));
}
if (!base::StringToInt64(*generation_id, &gen_id) || gen_id == 0) {
return base::unexpected(
Status(error::INVALID_ARGUMENT, "Could not parse generation id."));
}
return std::make_tuple(seq_id, gen_id);
}
// Destination comes back as a string from the server, transform it into a
// proto.
StatusOr<Destination> GetDestinationProto(
const std::string& destination_string) {
if (destination_string == "") {
return base::unexpected(Status(
error::NOT_FOUND, "Field destination is missing from ConfigFile"));
}
Destination destination;
if (!Destination_Parse(destination_string, &destination)) {
return base::unexpected(
Status(error::INVALID_ARGUMENT,
"Unable to parse destination from ConfigFile"));
}
// Reject undefined destination.
if (destination == UNDEFINED_DESTINATION) {
return base::unexpected(
Status(error::INVALID_ARGUMENT, "Received UNDEFINED_DESTINATION"));
}
return destination;
}
StatusOr<ConfigFile> GetConfigurationProtoFromDict(
const base::Value::Dict& file) {
ConfigFile config_file;
// Handle the version.
const auto config_file_version =
file.FindInt(json_keys::kConfigurationFileVersionResponse);
if (!config_file_version.has_value()) {
return base::unexpected(
Status(error::INVALID_ARGUMENT,
"Field version is missing from configurationFile"));
}
config_file.set_version(config_file_version.value());
// Handle the signature.
const std::string* config_file_signature_str =
file.FindString(json_keys::kConfigurationFileSignature);
if (!config_file_signature_str || config_file_signature_str->empty()) {
return base::unexpected(
Status(error::INVALID_ARGUMENT,
"Field configFileSignature is missing from configurationFile"));
}
std::string config_file_signature;
if (!base::Base64Decode(*config_file_signature_str, &config_file_signature)) {
return base::unexpected(Status(error::INVALID_ARGUMENT,
"Unable to decode configFileSignature"));
}
config_file.set_config_file_signature(config_file_signature);
auto* const event_config_result =
file.FindList(json_keys::kBlockedEventConfigs);
if (!event_config_result) {
return base::unexpected(
Status(error::INVALID_ARGUMENT,
"Field blockedEventConfigs is missing from configurationFile"));
}
// Parse the list of event configs.
for (auto& entry : *event_config_result) {
auto* const current_config = config_file.add_blocked_event_configs();
auto* const dict = entry.GetIfDict();
if (dict->empty()) {
return base::unexpected(Status(
error::INVALID_ARGUMENT, "Empty event config in configurationFile"));
}
// Find destination and turn it into a proto.
auto* const destination =
dict->FindString(json_keys::kConfigurationFileDestination);
ASSIGN_OR_RETURN(const auto proto_destination,
GetDestinationProto(*destination));
current_config->set_destination(proto_destination);
// Check if there are minimum and/or maximum release versions
// specified, if there are then we parse them and add them to the proto.
// This fields are optional so if they are not present it is okay.
const auto min_version =
dict->FindInt(json_keys::kConfigurationFileMinimumReleaseVersion);
if (min_version.has_value()) {
current_config->set_minimum_release_version(min_version.value());
}
const auto max_version =
dict->FindInt(json_keys::kConfigurationFileMaximumReleaseVersion);
if (max_version.has_value()) {
current_config->set_maximum_release_version(max_version.value());
}
}
return config_file;
}
} // namespace
UploadResponseParser::UploadResponseParser(bool is_generation_guid_required,
base::Value::Dict response)
: is_generation_guid_required_(is_generation_guid_required),
response_(std::move(response)) {}
UploadResponseParser::UploadResponseParser(UploadResponseParser&& other) =
default;
UploadResponseParser& UploadResponseParser::operator=(
UploadResponseParser&& other) = default;
UploadResponseParser::~UploadResponseParser() = default;
StatusOr<SequenceInformation>
UploadResponseParser::last_successfully_uploaded_record_sequence_info() const {
const base::Value::Dict* const
last_successfully_uploaded_record_sequence_info =
response_.FindDict(json_keys::kLastSucceedUploadedRecord);
if (last_successfully_uploaded_record_sequence_info == nullptr) {
return base::unexpected(Status(
error::NOT_FOUND,
base::StrCat({"Server responded with no lastSucceedUploadedRecord",
response_.DebugString()})));
}
ASSIGN_OR_RETURN(auto seq_info,
SequenceInformationValueToProto(
is_generation_guid_required_,
*last_successfully_uploaded_record_sequence_info));
return std::move(seq_info);
}
StatusOr<SignedEncryptionInfo> UploadResponseParser::encryption_settings()
const {
// Handle the encryption settings.
// Note: server can attach it to response regardless of whether
// the response indicates success or failure, and whether the client
// set attach_encryption_settings to true in request.
const auto* const signed_encryption_key_record =
response_.FindDict(json_keys::kEncryptionSettings);
if (signed_encryption_key_record == nullptr) {
return base::unexpected(Status(error::NOT_FOUND, "No encryption settings"));
}
std::string public_key;
{
const auto* const public_key_str =
signed_encryption_key_record->FindString(json_keys::kPublicKey);
if (public_key_str == nullptr ||
!base::Base64Decode(*public_key_str, &public_key)) {
return base::unexpected(
Status(error::FAILED_PRECONDITION,
"Public encryption key is malformed or missing"));
}
}
const auto public_key_id_result =
signed_encryption_key_record->FindInt(json_keys::kPublicKeyId);
if (!public_key_id_result.has_value()) {
return base::unexpected(Status(error::FAILED_PRECONDITION,
"Public encryption key ID is missing"));
}
std::string public_key_signature;
{
const auto* const public_key_signature_str =
signed_encryption_key_record->FindString(
json_keys::kPublicKeySignature);
if (public_key_signature_str == nullptr ||
!base::Base64Decode(*public_key_signature_str, &public_key_signature)) {
return base::unexpected(
Status(error::FAILED_PRECONDITION,
"Encryption settings signature missing or malformed"));
}
}
SignedEncryptionInfo signed_encryption_key;
signed_encryption_key.set_public_asymmetric_key(public_key);
signed_encryption_key.set_public_key_id(public_key_id_result.value());
signed_encryption_key.set_signature(public_key_signature);
return signed_encryption_key;
}
StatusOr<ConfigFile> UploadResponseParser::config_file() const {
if (!base::FeatureList::IsEnabled(kShouldRequestConfigurationFile)) {
return base::unexpected(Status(error::FAILED_PRECONDITION,
"Config file attachment not enabled"));
}
// Handle the configuration file.
// The server attaches the configuration file if it was requested
// by the client. Adding a check to make sure to only process it if the
// feature is enabled on the client side.
const base::Value::Dict* signed_configuration_file_record =
response_.FindDict(json_keys::kConfigurationFile);
if (signed_configuration_file_record == nullptr) {
return base::unexpected(
Status(error::NOT_FOUND, "Config file not attached"));
}
ASSIGN_OR_RETURN(
auto signed_configuration_file,
GetConfigurationProtoFromDict(*signed_configuration_file_record));
return std::move(signed_configuration_file);
}
StatusOr<EncryptedRecord>
UploadResponseParser::gap_record_for_permanent_failure() const {
const auto* const failed_uploaded_record = response_.FindDictByDottedPath(
base::StrCat({json_keys::kFirstFailedUploadedRecord, ".",
json_keys::kFailedUploadedRecord}));
if (failed_uploaded_record == nullptr) {
return base::unexpected(
Status(error::NOT_FOUND, "No permanent failures reporting"));
}
// if the record was after the current |highest_sequence_information_|
// we should return a gap record. A gap record consists of an
// EncryptedRecord with just SequenceInformation. The server will
// report success for the gap record and
// |highest_sequence_information_| will be updated in the next
// response. In the future there may be recoverable |failureStatus|,
// but for now all the device can do is delete the record.
ASSIGN_OR_RETURN(const auto last_succeed_uploaded,
last_successfully_uploaded_record_sequence_info());
ASSIGN_OR_RETURN(auto gap_record,
HandleFailedUploadedSequenceInformation(
is_generation_guid_required_, last_succeed_uploaded,
*failed_uploaded_record));
return std::move(gap_record);
}
bool UploadResponseParser::force_confirm_flag() const {
const auto force_confirm_flag = response_.FindBool(json_keys::kForceConfirm);
return force_confirm_flag.has_value() && force_confirm_flag.value();
}
bool UploadResponseParser::enable_upload_size_adjustment() const {
const auto enable_upload_size_adjustment =
response_.FindBool(json_keys::kEnableUploadSizeAdjustment);
return enable_upload_size_adjustment.has_value() &&
enable_upload_size_adjustment.value();
}
#if BUILDFLAG(IS_CHROMEOS)
// Returns true if `generation_guid` can be parsed as a GUID or if
// `generation_guid` does not need to be parsed based on the type of device.
// Returns false otherwise.
bool GenerationGuidIsValid(bool is_generation_guid_required,
const std::string& generation_guid) {
if (generation_guid.empty() && !is_generation_guid_required) {
// This is a legacy ChromeOS managed device and is not required to have
// a `generation_guid`.
return true;
}
// If the generation guid has some value, try to parse it.
return base::Uuid::ParseCaseInsensitive(generation_guid).is_valid();
}
// Returns true if `generation_guid` is required and missing.
// Returns false otherwise.
bool IsMissingGenerationGuid(bool is_generation_guid_required,
const std::string* generation_guid) {
if (!is_generation_guid_required) {
return false;
}
return !generation_guid || generation_guid->empty();
}
#endif // BUILDFLAG(IS_CHROMEOS)
// Returns true if any required sequence info is missing. Returns
// false otherwise.
bool IsMissingSequenceInformation(bool is_generation_guid_required,
const std::string* sequencing_id,
const std::string* generation_id,
const std::optional<Priority> priority_result,
const std::string* generation_guid) {
return !sequencing_id || !generation_id || generation_id->empty() ||
#if BUILDFLAG(IS_CHROMEOS)
IsMissingGenerationGuid(is_generation_guid_required,
generation_guid) ||
#endif // BUILDFLAG(IS_CHROMEOS)
!priority_result.has_value() ||
!Priority_IsValid(priority_result.value());
}
// static
StatusOr<SequenceInformation>
UploadResponseParser::SequenceInformationValueToProto(
bool is_generation_guid_required,
const base::Value::Dict& sequence_information_dict) {
const std::string* sequencing_id =
sequence_information_dict.FindString(json_keys::kSequencingId);
const std::string* generation_id =
sequence_information_dict.FindString(json_keys::kGenerationId);
const std::string* generation_guid =
sequence_information_dict.FindString(json_keys::kGenerationGuid);
const auto priority_result =
GetPriorityProtoFromSequenceInformationValue(sequence_information_dict);
// If required sequence info fields don't exist, or are malformed,
// return error.
// Note: `generation_guid` is allowed to be empty - managed devices
// may not have it.
if (IsMissingSequenceInformation(is_generation_guid_required, sequencing_id,
generation_id, priority_result,
generation_guid)) {
return base::unexpected(
Status(error::INVALID_ARGUMENT,
base::StrCat({"Provided value lacks some fields required by "
"SequenceInformation proto: ",
sequence_information_dict.DebugString()})));
}
ASSIGN_OR_RETURN(
(const auto [seq_id, gen_id]),
ParseSequencingIdAndGenerationId(sequencing_id, generation_id));
SequenceInformation proto;
proto.set_sequencing_id(seq_id);
proto.set_generation_id(gen_id);
proto.set_priority(Priority(priority_result.value()));
#if BUILDFLAG(IS_CHROMEOS)
// If `generation_guid` does not exist, set it to be an empty string.
const std::string gen_guid = generation_guid ? *generation_guid : "";
if (!GenerationGuidIsValid(is_generation_guid_required, gen_guid)) {
return base::unexpected(Status(
error::INVALID_ARGUMENT,
base::StrCat({"Provided value did not conform to a valid "
"SequenceInformation proto. Invalid generation guid : ",
sequence_information_dict.DebugString()})));
}
proto.set_generation_guid(gen_guid);
#endif // BUILDFLAG(IS_CHROMEOS)
return proto;
}
// static
StatusOr<EncryptedRecord>
UploadResponseParser::HandleFailedUploadedSequenceInformation(
bool is_generation_guid_required,
const SequenceInformation& highest_sequence_information,
const base::Value::Dict& sequence_information_dict) {
ASSIGN_OR_RETURN(SequenceInformation sequence_information,
SequenceInformationValueToProto(is_generation_guid_required,
sequence_information_dict));
// |seq_info| should be of the same generation, generation guid, and
// priority as highest_sequence_information, and have the next
// sequencing_id.
if (sequence_information.generation_id() !=
highest_sequence_information.generation_id() ||
sequence_information.generation_guid() !=
highest_sequence_information.generation_guid() ||
sequence_information.priority() !=
highest_sequence_information.priority() ||
sequence_information.sequencing_id() !=
highest_sequence_information.sequencing_id() + 1) {
base::UmaHistogramEnumeration(
reporting::kUmaDataLossErrorReason,
DataLossErrorReason::
FAILED_UPLOAD_CONTAINS_INVALID_SEQUENCE_INFORMATION,
DataLossErrorReason::MAX_VALUE);
return base::unexpected(
Status(error::DATA_LOSS,
base::StrCat({"Record was unprocessable by the server: ",
sequence_information_dict.DebugString()})));
}
// Build a gap record and return it.
EncryptedRecord encrypted_record;
*encrypted_record.mutable_sequence_information() =
std::move(sequence_information);
return encrypted_record;
}
} // namespace reporting
|