| // Copyright 2020 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/updater/tag.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/span.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/memory_mapped_file.h" |
| #include "base/logging.h" |
| #include "base/no_destructor.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "base/types/expected.h" |
| #include "base/uuid.h" |
| #include "build/build_config.h" |
| #include "chrome/updater/certificate_tag.h" |
| |
| #if BUILDFLAG(IS_MAC) |
| #include <sys/types.h> |
| #include <sys/xattr.h> |
| #endif // BUILDFLAG(IS_MAC) |
| |
| namespace updater::tagging { |
| namespace { |
| |
| // Magic string used to identify the tag in the binary. |
| constexpr uint8_t kTagMagicUtf8[] = {'G', 'a', 'c', 't', '2', '.', |
| '0', 'O', 'm', 'a', 'h', 'a'}; |
| |
| // These constants are conceptually cross-platform, but only currently used |
| // on Mac. |
| #if BUILDFLAG(IS_MAC) |
| // Maximum length for the string representation of a tag that can be written |
| // into a binary. This is the amount of space that must be reserved in a binary |
| // for dynamic tagging, in a file format where tags can only be patched in place |
| // rather than inserted, immediately after the magic signature and size bytes. |
| // Because binary tag format includes an explicit tag size, no null terminator |
| // is included in this count. |
| constexpr size_t kMaxTagStringBytes = 8192; |
| |
| // Maximum length for the binary representation of a tag, including its magic |
| // signature and length bytes. |
| constexpr size_t kMaxBinaryTagBytes = |
| kMaxTagStringBytes + 2 + sizeof(kTagMagicUtf8); |
| #endif // BUILDFLAG(IS_MAC) |
| |
| // The name of the bundle being installed. If not specified, the first app's |
| // appname is used. |
| constexpr std::string_view kTagArgBundleName = "bundlename"; |
| |
| // The language of the product the user is installing. |
| constexpr std::string_view kTagArgLanguage = "lang"; |
| |
| // Flag denoting that the user is flighting a new test feature. |
| constexpr std::string_view kTagArgFlighting = "flighting"; |
| |
| // Flag denoting that the user has agreed to provide usage stats, crashreports |
| // etc. |
| constexpr std::string_view kTagArgUsageStats = "usagestats"; |
| |
| // A unique value for this installation session. It can be used to follow the |
| // progress from the website to installation completion. |
| constexpr std::string_view kTagArgInstallationId = "iid"; |
| |
| // The brand code used for branding. This value sets the initial brand for the |
| // updater and the client app. If a brand value already exists on the system, |
| // the new brand value is ignored. |
| constexpr std::string_view kTagArgBrandCode = "brand"; |
| |
| // The Client ID used for branding. |
| // If a client value already exists on the system, it should be ignored. |
| // This value is used to set the initial client for the updater and the client |
| // app. |
| constexpr std::string_view kTagArgClientId = "client"; |
| |
| // A set of experiment labels used to track installs that are included in |
| // experiments. Use "experiments" for per-app arguments; use "omahaexperiments" |
| // for updater-specific labels. |
| constexpr std::string_view kAppArgExperimentLabels = "experiments"; |
| constexpr std::string_view kTagArgOmahaExperimentLabels = "omahaexperiments"; |
| |
| // A referral ID used for tracking referrals. |
| constexpr std::string_view kTagArgReferralId = "referral"; |
| |
| // Tells the updater what ap value to set in the registry. |
| constexpr std::string_view kAppArgAdditionalParameters = "ap"; |
| |
| // Indicates which browser to restart on successful install. |
| constexpr std::string_view kTagArgBrowserType = "browser"; |
| |
| // Runtime Mode: "runtime" argument in the tag tells the updater to install |
| // itself and stay on the system without any associated application for at least |
| // `kMaxServerStartsBeforeFirstReg` wakes. This feature is used to expose the |
| // COM API to a process that will install applications via that API. |
| // Example: |
| // "runtime=true&needsadmin=true" |
| constexpr std::string_view kTagArgRuntimeMode = "runtime"; |
| |
| // Enrollment token: "etoken" argument in the tag tells the per-machine updater |
| // to register the machine to the device management server. The value must be a |
| // GUID. |
| // Example: |
| // "etoken=5d086552-4514-4dfb-8a3e-337024ec35ac" |
| constexpr std::string_view kTagArgErollmentToken = "etoken"; |
| |
| // The list of arguments that are needed for a meta-installer, to |
| // indicate which application is being installed. These are stamped |
| // inside the meta-installer binary. |
| constexpr std::string_view kTagArgAppId = "appguid"; |
| constexpr std::string_view kAppArgAppName = "appname"; |
| constexpr std::string_view kTagArgNeedsAdmin = "needsadmin"; |
| constexpr std::string_view kAppArgInstallDataIndex = "installdataindex"; |
| constexpr std::string_view kAppArgUntrustedData = "untrusteddata"; |
| |
| // This switch allows extra data to be communicated to the application |
| // installer. The extra data needs to be URL-encoded. The data will be decoded |
| // and written to the file, that is then passed in the command line to the |
| // application installer in the form "/installerdata=blah.dat". One per |
| // application. |
| constexpr std::string_view kAppArgInstallerData = "installerdata"; |
| |
| // Character that is disallowed from appearing in the tag. |
| constexpr char kDisallowedCharInTag = '/'; |
| |
| std::optional<NeedsAdmin> ParseNeedsAdminEnum(std::string_view str) { |
| if (base::EqualsCaseInsensitiveASCII("false", str)) { |
| return NeedsAdmin::kNo; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII("true", str)) { |
| return NeedsAdmin::kYes; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII("prefers", str)) { |
| return NeedsAdmin::kPrefers; |
| } |
| |
| return std::nullopt; |
| } |
| |
| // Returns std::nullopt if parsing failed. |
| std::optional<bool> ParseBool(std::string_view str) { |
| if (base::EqualsCaseInsensitiveASCII("false", str)) { |
| return false; |
| } |
| |
| if (base::EqualsCaseInsensitiveASCII("true", str)) { |
| return true; |
| } |
| |
| return std::nullopt; |
| } |
| |
| // Functor used by associative containers of strings as a case-insensitive ASCII |
| // compare. `StringT` could be either UTF-8 or UTF-16. |
| struct CaseInsensitiveASCIICompare { |
| public: |
| template <typename StringT> |
| bool operator()(const StringT& x, const StringT& y) const { |
| return base::CompareCaseInsensitiveASCII(x, y) > 0; |
| } |
| }; |
| |
| namespace global_attributes { |
| |
| ErrorCode ParseBundleName(std::string_view value, TagArgs& args) { |
| value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL); |
| if (value.empty()) { |
| return ErrorCode::kGlobal_BundleNameCannotBeWhitespace; |
| } |
| |
| args.bundle_name = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseInstallationId(std::string_view value, TagArgs& args) { |
| args.installation_id = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseBrandCode(std::string_view value, TagArgs& args) { |
| args.brand_code = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseClientId(std::string_view value, TagArgs& args) { |
| args.client_id = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseOmahaExperimentLabels(std::string_view value, TagArgs& args) { |
| value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL); |
| if (value.empty()) { |
| return ErrorCode::kGlobal_ExperimentLabelsCannotBeWhitespace; |
| } |
| |
| args.experiment_labels = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseReferralId(std::string_view value, TagArgs& args) { |
| args.referral_id = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseBrowserType(std::string_view value, TagArgs& args) { |
| int browser_type = 0; |
| if (!base::StringToInt(value, &browser_type)) { |
| return ErrorCode::kGlobal_BrowserTypeIsInvalid; |
| } |
| |
| if (browser_type < 0) { |
| return ErrorCode::kGlobal_BrowserTypeIsInvalid; |
| } |
| |
| args.browser_type = |
| browser_type < base::to_underlying(TagArgs::BrowserType::kMax) |
| ? TagArgs::BrowserType(browser_type) |
| : TagArgs::BrowserType::kUnknown; |
| |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseLanguage(std::string_view value, TagArgs& args) { |
| // Even if we don't support the language, we want to pass it to the |
| // installer. Omaha will pick its language later. See http://b/1336966. |
| args.language = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseFlighting(std::string_view value, TagArgs& args) { |
| const std::optional<bool> flighting = ParseBool(value); |
| if (!flighting.has_value()) { |
| return ErrorCode::kGlobal_FlightingValueIsNotABoolean; |
| } |
| |
| args.flighting = flighting.value(); |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseUsageStats(std::string_view value, TagArgs& args) { |
| int tristate = 0; |
| if (!base::StringToInt(value, &tristate)) { |
| return ErrorCode::kGlobal_UsageStatsValueIsInvalid; |
| } |
| |
| if (tristate == 0) { |
| args.usage_stats_enable = false; |
| } else if (tristate == 1) { |
| args.usage_stats_enable = true; |
| } else if (tristate == 2) { |
| args.usage_stats_enable = std::nullopt; |
| } else { |
| return ErrorCode::kGlobal_UsageStatsValueIsInvalid; |
| } |
| return ErrorCode::kSuccess; |
| } |
| |
| // Parses an app ID and adds it to the list of apps in |args|, if valid. |
| ErrorCode ParseAppId(std::string_view value, TagArgs& args) { |
| if (!base::IsStringASCII(value)) { |
| return ErrorCode::kApp_AppIdIsNotValid; |
| } |
| |
| args.apps.push_back(AppArgs(value)); |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseRuntimeMode(std::string_view value, TagArgs& args) { |
| for (const std::string_view expected_value : {"true", "persist", "false"}) { |
| if (base::EqualsCaseInsensitiveASCII(expected_value, value)) { |
| args.runtime_mode = RuntimeModeArgs(); |
| return ErrorCode::kSuccess; |
| } |
| } |
| |
| return ErrorCode::kGlobal_RuntimeModeValueIsInvalid; |
| } |
| |
| ErrorCode ParseEnrollmentToken(std::string_view value, TagArgs& args) { |
| if (!base::Uuid::ParseCaseInsensitive(value).is_valid()) { |
| return ErrorCode::kGlobal_EnrollmentTokenValueIsInvalid; |
| } |
| args.enrollment_token = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| // |value| must not be empty. |
| using ParseGlobalAttributeFunPtr = ErrorCode (*)(std::string_view value, |
| TagArgs& args); |
| |
| using GlobalParseTable = std::map<std::string_view, |
| ParseGlobalAttributeFunPtr, |
| CaseInsensitiveASCIICompare>; |
| |
| const GlobalParseTable& GetTable() { |
| static const base::NoDestructor<GlobalParseTable> instance{ |
| {{kTagArgBundleName, &ParseBundleName}, |
| {kTagArgInstallationId, &ParseInstallationId}, |
| {kTagArgBrandCode, &ParseBrandCode}, |
| {kTagArgClientId, &ParseClientId}, |
| {kTagArgOmahaExperimentLabels, &ParseOmahaExperimentLabels}, |
| {kTagArgReferralId, &ParseReferralId}, |
| {kTagArgBrowserType, &ParseBrowserType}, |
| {kTagArgLanguage, &ParseLanguage}, |
| {kTagArgFlighting, &ParseFlighting}, |
| {kTagArgUsageStats, &ParseUsageStats}, |
| {kTagArgAppId, &ParseAppId}, |
| {kTagArgRuntimeMode, &ParseRuntimeMode}, |
| {kTagArgErollmentToken, &ParseEnrollmentToken}}}; |
| return *instance; |
| } |
| |
| } // namespace global_attributes |
| |
| namespace app_attributes { |
| |
| ErrorCode ParseAdditionalParameters(std::string_view value, AppArgs& args) { |
| args.ap = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseExperimentLabels(std::string_view value, AppArgs& args) { |
| value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL); |
| if (value.empty()) { |
| return ErrorCode::kApp_ExperimentLabelsCannotBeWhitespace; |
| } |
| |
| args.experiment_labels = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseAppName(std::string_view value, AppArgs& args) { |
| value = base::TrimWhitespaceASCII(value, base::TrimPositions::TRIM_ALL); |
| if (value.empty()) { |
| return ErrorCode::kApp_AppNameCannotBeWhitespace; |
| } |
| |
| args.app_name = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseNeedsAdmin(std::string_view value, AppArgs& args) { |
| const auto needs_admin = ParseNeedsAdminEnum(value); |
| if (!needs_admin.has_value()) { |
| return ErrorCode::kApp_NeedsAdminValueIsInvalid; |
| } |
| |
| args.needs_admin = needs_admin.value(); |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseInstallDataIndex(std::string_view value, AppArgs& args) { |
| args.install_data_index = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseUntrustedData(std::string_view value, AppArgs& args) { |
| args.untrusted_data = value; |
| return ErrorCode::kSuccess; |
| } |
| |
| // |value| must not be empty. |
| using ParseAppAttributeFunPtr = ErrorCode (*)(std::string_view value, |
| AppArgs& args); |
| |
| using AppParseTable = std:: |
| map<std::string_view, ParseAppAttributeFunPtr, CaseInsensitiveASCIICompare>; |
| |
| const AppParseTable& GetTable() { |
| static const base::NoDestructor<AppParseTable> instance{{ |
| {kAppArgAdditionalParameters, &ParseAdditionalParameters}, |
| {kAppArgExperimentLabels, &ParseExperimentLabels}, |
| {kAppArgAppName, &ParseAppName}, |
| {kTagArgNeedsAdmin, &ParseNeedsAdmin}, |
| {kAppArgInstallDataIndex, &ParseInstallDataIndex}, |
| {kAppArgUntrustedData, &ParseUntrustedData}, |
| }}; |
| return *instance; |
| } |
| |
| } // namespace app_attributes |
| |
| namespace runtime_mode_attributes { |
| |
| ErrorCode ParseNeedsAdmin(std::string_view value, RuntimeModeArgs& args) { |
| const auto needs_admin = ParseNeedsAdminEnum(value); |
| if (!needs_admin.has_value()) { |
| return ErrorCode::kRuntimeMode_NeedsAdminValueIsInvalid; |
| } |
| |
| args.needs_admin = needs_admin.value(); |
| return ErrorCode::kSuccess; |
| } |
| |
| // |value| must not be empty. |
| using ParseRuntimeModeAttributeFunPtr = ErrorCode (*)(std::string_view value, |
| RuntimeModeArgs& args); |
| |
| using RuntimeModeParseTable = std::map<std::string_view, |
| ParseRuntimeModeAttributeFunPtr, |
| CaseInsensitiveASCIICompare>; |
| |
| const RuntimeModeParseTable& GetTable() { |
| static const base::NoDestructor<RuntimeModeParseTable> instance{{ |
| {kTagArgNeedsAdmin, &ParseNeedsAdmin}, |
| }}; |
| return *instance; |
| } |
| |
| } // namespace runtime_mode_attributes |
| |
| namespace installer_data_attributes { |
| |
| // Search for the given appid specified by |value| in |args.apps| and write its |
| // index to |current_app_index|. |
| ErrorCode FindAppIdInTagArgs(std::string_view value, |
| TagArgs& args, |
| std::optional<size_t>& current_app_index) { |
| if (!base::IsStringASCII(value)) { |
| return ErrorCode::kApp_AppIdIsNotValid; |
| } |
| |
| // Find the app in the existing list. |
| for (size_t i = 0; i < args.apps.size(); i++) { |
| if (base::EqualsCaseInsensitiveASCII(args.apps[i].app_id, value)) { |
| current_app_index = i; |
| } |
| } |
| |
| if (!current_app_index.has_value()) { |
| return ErrorCode::kAppInstallerData_AppIdNotFound; |
| } |
| |
| return ErrorCode::kSuccess; |
| } |
| |
| ErrorCode ParseInstallerData(std::string_view value, |
| TagArgs& args, |
| std::optional<size_t>& current_app_index) { |
| if (!current_app_index.has_value()) { |
| return ErrorCode:: |
| kAppInstallerData_InstallerDataCannotBeSpecifiedBeforeAppId; |
| } |
| |
| args.apps[current_app_index.value()].encoded_installer_data = value; |
| |
| return ErrorCode::kSuccess; |
| } |
| |
| // |value| must not be empty. |
| // |current_app_index| is an in/out parameter. It stores the index of the |
| // current app and nullopt if no app has been set yet. Writing to it will set |
| // the index for future calls to these functions. |
| using ParseInstallerDataAttributeFunPtr = |
| ErrorCode (*)(std::string_view value, |
| TagArgs& args, |
| std::optional<size_t>& current_app_index); |
| |
| using InstallerDataParseTable = std::map<std::string_view, |
| ParseInstallerDataAttributeFunPtr, |
| CaseInsensitiveASCIICompare>; |
| |
| const InstallerDataParseTable& GetTable() { |
| static const base::NoDestructor<InstallerDataParseTable> instance{{ |
| {kTagArgAppId, &FindAppIdInTagArgs}, |
| {kAppArgInstallerData, &ParseInstallerData}, |
| }}; |
| return *instance; |
| } |
| |
| } // namespace installer_data_attributes |
| |
| namespace query_string { |
| |
| // An attribute in a metainstaller tag or app installer data args string. |
| // - The first value is the "name" of the attribute. |
| // - The second value is the "value" of the attribute. |
| using Attribute = std::pair<std::string, std::string>; |
| |
| // Splits |query_string| into |Attribute|s. Attribute values will be unescaped |
| // if |unescape_value| is true. |
| // |
| // Ownership follows the same rules as |base::SplitStringPiece|. |
| std::vector<Attribute> Split(std::string_view query_string, |
| bool unescape_value = true) { |
| std::vector<Attribute> attributes; |
| for (const auto& attribute_string : |
| base::SplitStringPiece(query_string, "&", base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_NONEMPTY)) { |
| size_t separate_pos = attribute_string.find_first_of("="); |
| if (separate_pos == std::string_view::npos) { |
| // Add a name-only attribute. |
| std::string_view name = base::TrimWhitespaceASCII( |
| attribute_string, base::TrimPositions::TRIM_ALL); |
| attributes.emplace_back(std::string{name}, ""); |
| } else { |
| std::string_view name = |
| base::TrimWhitespaceASCII(attribute_string.substr(0, separate_pos), |
| base::TrimPositions::TRIM_ALL); |
| std::string_view value = |
| base::TrimWhitespaceASCII(attribute_string.substr(separate_pos + 1), |
| base::TrimPositions::TRIM_ALL); |
| attributes.emplace_back( |
| name, |
| unescape_value |
| ? base::UnescapeURLComponent( |
| value, base::UnescapeRule::SPACES | |
| base::UnescapeRule:: |
| URL_SPECIAL_CHARS_EXCEPT_PATH_SEPARATORS | |
| base::UnescapeRule::PATH_SEPARATORS) |
| : std::string{value}); |
| } |
| } |
| return attributes; |
| } |
| |
| } // namespace query_string |
| |
| // Parses global and app-specific attributes from |tag|. |
| ErrorCode ParseTag(std::string_view tag, TagArgs& args) { |
| const auto& global_func_lookup_table = global_attributes::GetTable(); |
| const auto& app_func_lookup_table = app_attributes::GetTable(); |
| const auto& runtime_mode_func_lookup_table = |
| runtime_mode_attributes::GetTable(); |
| |
| const std::vector<std::pair<std::string, std::string>> attributes = |
| query_string::Split(tag); |
| for (const auto& [name, value] : attributes) { |
| // Attribute names are only ASCII, so no i18n case folding needed. |
| if (global_func_lookup_table.contains(name)) { |
| if (value.empty()) { |
| return ErrorCode::kAttributeMustHaveValue; |
| } |
| |
| const ErrorCode result = global_func_lookup_table.at(name)(value, args); |
| if (result != ErrorCode::kSuccess) { |
| return result; |
| } |
| } else if ((runtime_mode_func_lookup_table.contains(name)) && |
| args.runtime_mode) { |
| if (value.empty()) { |
| return ErrorCode::kAttributeMustHaveValue; |
| } |
| |
| const ErrorCode result = |
| runtime_mode_func_lookup_table.at(name)(value, *args.runtime_mode); |
| if (result != ErrorCode::kSuccess) { |
| return result; |
| } |
| } else if (app_func_lookup_table.contains(name)) { |
| if (args.apps.empty()) { |
| return ErrorCode::kApp_AppIdNotSpecified; |
| } |
| |
| if (value.empty()) { |
| return ErrorCode::kAttributeMustHaveValue; |
| } |
| |
| AppArgs& current_app = args.apps.back(); |
| const ErrorCode result = |
| app_func_lookup_table.at(name)(value, current_app); |
| if (result != ErrorCode::kSuccess) { |
| return result; |
| } |
| } else { |
| return ErrorCode::kUnrecognizedName; |
| } |
| } |
| |
| // The bundle name inherits the first app's name, if not set. |
| if (args.bundle_name.empty() && !args.apps.empty()) { |
| args.bundle_name = args.apps[0].app_name; |
| } |
| args.tag_string = tag; |
| args.attributes = attributes; |
| |
| return ErrorCode::kSuccess; |
| } |
| |
| // Parses app-specific installer data from |app_installer_data_args|. |
| ErrorCode ParseAppInstallerDataArgs(std::string_view app_installer_data_args, |
| TagArgs& args) { |
| // The currently tracked app index to apply installer data to. |
| std::optional<size_t> current_app_index; |
| |
| // Installer data is assumed to be URL-encoded, so we don't unescape it. |
| bool unescape_value = false; |
| |
| for (const auto& [name, value] : |
| query_string::Split(app_installer_data_args, unescape_value)) { |
| if (value.empty()) { |
| return ErrorCode::kAttributeMustHaveValue; |
| } |
| |
| const auto& func_lookup_table = installer_data_attributes::GetTable(); |
| if (!func_lookup_table.contains(name)) { |
| return ErrorCode::kUnrecognizedName; |
| } |
| |
| const ErrorCode result = |
| func_lookup_table.at(name)(value, args, current_app_index); |
| if (result != ErrorCode::kSuccess) { |
| return result; |
| } |
| } |
| |
| return ErrorCode::kSuccess; |
| } |
| |
| // Checks that |args| does not contain |kDisallowedCharInTag|. |
| bool IsValidArgs(std::string_view args) { |
| return !base::Contains(args, kDisallowedCharInTag); |
| } |
| |
| // Returns a `uint16_t` value as big-endian bytes. |
| std::array<uint8_t, 2> U16IntToBigEndian(uint16_t value) { |
| return {static_cast<uint8_t>((value & 0xFF00) >> 8), |
| static_cast<uint8_t>(value & 0x00FF)}; |
| } |
| |
| // Converts a big-endian 2-byte value to little-endian and returns it |
| // as a uint16_t. |
| uint16_t BigEndianReadU16(std::vector<uint8_t>::const_iterator it) { |
| static_assert(ARCH_CPU_LITTLE_ENDIAN, "Machine should be little-endian."); |
| return (uint16_t{*it} << 8) + (uint16_t{*(it + 1)}); |
| } |
| |
| // Loads up to the last 80K bytes from `filename`. |
| std::vector<uint8_t> ReadFileTail(const base::FilePath& filename) { |
| static constexpr int64_t kMaxBytesToRead = 81920; // 80K |
| |
| base::File file(filename, base::File::FLAG_OPEN | base::File::FLAG_READ); |
| if (!file.IsValid()) { |
| return {}; |
| } |
| |
| const int64_t file_length = file.GetLength(); |
| const int64_t bytes_to_read = std::min(file_length, kMaxBytesToRead); |
| const int64_t offset = |
| (file_length > bytes_to_read) ? file_length - bytes_to_read : 0; |
| |
| std::vector<uint8_t> buffer(bytes_to_read); |
| return file.ReadAndCheck(offset, base::span(buffer)) ? buffer |
| : std::vector<uint8_t>(); |
| } |
| |
| std::string ParseTagBuffer(const std::vector<uint8_t>& tag_buffer) { |
| if (tag_buffer.empty()) { |
| return {}; |
| } |
| |
| const std::string tag_string = ReadTag(tag_buffer.begin(), tag_buffer.end()); |
| LOG_IF(ERROR, tag_string.empty()) << __func__ << ": Tag not found in file."; |
| return tag_string; |
| } |
| |
| std::vector<uint8_t> ReadEntireFile(const base::FilePath& file) { |
| std::optional<int64_t> file_size = base::GetFileSize(file); |
| if (!file_size.has_value()) { |
| PLOG(ERROR) << __func__ << ": Could not get file size: " << file; |
| return {}; |
| } |
| |
| std::vector<uint8_t> contents(file_size.value()); |
| if (base::ReadFile(file, reinterpret_cast<char*>(&contents.front()), |
| contents.size()) == -1) { |
| PLOG(ERROR) << __func__ << ": Could not read file: " << file; |
| return {}; |
| } |
| return contents; |
| } |
| |
| } // namespace |
| |
| namespace internal { |
| std::vector<uint8_t>::const_iterator AdvanceIt( |
| std::vector<uint8_t>::const_iterator it, |
| size_t distance, |
| std::vector<uint8_t>::const_iterator end) { |
| if (it >= end) { |
| return end; |
| } |
| |
| ptrdiff_t dist_to_end = 0; |
| if (!base::CheckedNumeric<ptrdiff_t>(end - it).AssignIfValid(&dist_to_end)) { |
| return end; |
| } |
| |
| return it + std::min(distance, static_cast<size_t>(dist_to_end)); |
| } |
| |
| bool CheckRange(std::vector<uint8_t>::const_iterator it, |
| size_t size, |
| std::vector<uint8_t>::const_iterator end) { |
| if (it >= end || size == 0) { |
| return false; |
| } |
| |
| ptrdiff_t dist_to_end = 0; |
| if (!base::CheckedNumeric<ptrdiff_t>(end - it).AssignIfValid(&dist_to_end)) { |
| return false; |
| } |
| |
| return size <= static_cast<size_t>(dist_to_end); |
| } |
| } // namespace internal |
| |
| AppArgs::AppArgs(std::string_view app_id) : app_id(base::ToLowerASCII(app_id)) { |
| CHECK(!app_id.empty()); |
| } |
| |
| AppArgs::~AppArgs() = default; |
| AppArgs::AppArgs(const AppArgs&) = default; |
| AppArgs& AppArgs::operator=(const AppArgs&) = default; |
| AppArgs::AppArgs(AppArgs&&) = default; |
| AppArgs& AppArgs::operator=(AppArgs&&) = default; |
| |
| TagArgs::TagArgs() = default; |
| TagArgs::~TagArgs() = default; |
| TagArgs::TagArgs(const TagArgs&) = default; |
| TagArgs& TagArgs::operator=(const TagArgs&) = default; |
| TagArgs::TagArgs(TagArgs&&) = default; |
| TagArgs& TagArgs::operator=(TagArgs&&) = default; |
| |
| ErrorCode Parse(std::string_view tag, |
| std::optional<std::string_view> app_installer_data_args, |
| TagArgs& args) { |
| if (!IsValidArgs(tag)) { |
| return ErrorCode::kTagIsInvalid; |
| } |
| |
| const ErrorCode result = ParseTag(tag, args); |
| if (result != ErrorCode::kSuccess) { |
| return result; |
| } |
| |
| if (!app_installer_data_args.has_value()) { |
| return ErrorCode::kSuccess; |
| } |
| |
| if (!IsValidArgs(app_installer_data_args.value())) { |
| return ErrorCode::kTagIsInvalid; |
| } |
| |
| return ParseAppInstallerDataArgs(app_installer_data_args.value(), args); |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const ErrorCode& error_code) { |
| switch (error_code) { |
| case ErrorCode::kSuccess: |
| return os << "ErrorCode::kSuccess"; |
| case ErrorCode::kUnrecognizedName: |
| return os << "ErrorCode::kUnrecognizedName"; |
| case ErrorCode::kTagIsInvalid: |
| return os << "ErrorCode::kTagIsInvalid"; |
| case ErrorCode::kAttributeMustHaveValue: |
| return os << "ErrorCode::kAttributeMustHaveValue"; |
| case ErrorCode::kApp_AppIdNotSpecified: |
| return os << "ErrorCode::kApp_AppIdNotSpecified"; |
| case ErrorCode::kApp_ExperimentLabelsCannotBeWhitespace: |
| return os << "ErrorCode::kApp_ExperimentLabelsCannotBeWhitespace"; |
| case ErrorCode::kApp_AppIdIsNotValid: |
| return os << "ErrorCode::kApp_AppIdIsNotValid"; |
| case ErrorCode::kApp_AppNameCannotBeWhitespace: |
| return os << "ErrorCode::kApp_AppNameCannotBeWhitespace"; |
| case ErrorCode::kApp_NeedsAdminValueIsInvalid: |
| return os << "ErrorCode::kApp_NeedsAdminValueIsInvalid"; |
| case ErrorCode::kAppInstallerData_AppIdNotFound: |
| return os << "ErrorCode::kAppInstallerData_AppIdNotFound"; |
| case ErrorCode::kAppInstallerData_InstallerDataCannotBeSpecifiedBeforeAppId: |
| return os << "ErrorCode::kAppInstallerData_" |
| "InstallerDataCannotBeSpecifiedBeforeAppId"; |
| case ErrorCode::kGlobal_BundleNameCannotBeWhitespace: |
| return os << "ErrorCode::kGlobal_BundleNameCannotBeWhitespace"; |
| case ErrorCode::kGlobal_ExperimentLabelsCannotBeWhitespace: |
| return os << "ErrorCode::kGlobal_ExperimentLabelsCannotBeWhitespace"; |
| case ErrorCode::kGlobal_BrowserTypeIsInvalid: |
| return os << "ErrorCode::kGlobal_BrowserTypeIsInvalid"; |
| case ErrorCode::kGlobal_FlightingValueIsNotABoolean: |
| return os << "ErrorCode::kGlobal_FlightingValueIsNotABoolean"; |
| case ErrorCode::kGlobal_UsageStatsValueIsInvalid: |
| return os << "ErrorCode::kGlobal_UsageStatsValueIsInvalid"; |
| case ErrorCode::kGlobal_RuntimeModeValueIsInvalid: |
| return os << "ErrorCode::kGlobal_RuntimeModeValueIsInvalid"; |
| case ErrorCode::kGlobal_EnrollmentTokenValueIsInvalid: |
| return os << "ErrorCode::kGlobal_EnrollmentTokenValueIsInvalid"; |
| case ErrorCode::kRuntimeMode_NeedsAdminValueIsInvalid: |
| return os << "ErrorCode::kRuntimeMode_NeedsAdminValueIsInvalid"; |
| case ErrorCode::kTagNotFound: |
| return os << "ErrorCode::kTagNotFound"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& os, const NeedsAdmin& needs_admin) { |
| switch (needs_admin) { |
| case NeedsAdmin::kNo: |
| return os << "NeedsAdmin::kNo"; |
| case NeedsAdmin::kYes: |
| return os << "NeedsAdmin::kYes"; |
| case NeedsAdmin::kPrefers: |
| return os << "NeedsAdmin::kPrefers"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& os, |
| const TagArgs::BrowserType& browser_type) { |
| switch (browser_type) { |
| case TagArgs::BrowserType::kUnknown: |
| return os << "TagArgs::BrowserType::kUnknown"; |
| case TagArgs::BrowserType::kDefault: |
| return os << "TagArgs::BrowserType::kDefault"; |
| case TagArgs::BrowserType::kInternetExplorer: |
| return os << "TagArgs::BrowserType::kInternetExplorer"; |
| case TagArgs::BrowserType::kFirefox: |
| return os << "TagArgs::BrowserType::kFirefox"; |
| case TagArgs::BrowserType::kChrome: |
| return os << "TagArgs::BrowserType::kChrome"; |
| default: |
| return os << "TagArgs::BrowserType(" << browser_type << ")"; |
| } |
| } |
| |
| std::vector<uint8_t> GetTagFromTagString(const std::string& tag_string) { |
| std::vector<uint8_t> tag(std::begin(kTagMagicUtf8), std::end(kTagMagicUtf8)); |
| const std::array<uint8_t, 2> tag_length = |
| U16IntToBigEndian(tag_string.length()); |
| tag.insert(tag.end(), tag_length.begin(), tag_length.end()); |
| tag.insert(tag.end(), tag_string.begin(), tag_string.end()); |
| return tag; |
| } |
| |
| std::string ReadTag(std::vector<uint8_t>::const_iterator begin, |
| std::vector<uint8_t>::const_iterator end) { |
| const uint8_t* magic_begin = std::begin(kTagMagicUtf8); |
| const uint8_t* magic_end = std::end(kTagMagicUtf8); |
| |
| std::vector<uint8_t>::const_iterator magic_str = |
| std::find_end(begin, end, magic_begin, magic_end); |
| if (magic_str == end) { |
| return std::string(); |
| } |
| |
| std::vector<uint8_t>::const_iterator taglen_buf = |
| internal::AdvanceIt(magic_str, magic_end - magic_begin, end); |
| |
| // Checks that the stored tag length is found within the binary. |
| if (!internal::CheckRange(taglen_buf, sizeof(uint16_t), end)) { |
| return std::string(); |
| } |
| |
| // Tag length is stored as a big-endian uint16_t. |
| const uint16_t tag_len = BigEndianReadU16(taglen_buf); |
| |
| std::vector<uint8_t>::const_iterator tag_buf = |
| internal::AdvanceIt(taglen_buf, sizeof(uint16_t), end); |
| if (tag_buf == end) { |
| return std::string(); |
| } |
| |
| // Checks that the specified tag is found within the binary. |
| if (!internal::CheckRange(tag_buf, tag_len, end)) { |
| return std::string(); |
| } |
| |
| return std::string(tag_buf, tag_buf + tag_len); |
| } |
| |
| std::unique_ptr<tagging::BinaryInterface> CreateBinary( |
| const base::FilePath& file, |
| base::span<const uint8_t> contents) { |
| if (file.MatchesExtension(FILE_PATH_LITERAL(".exe"))) { |
| return CreatePEBinary(contents); |
| } else if (file.MatchesExtension(FILE_PATH_LITERAL(".msi"))) { |
| return CreateMSIBinary(contents); |
| } else { |
| std::unique_ptr<BinaryInterface> binary = CreatePEBinary(contents); |
| if (!binary) { |
| binary = CreateMSIBinary(contents); |
| } |
| return binary; |
| } |
| } |
| |
| std::string BinaryReadTagString(const base::FilePath& file) { |
| // For MSI files, simply search the tail of the file for the tag. |
| if (!file.MatchesExtension(FILE_PATH_LITERAL(".exe"))) { |
| return ParseTagBuffer(ReadFileTail(file)); |
| } |
| |
| base::MemoryMappedFile mapped_file; |
| if (!mapped_file.Initialize(file)) { |
| LOG(ERROR) << __func__ << ": Unknown or empty file: " << file; |
| return {}; |
| } |
| std::unique_ptr<tagging::BinaryInterface> bin = |
| CreateBinary(file, mapped_file.bytes()); |
| if (!bin) { |
| LOG(ERROR) << __func__ << ": Could not parse binary: " << file; |
| return {}; |
| } |
| |
| std::optional<std::vector<uint8_t>> tag = bin->tag(); |
| if (!tag) { |
| LOG(ERROR) << __func__ << ": No superfluous certificate in file: " << file; |
| return {}; |
| } |
| |
| const std::vector<uint8_t> tag_data = {tag->begin(), tag->end()}; |
| const std::string tag_string = ReadTag(tag_data.begin(), tag_data.end()); |
| if (tag_string.empty()) { |
| LOG(ERROR) << __func__ << ": file is untagged: " << file; |
| } |
| return tag_string; |
| } |
| |
| std::optional<tagging::TagArgs> BinaryReadTag(const base::FilePath& file) { |
| const std::string tag_string = BinaryReadTagString(file); |
| if (tag_string.empty()) { |
| return {}; |
| } |
| tagging::TagArgs tag_args; |
| const tagging::ErrorCode error = tagging::Parse(tag_string, {}, tag_args); |
| if (error != tagging::ErrorCode::kSuccess) { |
| LOG(ERROR) << __func__ << ": Invalid tag string: " << tag_string << ": " |
| << error; |
| return {}; |
| } |
| return tag_args; |
| } |
| |
| bool BinaryWriteTag(const base::FilePath& in_file, |
| const std::string& tag_string, |
| int padded_length, |
| base::FilePath out_file) { |
| const std::vector<uint8_t> contents = ReadEntireFile(in_file); |
| std::unique_ptr<tagging::BinaryInterface> bin = |
| CreateBinary(in_file, contents); |
| if (!bin) { |
| LOG(ERROR) << __func__ << ": Could not parse binary: " << in_file; |
| return false; |
| } |
| |
| // Validate the tag string, if any. |
| if (!tag_string.empty()) { |
| tagging::TagArgs tag_args; |
| const tagging::ErrorCode error = tagging::Parse(tag_string, {}, tag_args); |
| if (error != tagging::ErrorCode::kSuccess) { |
| LOG(ERROR) << __func__ << ": Invalid tag string: " << tag_string << ": " |
| << error; |
| return false; |
| } |
| } |
| |
| std::vector<uint8_t> tag_contents = tagging::GetTagFromTagString(tag_string); |
| |
| if (padded_length > 0) { |
| size_t new_size = 0; |
| if (base::CheckAdd(tag_contents.size(), padded_length) |
| .AssignIfValid(&new_size)) { |
| tag_contents.resize(new_size); |
| } else { |
| LOG(ERROR) << __func__ << "Failed to pad the tag contents."; |
| return false; |
| } |
| } |
| |
| auto new_contents = bin->SetTag(tag_contents); |
| if (!new_contents) { |
| LOG(ERROR) << __func__ |
| << "Error while setting superfluous certificate tag."; |
| return false; |
| } |
| if (out_file.empty()) { |
| out_file = in_file; |
| } |
| if (!base::WriteFile(out_file, *new_contents)) { |
| PLOG(ERROR) << __func__ << "Error while writing updated file: " << out_file; |
| return false; |
| } |
| return true; |
| } |
| |
| #if BUILDFLAG(IS_MAC) |
| |
| base::expected<TagArgs, ErrorCode> ReadTagFromApplicationInstanceXattr( |
| const base::FilePath& path) { |
| if (path.empty()) { |
| VLOG(0) << "no path in ReadTagFromApplicationInstanceXattr"; |
| return base::unexpected(ErrorCode::kTagNotFound); |
| } |
| |
| std::vector<uint8_t> raw_tag(kMaxBinaryTagBytes, 0); |
| ssize_t got_bytes = |
| getxattr(path.value().c_str(), "com.apple.application-instance", |
| raw_tag.data(), kMaxBinaryTagBytes, 0, 0); |
| // If a C API says it wrote past the end of a buffer, believe it. |
| CHECK(got_bytes <= static_cast<ssize_t>(kMaxBinaryTagBytes)) |
| << "getxattr wrote " << got_bytes << " bytes into a " |
| << kMaxBinaryTagBytes << " byte buffer!"; |
| if (got_bytes < 0) { |
| VPLOG(1) << "getxattr could not read com.apple.application-instance on " |
| << path; |
| return base::unexpected(ErrorCode::kTagNotFound); |
| } |
| std::vector<uint8_t>::iterator tag_data_begin = raw_tag.begin(); |
| std::string tag_string = ReadTag(tag_data_begin, tag_data_begin + got_bytes); |
| if (tag_string.empty()) { |
| return base::unexpected(ErrorCode::kTagNotFound); |
| } |
| TagArgs value; |
| ErrorCode code = Parse(tag_string, {}, value); |
| if (code != ErrorCode::kSuccess) { |
| return base::unexpected(code); |
| } |
| return value; |
| } |
| |
| bool WriteTagStringToApplicationInstanceXattr(const base::FilePath& path, |
| const std::string& tag_string) { |
| if (path.empty()) { |
| VLOG(0) << "no path provided when writing xattr tag"; |
| return false; |
| } |
| if (tag_string.size() > kMaxTagStringBytes) { |
| VLOG(1) << "xattr tag too big, will be truncated when read"; |
| // warning only; continue |
| } |
| if (tag_string.empty()) { |
| VLOG(1) << "writing empty xattr tag"; |
| // warning only; continue |
| } |
| std::vector<uint8_t> tag_bytes = GetTagFromTagString(tag_string); |
| if (tag_bytes.empty()) { |
| VLOG(0) << "could not create xattr tag"; |
| return false; |
| } |
| int result = setxattr(path.value().c_str(), "com.apple.application-instance", |
| tag_bytes.data(), tag_bytes.size(), 0, 0); |
| if (result) { |
| VPLOG(0) << "setxattr failed on " << path; |
| return false; |
| } |
| return true; |
| } |
| |
| #endif // BUILDFLAG(IS_MAC) |
| |
| } // namespace updater::tagging |