chromium/chromeos/ash/components/network/onc/network_onc_utils.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/ash/components/network/onc/network_onc_utils.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <string>
#include <utility>

#include "base/base64.h"
#include "base/containers/flat_map.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/values.h"
#include "chromeos/ash/components/network/managed_network_configuration_handler.h"
#include "chromeos/ash/components/network/network_configuration_handler.h"
#include "chromeos/ash/components/network/network_event_log.h"
#include "chromeos/ash/components/network/network_profile.h"
#include "chromeos/ash/components/network/network_profile_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_ui_data.h"
#include "chromeos/ash/components/network/onc/onc_normalizer.h"
#include "chromeos/ash/components/network/onc/onc_translator.h"
#include "chromeos/ash/components/network/tether_constants.h"
#include "chromeos/components/onc/onc_mapper.h"
#include "chromeos/components/onc/onc_signature.h"
#include "chromeos/components/onc/onc_utils.h"
#include "chromeos/components/onc/onc_validator.h"
#include "components/account_id/account_id.h"
#include "components/device_event_log/device_event_log.h"
#include "components/onc/onc_constants.h"
#include "components/onc/onc_pref_names.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/url_formatter/url_fixer.h"
#include "components/user_manager/user.h"
#include "components/user_manager/user_manager.h"
#include "crypto/encryptor.h"
#include "crypto/hmac.h"
#include "crypto/symmetric_key.h"
#include "net/base/host_port_pair.h"
#include "net/base/proxy_server.h"
#include "net/base/proxy_string_util.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util_nss.h"
#include "net/proxy_resolution/proxy_bypass_rules.h"
#include "net/proxy_resolution/proxy_config.h"
#include "third_party/boringssl/src/pki/pem.h"
#include "third_party/cros_system_api/dbus/service_constants.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace ash::onc {

