File: templates_uri_resolver_impl.cc

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (392 lines) | stat: -rw-r--r-- 16,201 bytes parent folder | download | duplicates (5)
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