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
|
// Copyright 2022 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/ash/net/dns_over_https/templates_uri_resolver_impl.h"
#include <memory>
#include <string>
#include "ash/constants/ash_features.h"
#include "base/check_is_test.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/policy/core/device_attributes.h"
#include "chrome/browser/ash/policy/core/device_attributes_fake.h"
#include "chrome/browser/ash/policy/core/device_attributes_impl.h"
#include "chrome/browser/net/secure_dns_config.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/network/device_state.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user.h"
#include "crypto/sha2.h"
namespace {
constexpr int kMinSaltSize = 8;
constexpr int kMaxSaltSize = 32;
constexpr char kUserEmailPlaceholder[] = "${USER_EMAIL}";
constexpr char kUserEmailDomainPlaceholder[] = "${USER_EMAIL_DOMAIN}";
constexpr char kUserEmailNamePlaceholder[] = "${USER_EMAIL_NAME}";
constexpr char kDeviceDirectoryIdPlaceholder[] = "${DEVICE_DIRECTORY_ID}";
constexpr char kDeviceSerialNumberPlaceholder[] = "${DEVICE_SERIAL_NUMBER}";
constexpr char kDeviceAssetIdPlaceholder[] = "${DEVICE_ASSET_ID}";
constexpr char kDeviceAnnotatedLocationPlaceholder[] =
"${DEVICE_ANNOTATED_LOCATION}";
constexpr char kDeviceIpPlaceholder[] = "${DEVICE_IP_ADDRESSES}";
constexpr char kPlaceholderStartSymbol[] = "${";
constexpr char kPlaceholderEndSymbol[] = "}";
// Prefix values used to indicate the IP protocol of the IP addresses in the
// effective DoH template URI.
constexpr char kIPv4Prefix[] = "0010";
constexpr char kIPv6Prefix[] = "0020";
// Used as a replacement value for device identifiers when the user is
// unaffiliated.
constexpr char kDeviceNotManaged[] = "VALUE_NOT_AVAILABLE";
constexpr char kIdentifierNotAvailable[] = "${VALUE_NOT_AVAILABLE}";
constexpr char kUnknownPlaceholderMessage[] =
"Templates contain not replaced placeholder: ";
// Part before "@" of the given |email| address.
// "some_email@domain.com" => "some_email"
//
// Returns empty string if |email| does not contain an "@".
std::string EmailName(const std::string& email) {
size_t at_sign_pos = email.find("@");
if (at_sign_pos == std::string::npos) {
return std::string();
}
return email.substr(0, at_sign_pos);
}
// Part after "@" of an email address.
// "some_email@domain.com" => "domain.com"
//
// Returns empty string if |email| does not contain an "@".
std::string EmailDomain(const std::string& email) {
size_t at_sign_pos = email.find("@");
if (at_sign_pos == std::string::npos) {
return std::string();
}
return email.substr(at_sign_pos + 1);
}
// If `hash_variable` is true, the output is the hex encoded result of the
// hashed `salt` + `input` value. Otherwise we return the input between
// placeholder delimiters.
std::string FormatVariable(const std::string& input,
const std::string& salt,
bool hash_variable) {
if (!hash_variable) {
return "${" + input + "}";
}
return base::HexEncode(crypto::SHA256HashString(salt + input));
}
// Returns a hex string representing all IP addresses (IPv4 and/or IPv6)
// associated with the default network. The addresses are hex encoded in network
// byte order. The addresses are prefixed with a string that indicates the
// protocol of the address (`kIPv4Prefix` and `kIPv6Prefix`). For privacy
// reasons, IP replacement in the DoH URI template is only allowed if:
// - The network is managed via user policy.
// - The network is managed via device policy and the user is
// affiliated.
// - The default network is not a VPN.
// If the conditions above are not met or there is no connected network, this
// method returns an empty string.
// There is no separator between addresses if multiple IP addresses are
// returned.
std::string GetIpReplacementValue(bool use_network_byte_order,
const user_manager::User& user) {
// NetworkHandler may be un-initialized in unit tests.
if (!ash::NetworkHandler::IsInitialized()) {
return std::string();
}
const ash::NetworkStateHandler* network_state_handler =
ash::NetworkHandler::Get()->network_state_handler();
if (!network_state_handler) {
return std::string();
}
const ash::NetworkState* network = network_state_handler->DefaultNetwork();
if (!network) {
return std::string();
}
if (network->type() == shill::kTypeVPN) {
return std::string();
}
if (network->onc_source() != ::onc::ONCSource::ONC_SOURCE_USER_POLICY &&
(!user.IsAffiliated() ||
network->onc_source() != ::onc::ONCSource::ONC_SOURCE_DEVICE_POLICY)) {
return std::string();
}
const ash::DeviceState* device =
network_state_handler->GetDeviceState(network->device_path());
if (!device) {
return std::string();
}
std::string replacement;
net::IPAddress ipv4_address;
if (ipv4_address.AssignFromIPLiteral(
device->GetIpAddressByType(shill::kTypeIPv4))) {
if (use_network_byte_order) {
replacement = kIPv4Prefix + base::HexEncode(ipv4_address.bytes());
} else {
replacement =
FormatVariable(ipv4_address.ToString(), /*salt=*/std::string(),
/*hash_variable=*/false);
}
}
// The default network can have multiple IPv6 addresses. Only the RFC 4941
// privacy address is relevant, the following code fetches that address.
net::IPAddress ipv6_address;
if (ipv6_address.AssignFromIPLiteral(
device->GetIpAddressByType(shill::kTypeIPv6))) {
if (use_network_byte_order) {
replacement += kIPv6Prefix + base::HexEncode(ipv6_address.bytes());
} else {
replacement +=
FormatVariable(ipv6_address.ToString(), /*salt=*/std::string(),
/*hash_variable=*/false);
}
}
return replacement;
}
// Returns first found placeholder in `templates` starting from position `pos`,
// as option value. If no placeholders are found, returns empty optional.
std::optional<std::string_view> GetNextPlaceholder(std::string_view templates,
size_t pos) {
size_t placeholder_start = templates.find(kPlaceholderStartSymbol, pos);
if (placeholder_start == std::string::npos) {
return std::nullopt;
}
size_t placeholder_end =
templates.find(kPlaceholderEndSymbol, placeholder_start);
if (placeholder_end == std::string::npos) {
LOG(WARNING) << "Placeholders end symbol is missed in " << templates;
return std::nullopt;
}
return std::make_optional(templates.substr(
placeholder_start, placeholder_end - placeholder_start + 1));
}
// Checks if display templates `display_str` (with replaced identifiers) and
// original templates `raw_str` contain placeholders with the same values. This
// will indicate that those placeholders were not replaced. Replace those
// placeholders with kIdentifierNotAvailable. Some placeholders like
// "DEVICE_IP_ADDRESSES" can create 2 new placeholders in the display
// template, so searching for every original placeholder instead of fetching
// all of them and comparing one to one.
void HighlightUnknownDisplayPlaceholders(std::string& display_str,
std::string_view raw_str) {
size_t search_start_pos = 0;
std::optional<std::string_view> maybe_placeholder =
GetNextPlaceholder(raw_str, search_start_pos);
while (maybe_placeholder.has_value()) {
std::string_view placeholder = maybe_placeholder.value();
size_t placeholder_pos_in_display = display_str.find(placeholder);
if (placeholder_pos_in_display != std::string::npos) {
LOG(WARNING) << kUnknownPlaceholderMessage << placeholder
<< ", value is not available";
base::ReplaceSubstringsAfterOffset(&display_str,
placeholder_pos_in_display,
placeholder, kIdentifierNotAvailable);
}
search_start_pos =
raw_str.find(kPlaceholderEndSymbol, search_start_pos + 1);
maybe_placeholder = GetNextPlaceholder(raw_str, search_start_pos);
}
}
// Looks into effective `templates` if they still contain placeholders which
// were not replaced with data and strip them off. This step keeps compatibility
// between new type of placeholders delivered by policy and older OS versions
// which still have no definitions for such placeholders.
void StripUnknownEffectivePlaceholders(std::string& templates) {
size_t search_start_pos = 0;
std::optional<std::string_view> maybe_placeholder =
GetNextPlaceholder(templates, search_start_pos);
while (maybe_placeholder.has_value()) {
std::string placeholder(maybe_placeholder.value());
LOG(WARNING) << kUnknownPlaceholderMessage << placeholder
<< ", it will be deleted";
search_start_pos =
templates.find(kPlaceholderStartSymbol, search_start_pos);
base::ReplaceSubstringsAfterOffset(&templates, search_start_pos,
placeholder, "");
maybe_placeholder = GetNextPlaceholder(templates, search_start_pos);
}
}
// Returns a copy of `template` where the identifier placeholders are replaced
// with real user and device data.
// If `hash_variable` is true, then the user and device identifiers are hashed
// with `salt` and hex encoded. The salt is optional and can be an empty string.
// If `hash_variable` is false, the output is a
// user-friendly version of the effective DNS URI template. This value is used
// to inform the user of identifiers which are shared with the DoH server when
// sending a DNS resolution request.
// Only affiliated users can share device identifiers. If the user is not
// affiliated, the device identifier placeholder will be replaced by
// `kDeviceNotManaged`; e.g for `hash_variable`=true
// ${DEVICE_ASSET_ID} is replaced by hash(VALUE_NOT_AVAILABLE+salt).
std::string ReplaceVariables(std::string templates,
const user_manager::User& user,
const std::string& salt,
policy::DeviceAttributes* attributes,
bool hash_variable) {
std::string user_email = user.GetAccountId().GetUserEmail();
std::string user_email_domain = EmailDomain(user_email);
std::string user_email_name = EmailName(user_email);
std::string original_templates = templates;
base::ReplaceSubstringsAfterOffset(
&templates, 0, kUserEmailPlaceholder,
FormatVariable(user_email, salt, hash_variable));
base::ReplaceSubstringsAfterOffset(
&templates, 0, kUserEmailDomainPlaceholder,
FormatVariable(user_email_domain, salt, hash_variable));
base::ReplaceSubstringsAfterOffset(
&templates, 0, kUserEmailNamePlaceholder,
FormatVariable(user_email_name, salt, hash_variable));
std::string device_directory_id = kDeviceNotManaged;
std::string device_asset_id = kDeviceNotManaged;
std::string device_serial_number = kDeviceNotManaged;
std::string device_annotated_location = kDeviceNotManaged;
if (user.IsAffiliated() && attributes) {
device_directory_id = attributes->GetDirectoryApiID();
device_asset_id = attributes->GetDeviceAssetID();
device_serial_number = attributes->GetDeviceSerialNumber();
device_annotated_location = attributes->GetDeviceAnnotatedLocation();
} else {
// Device identifiers are only replaced for affiliated users.
LOG(WARNING)
<< "Skipping device variables replacement for unaffiliated user";
}
base::ReplaceSubstringsAfterOffset(
&templates, 0, kDeviceDirectoryIdPlaceholder,
FormatVariable(device_directory_id, salt, hash_variable));
base::ReplaceSubstringsAfterOffset(
&templates, 0, kDeviceAssetIdPlaceholder,
FormatVariable(device_asset_id, salt, hash_variable));
base::ReplaceSubstringsAfterOffset(
&templates, 0, kDeviceSerialNumberPlaceholder,
FormatVariable(device_serial_number, salt, hash_variable));
base::ReplaceSubstringsAfterOffset(
&templates, 0, kDeviceAnnotatedLocationPlaceholder,
FormatVariable(device_annotated_location, salt, hash_variable));
// The device IP addresses are not hashed in the DNS URI template. In this
// case, `hash_variable` is used to indicate if the IP addresses should be
// replaced with a string that represents the network byte order (required by
// the DNS server) or as a human-readable string used for privacy disclosure.
base::ReplaceSubstringsAfterOffset(
&templates, 0, kDeviceIpPlaceholder,
GetIpReplacementValue(/*use_network_byte_order=*/hash_variable, user));
bool is_display_mode = !hash_variable;
if (is_display_mode) {
HighlightUnknownDisplayPlaceholders(templates, original_templates);
} else {
StripUnknownEffectivePlaceholders(templates);
}
return templates;
}
} // namespace
namespace ash::dns_over_https {
TemplatesUriResolverImpl::TemplatesUriResolverImpl() {
attributes_ = std::make_unique<policy::DeviceAttributesImpl>();
}
TemplatesUriResolverImpl::~TemplatesUriResolverImpl() = default;
void TemplatesUriResolverImpl::Update(const PrefService& local_state,
const user_manager::User& user) {
doh_with_identifiers_active_ = false;
const std::string& mode = local_state.GetString(prefs::kDnsOverHttpsMode);
if (mode == SecureDnsConfig::kModeOff) {
return;
}
effective_templates_ = local_state.GetString(prefs::kDnsOverHttpsTemplates);
// In ChromeOS only, the DnsOverHttpsTemplatesWithIdentifiers policy will
// overwrite the DnsOverHttpsTemplates policy. For privacy reasons, the
// replacement only happens if the is a salt specified which will be used to
// hash the identifiers in the template URI.
std::string templates_with_identifiers =
local_state.GetString(prefs::kDnsOverHttpsTemplatesWithIdentifiers);
std::string salt = local_state.GetString(prefs::kDnsOverHttpsSalt);
if (!salt.empty() &&
(salt.size() < kMinSaltSize || salt.size() > kMaxSaltSize)) {
// If the salt is set but the size is not within the specified limits, then
// we ignore the config. This should have been checked upfront so no need to
// report here.
return;
}
std::string effective_templates = ReplaceVariables(
templates_with_identifiers, user, salt, attributes_.get(),
/*hash_variable=*/true);
std::string display_templates =
ReplaceVariables(templates_with_identifiers, user, "", attributes_.get(),
/*hash_variable=*/false);
if (effective_templates.empty() || display_templates.empty()) {
return;
}
// We only use this if the variable substitution was successful for both
// effective and display templates. Otherwise something is wrong and this
// should have been reported earlier.
effective_templates_ = effective_templates;
display_templates_ = display_templates;
doh_with_identifiers_active_ = true;
}
bool TemplatesUriResolverImpl::GetDohWithIdentifiersActive() {
return doh_with_identifiers_active_;
}
std::string TemplatesUriResolverImpl::GetEffectiveTemplates() {
return effective_templates_;
}
std::string TemplatesUriResolverImpl::GetDisplayTemplates() {
return display_templates_;
}
void TemplatesUriResolverImpl::SetDeviceAttributesForTesting(
std::unique_ptr<policy::FakeDeviceAttributes> attributes) {
CHECK_IS_TEST();
attributes_ = std::move(attributes);
}
// static
bool TemplatesUriResolverImpl::IsDeviceIpAddressIncludedInUriTemplate(
std::string_view uri_templates) {
return uri_templates.find(kDeviceIpPlaceholder) != std::string::npos;
}
} // namespace ash::dns_over_https
|