namespace {

// Scheme strings for supported |net::ProxyServer::SCHEME_*| enum values.
constexpr char kSocksScheme[] = "socks";
constexpr char kSocks4Scheme[] = "socks4";
constexpr char kSocks5Scheme[] = "socks5";

std::string GetString(const base::Value::Dict& dict, const char* key) {
  const std::string* value = dict.FindString(key);
  if (!value)
    return std::string();
  return *value;
}

int GetInt(const base::Value::Dict& dict, const char* key, int default_value) {
  return dict.FindInt(key).value_or(default_value);
}

net::ProxyServer ConvertOncProxyLocationToHostPort(
    net::ProxyServer::Scheme default_proxy_scheme,
    const base::Value::Dict& onc_proxy_location) {
  std::string host = GetString(onc_proxy_location, ::onc::proxy::kHost);
  // Parse |host| according to the format [<scheme>"://"]<server>[":"<port>].
  net::ProxyServer proxy_server =
      net::ProxyUriToProxyServer(host, default_proxy_scheme);
  int port = GetInt(onc_proxy_location, ::onc::proxy::kPort, 0);

  // Replace the port parsed from |host| by the provided |port|.
  return net::ProxyServer(
      proxy_server.scheme(),
      net::HostPortPair(proxy_server.host_port_pair().host(),
                        static_cast<uint16_t>(port)));
}

void AppendProxyServerForScheme(const base::Value::Dict& onc_manual,
                                const std::string& onc_scheme,
                                std::string* spec) {
  const base::Value::Dict* onc_proxy_location = onc_manual.FindDict(onc_scheme);
  if (!onc_proxy_location)
    return;

  net::ProxyServer::Scheme default_proxy_scheme = net::ProxyServer::SCHEME_HTTP;
  std::string url_scheme;
  if (onc_scheme == ::onc::proxy::kFtp) {
    url_scheme = url::kFtpScheme;
  } else if (onc_scheme == ::onc::proxy::kHttp) {
    url_scheme = url::kHttpScheme;
  } else if (onc_scheme == ::onc::proxy::kHttps) {
    url_scheme = url::kHttpsScheme;
  } else if (onc_scheme == ::onc::proxy::kSocks) {
    default_proxy_scheme = net::ProxyServer::SCHEME_SOCKS4;
    url_scheme = kSocksScheme;
  } else {
    NOTREACHED_IN_MIGRATION();
  }

  net::ProxyServer proxy_server = ConvertOncProxyLocationToHostPort(
      default_proxy_scheme, *onc_proxy_location);

  ProxyConfigDictionary::EncodeAndAppendProxyServer(url_scheme, proxy_server,
                                                    spec);
}

net::ProxyBypassRules ConvertOncExcludeDomainsToBypassRules(
    const base::Value::List& onc_exclude_domains) {
  net::ProxyBypassRules rules;
  for (const base::Value& value : onc_exclude_domains) {
    if (!value.is_string()) {
      LOG(ERROR) << "Badly formatted ONC exclude domains";
      continue;
    }
    rules.AddRuleFromString(value.GetString());
  }
  return rules;
}

std::string SchemeToString(net::ProxyServer::Scheme scheme) {
  switch (scheme) {
    case net::ProxyServer::SCHEME_HTTP:
      return url::kHttpScheme;
    case net::ProxyServer::SCHEME_SOCKS4:
      return kSocks4Scheme;
    case net::ProxyServer::SCHEME_SOCKS5:
      return kSocks5Scheme;
    case net::ProxyServer::SCHEME_HTTPS:
      return url::kHttpsScheme;
    case net::ProxyServer::SCHEME_QUIC:
      // Re-map the legacy "quic://" proxy protocol scheme to "https://",
      // because that's how it's actually treated. See
      // https://issues.chromium.org/issues/40141686.
      return url::kHttpsScheme;
    case net::ProxyServer::SCHEME_INVALID:
      break;
  }
  NOTREACHED_IN_MIGRATION();
  return "";
}

void SetProxyForScheme(const net::ProxyConfig::ProxyRules& proxy_rules,
                       const std::string& scheme,
                       const std::string& onc_scheme,
                       base::Value::Dict& dict) {
  const net::ProxyList* proxy_list = nullptr;
  if (proxy_rules.type == net::ProxyConfig::ProxyRules::Type::PROXY_LIST) {
    proxy_list = &proxy_rules.single_proxies;
  } else if (proxy_rules.type ==
             net::ProxyConfig::ProxyRules::Type::PROXY_LIST_PER_SCHEME) {
    proxy_list = proxy_rules.MapUrlSchemeToProxyList(scheme);
  }
  if (!proxy_list || proxy_list->IsEmpty())
    return;
  const net::ProxyChain& chain = proxy_list->First();
  CHECK(chain.is_single_proxy());
  const net::ProxyServer& server = chain.First();
  std::string host = server.host_port_pair().host();

  // For all proxy types except SOCKS, the default scheme of the proxy host is
  // HTTP.
  net::ProxyServer::Scheme default_scheme =
      (onc_scheme == ::onc::proxy::kSocks) ? net::ProxyServer::SCHEME_SOCKS4
                                           : net::ProxyServer::SCHEME_HTTP;
  // Only prefix the host with a non-default scheme.
  if (server.scheme() != default_scheme) {
    host = SchemeToString(server.scheme()) + "://" + host;
  }
  auto url_dict = base::Value::Dict()
                      .Set(::onc::proxy::kHost, host)
                      .Set(::onc::proxy::kPort, server.host_port_pair().port());
  dict.Set(onc_scheme, std::move(url_dict));
}

// Returns the NetworkConfiguration with |guid| from |network_configs|, or
// nullptr if no such NetworkConfiguration is found.
const base::Value::Dict* GetNetworkConfigByGUID(
    const base::Value::List& network_configs,
    const std::string& guid) {
  for (const auto& network : network_configs) {
    DCHECK(network.is_dict());

    std::string current_guid =
        GetString(network.GetDict(), ::onc::network_config::kGUID);
    if (current_guid == guid)
      return &network.GetDict();
  }
  return nullptr;
}

// Returns the first Ethernet NetworkConfiguration from |network_configs| with
// "Authentication: None", or nullptr if no such NetworkConfiguration is found.
const base::Value::Dict* GetNetworkConfigForEthernetWithoutEAP(
    const base::Value::List& network_configs) {
  VLOG(2) << "Search for ethernet policy without EAP.";
  for (const auto& network : network_configs) {
    DCHECK(network.is_dict());

    const base::Value::Dict& network_dict = network.GetDict();
    std::string type = GetString(network_dict, ::onc::network_config::kType);
    if (type != ::onc::network_type::kEthernet)
      continue;

    const base::Value::Dict* ethernet =
        network_dict.FindDict(::onc::network_config::kEthernet);
    if (!ethernet)
      continue;

    std::string auth = GetString(*ethernet, ::onc::ethernet::kAuthentication);
    if (auth == ::onc::ethernet::kAuthenticationNone)
      return &network_dict;
  }
  return nullptr;
}

// Returns the NetworkConfiguration object for |network| from
// |network_configs| or nullptr if no matching NetworkConfiguration is found. If
// |network| is a non-Ethernet network, performs a lookup by GUID. If |network|
// is an Ethernet network, tries lookup of the GUID of the shared EthernetEAP
// service, or otherwise returns the first Ethernet NetworkConfiguration with
// "Authentication: None".
const base::Value::Dict* GetNetworkConfigForNetworkFromOnc(
    const base::Value::List& network_configs,
    const NetworkState& network) {
  // In all cases except Ethernet, we use the GUID of |network|.
  if (!network.Matches(NetworkTypePattern::Ethernet()))
    return GetNetworkConfigByGUID(network_configs, network.guid());

  // Ethernet is always shared and thus cannot store a GUID per user. Thus we
  // search for any Ethernet policy instead of a matching GUID.
  // EthernetEAP service contains only the EAP parameters and stores the GUID of
  // the respective ONC policy. The EthernetEAP service itself is however never
  // in state "connected". An EthernetEAP policy must be applied, if an Ethernet
  // service is connected using the EAP parameters.
  const NetworkState* ethernet_eap = nullptr;
  if (NetworkHandler::IsInitialized()) {
    ethernet_eap =
        NetworkHandler::Get()->network_state_handler()->GetEAPForEthernet(
            network.path(), /*connected_only=*/true);
  }

  // The GUID associated with the EthernetEAP service refers to the ONC policy
  // with "Authentication: 8021X".
  if (ethernet_eap)
    return GetNetworkConfigByGUID(network_configs, ethernet_eap->guid());

  // Otherwise, EAP is not used and instead the Ethernet policy with
  // "Authentication: None" applies.
  return GetNetworkConfigForEthernetWithoutEAP(network_configs);
}

// Expects |pref_name| in |pref_service| to be a pref holding an ONC blob.
// Returns the NetworkConfiguration ONC object for |network| from this ONC, or
// nullptr if no configuration is found. See |GetNetworkConfigForNetworkFromOnc|
// for the NetworkConfiguration lookup rules.
const base::Value::Dict* GetPolicyForNetworkFromPref(
    const PrefService* pref_service,
    const char* pref_name,
    const NetworkState& network) {
  if (!pref_service) {
    VLOG(2) << "No pref service";
    return nullptr;
  }

  const PrefService::Preference* preference =
      pref_service->FindPreference(pref_name);
  if (!preference) {
    VLOG(2) << "No preference " << pref_name;
    // The preference may not exist in tests.
    return nullptr;
  }

  // User prefs are not stored in this Preference yet but only the policy.
  //
  // The policy server incorrectly configures the OpenNetworkConfiguration user
  // policy as Recommended. To work around that, we handle the Recommended and
  // the Mandatory value in the same way.
  // TODO(pneubeck): Remove this workaround, once the server is fixed. See
  // http://crbug.com/280553 .
  if (preference->IsDefaultValue()) {
    VLOG(2) << "Preference has no recommended or mandatory value.";
    // No policy set.
    return nullptr;
  }
  VLOG(2) << "Preference with policy found.";
  const base::Value* onc_policy_value = preference->GetValue();
  DCHECK(onc_policy_value);

  return GetNetworkConfigForNetworkFromOnc(onc_policy_value->GetList(),
                                           network);
}

// Returns the global network configuration dictionary from the ONC policy of
// the active user if |for_active_user| is true, or from device policy if it is
// false.
const base::Value::Dict* GetGlobalConfigFromPolicy(bool for_active_user) {
  std::string username_hash;
  if (for_active_user) {
    const user_manager::User* user =
        user_manager::UserManager::Get()->GetActiveUser();
    if (!user) {
      LOG(ERROR) << "No user logged in yet.";
      return nullptr;
    }
    username_hash = user->username_hash();
  }
  return NetworkHandler::Get()
      ->managed_network_configuration_handler()
      ->GetGlobalConfigFromPolicy(username_hash);
}

// Replaces user-specific string placeholders in |network_configs|, which must
// be a list of ONC NetworkConfigurations. Currently only user name placeholders
// are implemented, which are replaced by attributes from |user|.
void ExpandStringPlaceholdersInNetworksForUser(
    const user_manager::User* user,
    base::Value::List& network_configs) {
  if (!user) {
    // In tests no user may be logged in. It's not harmful if we just don't
    // expand the strings.
    return;
  }

  // Note: It is OK for the placeholders to be replaced with empty strings if
  // that is what the getters on |user| provide.
  chromeos::VariableExpander variable_expander(
      GetVariableExpansionsForUser(user));
  chromeos::onc::ExpandStringsInNetworks(variable_expander, network_configs);
}

}  // namespace

