blob: c3d002ff50cdbeadfabafc00366ea5b8099be0b4 [file] [log] [blame]
// 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/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/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::kConfigurationFileVersion);
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::kConfigurationFileMinimumReleaseVerision);
if (min_version.has_value()) {
current_config->set_minimum_release_version(min_version.value());
}
const auto max_version =
dict->FindInt(json_keys::kConfigurationFileMaximumReleaseVerision);
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) {
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