// 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_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 "components/user_manager/user_manager.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}";
// 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";
// Part before "@" of the given |email| address.
// "[email protected]" => "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.
// "[email protected]" => "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 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 std::string salt,
policy::DeviceAttributes* attributes,
bool hash_variable) {
if (!user_manager::UserManager::IsInitialized()) {
return std::string();
}
const user_manager::User* user =
user_manager::UserManager::Get()->GetActiveUser();
if (!user) {
return std::string();
}
std::string user_email = user->GetAccountId().GetUserEmail();
std::string user_email_domain = EmailDomain(user_email);
std::string user_email_name = EmailName(user_email);
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));
return templates;
}
} // namespace
namespace ash::dns_over_https {
TemplatesUriResolverImpl::TemplatesUriResolverImpl() {
attributes_ = std::make_unique<policy::DeviceAttributesImpl>();
}
TemplatesUriResolverImpl::~TemplatesUriResolverImpl() = default;
void TemplatesUriResolverImpl::Update(PrefService* pref_service) {
doh_with_identifiers_active_ = false;
const std::string& mode = pref_service->GetString(prefs::kDnsOverHttpsMode);
if (mode == SecureDnsConfig::kModeOff) {
return;
}
effective_templates_ = pref_service->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 =
pref_service->GetString(prefs::kDnsOverHttpsTemplatesWithIdentifiers);
std::string salt = pref_service->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, salt, attributes_.get(),
/*hash_variable=*/true);
std::string display_templates =
ReplaceVariables(templates_with_identifiers, "", 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