NetworkTypePattern NetworkTypePatternFromOncType(const std::string& type) {
  if (type == ::onc::network_type::kAllTypes)
    return NetworkTypePattern::Default();
  if (type == ::onc::network_type::kCellular)
    return NetworkTypePattern::Cellular();
  if (type == ::onc::network_type::kEthernet)
    return NetworkTypePattern::Ethernet();
  if (type == ::onc::network_type::kTether)
    return NetworkTypePattern::Tether();
  if (type == ::onc::network_type::kVPN)
    return NetworkTypePattern::VPN();
  if (type == ::onc::network_type::kWiFi)
    return NetworkTypePattern::WiFi();
  if (type == ::onc::network_type::kWireless)
    return NetworkTypePattern::Wireless();
  NET_LOG(ERROR) << "Unrecognized ONC type: " << type;
  return NetworkTypePattern::Default();
}

std::optional<base::Value::Dict> ConvertOncProxySettingsToProxyConfig(
    const base::Value::Dict& onc_proxy_settings) {
  std::string type = GetString(onc_proxy_settings, ::onc::proxy::kType);

  if (type == ::onc::proxy::kDirect) {
    return ProxyConfigDictionary::CreateDirect();
  }
  if (type == ::onc::proxy::kWPAD) {
    return ProxyConfigDictionary::CreateAutoDetect();
  }
  if (type == ::onc::proxy::kPAC) {
    std::string pac_url = GetString(onc_proxy_settings, ::onc::proxy::kPAC);
    GURL url(url_formatter::FixupURL(pac_url, std::string()));
    return ProxyConfigDictionary::CreatePacScript(
        url.is_valid() ? url.spec() : std::string(), false);
  }
  if (type == ::onc::proxy::kManual) {
    const base::Value::Dict* manual_dict =
        onc_proxy_settings.FindDict(::onc::proxy::kManual);
    if (!manual_dict) {
      NET_LOG(ERROR) << "Manual proxy missing dictionary";
      return std::nullopt;
    }
    std::string manual_spec;
    AppendProxyServerForScheme(*manual_dict, ::onc::proxy::kFtp, &manual_spec);
    AppendProxyServerForScheme(*manual_dict, ::onc::proxy::kHttp, &manual_spec);
    AppendProxyServerForScheme(*manual_dict, ::onc::proxy::kSocks,
                               &manual_spec);
    AppendProxyServerForScheme(*manual_dict, ::onc::proxy::kHttps,
                               &manual_spec);

    net::ProxyBypassRules bypass_rules;
    const base::Value::List* exclude_domains =
        onc_proxy_settings.FindList(::onc::proxy::kExcludeDomains);
    if (exclude_domains)
      bypass_rules = ConvertOncExcludeDomainsToBypassRules(*exclude_domains);
    return ProxyConfigDictionary::CreateFixedServers(manual_spec,
                                                     bypass_rules.ToString());
  }
  NOTREACHED_IN_MIGRATION();
  return std::nullopt;
}

std::optional<base::Value::Dict> ConvertProxyConfigToOncProxySettings(
    const base::Value::Dict& proxy_config_dict) {
  // Create a ProxyConfigDictionary from the dictionary.
  ProxyConfigDictionary proxy_config(proxy_config_dict.Clone());

  // Create the result Value and populate it.
  base::Value::Dict proxy_settings;
  ProxyPrefs::ProxyMode mode;
  if (!proxy_config.GetMode(&mode)) {
    return std::nullopt;
  }
  switch (mode) {
    case ProxyPrefs::MODE_DIRECT: {
      proxy_settings.Set(::onc::proxy::kType, ::onc::proxy::kDirect);
      break;
    }
    case ProxyPrefs::MODE_AUTO_DETECT: {
      proxy_settings.Set(::onc::proxy::kType, ::onc::proxy::kWPAD);
      break;
    }
    case ProxyPrefs::MODE_PAC_SCRIPT: {
      proxy_settings.Set(::onc::proxy::kType, ::onc::proxy::kPAC);
      std::string pac_url;
      proxy_config.GetPacUrl(&pac_url);
      proxy_settings.Set(::onc::proxy::kPAC, pac_url);
      break;
    }
    case ProxyPrefs::MODE_FIXED_SERVERS: {
      proxy_settings.Set(::onc::proxy::kType, ::onc::proxy::kManual);
      base::Value::Dict manual;
      std::string proxy_rules_string;
      if (proxy_config.GetProxyServer(&proxy_rules_string)) {
        net::ProxyConfig::ProxyRules proxy_rules;
        proxy_rules.ParseFromString(proxy_rules_string);
        SetProxyForScheme(proxy_rules, url::kFtpScheme, ::onc::proxy::kFtp,
                          manual);
        SetProxyForScheme(proxy_rules, url::kHttpScheme, ::onc::proxy::kHttp,
                          manual);
        SetProxyForScheme(proxy_rules, url::kHttpsScheme, ::onc::proxy::kHttps,
                          manual);
        SetProxyForScheme(proxy_rules, kSocksScheme, ::onc::proxy::kSocks,
                          manual);
      }
      proxy_settings.Set(::onc::proxy::kManual, std::move(manual));

      // Convert the 'bypass_list' string into dictionary entries.
      std::string bypass_rules_string;
      if (proxy_config.GetBypassList(&bypass_rules_string)) {
        net::ProxyBypassRules bypass_rules;
        bypass_rules.ParseFromString(bypass_rules_string);
        base::Value::List exclude_domains;
        for (const auto& rule : bypass_rules.rules())
          exclude_domains.Append(rule->ToString());
        if (!exclude_domains.empty()) {
          proxy_settings.Set(::onc::proxy::kExcludeDomains,
                             std::move(exclude_domains));
        }
      }
      break;
    }
    default: {
      LOG(ERROR) << "Unexpected proxy mode in Shill config: " << mode;
      return std::nullopt;
    }
  }
  return proxy_settings;
}

base::flat_map<std::string, std::string> GetVariableExpansionsForUser(
    const user_manager::User* user) {
  base::flat_map<std::string, std::string> expansions;
  expansions[::onc::substitutes::kLoginID] = user->GetAccountName(false);
  expansions[::onc::substitutes::kLoginEmail] =
      user->GetAccountId().GetUserEmail();
  return expansions;
}

int ImportNetworksForUser(const user_manager::User* user,
                          const base::Value::List& network_configs,
                          std::string* error) {
  error->clear();

  base::Value::List expanded_networks(network_configs.Clone());
  ExpandStringPlaceholdersInNetworksForUser(user, expanded_networks);

  const NetworkProfile* profile =
      NetworkHandler::Get()->network_profile_handler()->GetProfileForUserhash(
          user->username_hash());
  if (!profile) {
    *error = "User profile doesn't exist for: " + user->display_email();
    return 0;
  }

  bool ethernet_not_found = false;
  int networks_created = 0;
  for (const auto& network_value : expanded_networks) {
    const base::Value::Dict& network = network_value.GetDict();

    // Remove irrelevant fields.
    onc::Normalizer normalizer(true /* remove recommended fields */);
    base::Value::Dict normalized_network = normalizer.NormalizeObject(
        &chromeos::onc::kNetworkConfigurationSignature, network);

    std::string type =
        GetString(normalized_network, ::onc::network_config::kType);
    ManagedNetworkConfigurationHandler* managed_network_config_handler =
        NetworkHandler::Get()->managed_network_configuration_handler();
    if (type == ::onc::network_config::kEthernet) {
      // Ethernet has to be configured using an existing Ethernet service.
      const NetworkState* ethernet =
          NetworkHandler::Get()->network_state_handler()->FirstNetworkByType(
              NetworkTypePattern::Ethernet());
      if (ethernet) {
        managed_network_config_handler->SetProperties(
            ethernet->path(), normalized_network.Clone(), base::OnceClosure(),
            network_handler::ErrorCallback());
      } else {
        ethernet_not_found = true;
      }
    } else {
      managed_network_config_handler->CreateConfiguration(
          user->username_hash(), normalized_network.Clone(),
          network_handler::ServiceResultCallback(),
          network_handler::ErrorCallback());
      ++networks_created;
    }
  }

  if (ethernet_not_found)
    *error = "No Ethernet available to configure.";
  return networks_created;
}

bool PolicyAllowsOnlyPolicyNetworksToAutoconnect(bool for_active_user) {
  const base::Value::Dict* global_config =
      GetGlobalConfigFromPolicy(for_active_user);
  if (!global_config)
    return false;  // By default, all networks are allowed to autoconnect.

  return global_config
      ->FindBool(
          ::onc::global_network_config::kAllowOnlyPolicyNetworksToAutoconnect)
      .value_or(false);
}

const base::Value::Dict* GetPolicyForNetwork(
    const PrefService* profile_prefs,
    const PrefService* local_state_prefs,
    const NetworkState& network,
    ::onc::ONCSource* onc_source) {
  VLOG(2) << "GetPolicyForNetwork: " << network.path();
  *onc_source = ::onc::ONC_SOURCE_NONE;

  const base::Value::Dict* network_policy = GetPolicyForNetworkFromPref(
      profile_prefs, ::onc::prefs::kOpenNetworkConfiguration, network);
  if (network_policy) {
    VLOG(1) << "Network " << network.path() << " is managed by user policy.";
    *onc_source = ::onc::ONC_SOURCE_USER_POLICY;
    return network_policy;
  }
  network_policy = GetPolicyForNetworkFromPref(
      local_state_prefs, ::onc::prefs::kDeviceOpenNetworkConfiguration,
      network);
  if (network_policy) {
    VLOG(1) << "Network " << network.path() << " is managed by device policy.";
    *onc_source = ::onc::ONC_SOURCE_DEVICE_POLICY;
    return network_policy;
  }
  VLOG(2) << "Network " << network.path() << " is unmanaged.";
  return nullptr;
}

bool HasPolicyForNetwork(const PrefService* profile_prefs,
                         const PrefService* local_state_prefs,
                         const NetworkState& network) {
  ::onc::ONCSource ignored_onc_source;
  const base::Value::Dict* policy = onc::GetPolicyForNetwork(
      profile_prefs, local_state_prefs, network, &ignored_onc_source);
  return policy != nullptr;
}

bool HasUserPasswordSubstitutionVariable(
    const chromeos::onc::OncValueSignature& signature,
    const base::Value::Dict& onc_object) {
  if (&signature == &chromeos::onc::kEAPSignature) {
    const std::string* password_field =
        onc_object.FindString(::onc::eap::kPassword);
    return password_field &&
           *password_field == ::onc::substitutes::kPasswordPlaceholderVerbatim;
  }
  if (&signature == &chromeos::onc::kL2TPSignature) {
    const std::string* password_field =
        onc_object.FindString(::onc::l2tp::kPassword);
    return password_field &&
           *password_field == ::onc::substitutes::kPasswordPlaceholderVerbatim;
  }

  // Recurse into nested objects.
  for (auto it : onc_object) {
    if (!it.second.is_dict())
      continue;

    const chromeos::onc::OncFieldSignature* field_signature =
        chromeos::onc::GetFieldSignature(signature, it.first);
    if (!field_signature)
      continue;

    bool result = HasUserPasswordSubstitutionVariable(
        *field_signature->value_signature, it.second.GetDict());
    if (result)
      return true;
  }

  return false;
}

bool HasUserPasswordSubstitutionVariable(
    const base::Value::List& network_configs) {
  for (const auto& network : network_configs) {
    DCHECK(network.is_dict());
    bool result = HasUserPasswordSubstitutionVariable(
        chromeos::onc::kNetworkConfigurationSignature, network.GetDict());
    if (result)
      return true;
  }
  return false;
}

}  // namespace ash::onc