chromium/chromeos/components/onc/onc_utils.cc

// Copyright 2021 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/components/onc/onc_utils.h"

#include <string>
#include <vector>

#include "base/base64.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "chromeos/components/onc/onc_mapper.h"
#include "chromeos/components/onc/onc_signature.h"
#include "chromeos/components/onc/onc_validator.h"
#include "chromeos/components/onc/variable_expander.h"
#include "components/device_event_log/device_event_log.h"
#include "crypto/encryptor.h"
#include "crypto/hmac.h"
#include "crypto/symmetric_key.h"
#include "net/cert/x509_certificate.h"
#include "third_party/boringssl/src/pki/pem.h"

namespace chromeos::onc {
namespace {

using IdToAPNMap = std::map<std::string, const base::Value::Dict*>;

// Error messages that can be reported when decrypting encrypted ONC.
constexpr char kUnableToDecrypt[] = "Unable to decrypt encrypted ONC";
constexpr char kUnableToDecode[] = "Unable to decode encrypted ONC";

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

bool GetInt(const base::Value::Dict& dict, const char* key, int* result) {
  const std::optional<int> value = dict.FindInt(key);
  if (!value) {
    return false;
  }
  *result = value.value();
  return true;
}

// Runs |variable_expander.ExpandString| on the field |fieldname| in
// |onc_object|.
void ExpandField(const std::string& fieldname,
                 const VariableExpander& variable_expander,
                 base::Value::Dict* onc_object) {
  std::string* field_value = onc_object->FindString(fieldname);
  if (!field_value) {
    return;
  }
  variable_expander.ExpandString(field_value);
}

bool CanContainPasswordPlaceholder(const std::string& field_name,
                                   const OncValueSignature& object_signature) {
  return (&object_signature == &kEAPSignature &&
          field_name == ::onc::eap::kPassword) ||
         (&object_signature == &kL2TPSignature &&
          field_name == ::onc::l2tp::kPassword);
}

bool IsUserLoginPasswordPlaceholder(const std::string& field_name,
                                    const OncValueSignature& object_signature,
                                    const base::Value& onc_value) {
  if (!CanContainPasswordPlaceholder(field_name, object_signature)) {
    return false;
  }
  DCHECK(onc_value.is_string());
  return onc_value.GetString() ==
         ::onc::substitutes::kPasswordPlaceholderVerbatim;
}

// A |Mapper| for masking sensitive fields (e.g. credentials such as
// passphrases) in ONC.
class OncMaskValues : public Mapper {
 public:
  static base::Value::Dict Mask(const OncValueSignature& signature,
                                const base::Value::Dict& onc_object,
                                const std::string& mask) {
    OncMaskValues masker(mask);
    bool error = false;
    base::Value::Dict result = masker.MapObject(signature, onc_object, &error);
    return result;
  }

 protected:
  explicit OncMaskValues(const std::string& mask) : mask_(mask) {}

  base::Value MapField(const std::string& field_name,
                       const OncValueSignature& object_signature,
                       const base::Value& onc_value,
                       bool* found_unknown_field,
                       bool* error) override {
    if (FieldIsCredential(object_signature, field_name)) {
      // If it's the password field and the substitution string is used, don't
      // mask it.
      if (IsUserLoginPasswordPlaceholder(field_name, object_signature,
                                         onc_value)) {
        return Mapper::MapField(field_name, object_signature, onc_value,
                                found_unknown_field, error);
      }
      return base::Value(mask_);
    } else {
      return Mapper::MapField(field_name, object_signature, onc_value,
                              found_unknown_field, error);
    }
  }

 private:
  // Mask to insert in place of the sensitive values.
  std::string mask_;
};

// Returns a map GUID->PEM of all server and authority certificates defined in
// the Certificates section of ONC, which is passed in as |certificates|.
CertPEMsByGUIDMap GetServerAndCACertsByGUID(
    const base::Value::List& certificates) {
  CertPEMsByGUIDMap certs_by_guid;
  for (const auto& cert_value : certificates) {
    const base::Value::Dict& cert = cert_value.GetDict();

    const std::string* guid = cert.FindString(::onc::certificate::kGUID);
    if (!guid || guid->empty()) {
      NET_LOG(ERROR) << "Certificate with missing or empty GUID.";
      continue;
    }
    const std::string* cert_type = cert.FindString(::onc::certificate::kType);
    DCHECK(cert_type);
    if (*cert_type != ::onc::certificate::kServer &&
        *cert_type != ::onc::certificate::kAuthority) {
      continue;
    }
    const std::string* x509_data = cert.FindString(::onc::certificate::kX509);
    std::string der;
    if (x509_data) {
      der = DecodePEM(*x509_data);
    }
    std::string pem;
    if (der.empty() || !net::X509Certificate::GetPEMEncodedFromDER(der, &pem)) {
      NET_LOG(ERROR) << "Certificate not PEM encoded, GUID: " << *guid;
      continue;
    }
    certs_by_guid[*guid] = pem;
  }

  return certs_by_guid;
}

// Set APN dictionary and associated recommended values to solve the issue
// of setting the APN for managed eSIM profiles (see http://b/295226668) in
// old APN UI.
void SetAPNDictAndRecommendedIfNone(base::Value::Dict& cellular_fields) {
  if (cellular_fields.Find(::onc::cellular::kAPN)) {
    return;
  }

  auto apn_recommended_list = base::Value::List()
                                  .Append(::onc::cellular_apn::kAccessPointName)
                                  .Append(::onc::cellular_apn::kAttach)
                                  .Append(::onc::cellular_apn::kAuthentication)
                                  .Append(::onc::cellular_apn::kUsername)
                                  .Append(::onc::cellular_apn::kPassword);

  base::Value* apn_dict = cellular_fields.Set(
      ::onc::cellular::kAPN, base::Value(base::Value::Type::DICT));
  apn_dict->GetDict().Set(::onc::kRecommended, std::move(apn_recommended_list));
}

// Modify recommended list to include custom APN list field to solve the issue
// of setting the APN for managed eSIM profiles (see http://b/295226668) in
// revamp APN UI.
void AddCustomAPNListToRecommended(base::Value::Dict& cellular_fields) {
  auto* recommended = cellular_fields.Find(::onc::kRecommended);
  if (!recommended) {
    recommended = cellular_fields.Set(::onc::kRecommended,
                                      base::Value(base::Value::Type::LIST));
  }
  for (const auto& field : recommended->GetList()) {
    if (field == ::onc::cellular::kCustomAPNList) {
      return;
    }
  }
  recommended->GetList().Append(::onc::cellular::kCustomAPNList);
}

void FillInCellularDefaultsInOncObject(const OncValueSignature& signature,
                                       base::Value::Dict& onc_object,
                                       bool allow_apn_modification) {
  if (&signature == &kCellularSignature) {
    if (allow_apn_modification) {
      AddCustomAPNListToRecommended(onc_object);
    } else {
      onc_object.Set(::onc::cellular::kCustomAPNList, base::Value::List());
    }
    SetAPNDictAndRecommendedIfNone(onc_object);

    return;
  }

  // The function takes any ONC object and recursively searches until it finds a
  // Cellular dictionary to set the default values.
  for (auto it : onc_object) {
    if (!it.second.is_dict()) {
      continue;
    }

    const OncFieldSignature* field_signature =
        GetFieldSignature(signature, it.first);
    if (!field_signature) {
      continue;
    }

    FillInCellularDefaultsInOncObject(*field_signature->value_signature,
                                      it.second.GetDict(),
                                      allow_apn_modification);
  }
}

// Creates an APN dict with nested recommended field in cellular entries lacking
// an APN dict in |network_configs| list. If |allow_apn_modification| is true,
// "CustomAPNList" is added as a recommended field to the cellular config,
// otherwise, the CustomAPNList field is set to an empty list.
void FillInCellularDefaultsInNetworks(base::Value::List& network_configs,
                                      bool allow_apn_modification) {
  for (auto& network : network_configs) {
    FillInCellularDefaultsInOncObject(kNetworkConfigurationSignature,
                                      network.GetDict(),
                                      allow_apn_modification);
  }
}

// Creates a map from APN IDs to their corresponding configuration dictionaries.
IdToAPNMap BuildIdToAPNMap(const base::Value::List* apn_list) {
  IdToAPNMap apn_map;

  if (!apn_list) {
    return apn_map;
  }

  for (const base::Value& apn_value : *apn_list) {
    const base::Value::Dict& apn_dict = apn_value.GetDict();
    const std::string* apn_id = apn_dict.FindString(::onc::cellular_apn::kId);

    if (apn_id) {
      apn_map.emplace(*apn_id, &apn_dict);
    }
  }

  return apn_map;
}

// Extracts a list of APN dictionaries based on a provided list of APN IDs, such
// that |apn_id_list| is a list of string IDs representing the APNs to extract,
// and |apn_map| is a map of all available APN dictionaries with key being APN
// ID. Returns a base::List if IDs are successfully extracted and the source is
// set successfully, and an std::nullopt otherwise.
std::optional<base::Value::List> ExtractAPNsByIdsAndSetAdminSource(
    const base::Value::List* apn_id_list,
    const IdToAPNMap& apn_map) {
  base::Value::List result = base::Value::List();

  for (const base::Value& apn_id_value : *apn_id_list) {
    const std::string apn_id = apn_id_value.GetString();

    // Find the APN in the map
    auto it = apn_map.find(apn_id);
    if (it == apn_map.end()) {
      NET_LOG(ERROR)
          << "Failed to find an admin provided APN associated to an ID of "
          << apn_id;
      return std::nullopt;
    }
    base::Value::Dict apn_cpy = it->second->Clone();
    apn_cpy.Set(::onc::cellular_apn::kSource,
                ::onc::cellular_apn::kSourceAdmin);

    result.Append(std::move(apn_cpy));
  }

  return result;
}

// Updates a cellular network configuration with custom APN information from
// admin-assigned APNs. Looks for a list of admin-assigned APN IDs in
// |cellular_fields|. If found, it extracts the corresponding APN dictionaries
// from |admin_apn_by_id| and sets the CustomAPNList field in |cellular_fields|.
// Note that if |admin_apn_by_id| is null, no changes are made to
// |cellular_fields|. Also note that each extracted APN will have a
// |::onc::cellular_apn::kSource| of
// |::onc::cellular_apn::kSourceAdmin|. Returns true if |cellular_fields| are
// successfully updated.
bool UpdateCellularFieldsWithAdminApns(base::Value::Dict& cellular_fields,
                                       const IdToAPNMap& admin_apn_by_id) {
  const base::Value::List* admin_apn_id_list =
      cellular_fields.FindList(::onc::cellular::kAdminAssignedAPNIds);
  if (!admin_apn_id_list) {
    return true;
  }

  if (admin_apn_id_list->empty()) {
    cellular_fields.Set(::onc::cellular::kCustomAPNList, base::Value::List());
    return true;
  }

  std::optional<base::Value::List> admin_apns =
      ExtractAPNsByIdsAndSetAdminSource(admin_apn_id_list, admin_apn_by_id);
  if (!admin_apns.has_value()) {
    NET_LOG(ERROR) << "Failed to extract admin APNs";
    return false;
  }

  cellular_fields.Set(::onc::cellular::kCustomAPNList, std::move(*admin_apns));
  return true;
}

bool ConstructAndSetPSIMAdminAPNs(base::Value::Dict& global_network_config,
                                  const IdToAPNMap& admin_apn_by_id) {
  if (admin_apn_by_id.empty()) {
    return true;
  }
  const base::Value::List* psim_admin_apn_id_list =
      global_network_config.FindList(
          ::onc::global_network_config::kPSIMAdminAssignedAPNIds);
  if (!psim_admin_apn_id_list) {
    return true;
  }

  std::optional<base::Value::List> psim_admin_apns =
      ExtractAPNsByIdsAndSetAdminSource(psim_admin_apn_id_list,
                                        admin_apn_by_id);
  if (!psim_admin_apns.has_value()) {
    NET_LOG(ERROR) << "Failed to extract pSIM admin APNs";
    return false;
  }

  global_network_config.Set(
      ::onc::global_network_config::kPSIMAdminAssignedAPNs,
      std::move(*psim_admin_apns));
  return true;
}

// Recursively traverses the |onc_object|, searching for
// cellular dictionaries. If found, it updates the 'CustomAPNList' field within
// the Cellular dictionary using |admin_apn_by_id| if applicable.
//
// The recursion is guided by the |signature|, which defines the structure of
// the ONC object and helps the function determine which fields to traverse.
// Returns true if admin APNs are successfully applied.
bool ApplyAdminApnsToOncObject(const OncValueSignature& signature,
                               base::Value::Dict& onc_object,
                               const IdToAPNMap& admin_apn_by_id) {
  if (&signature == &kCellularSignature) {
    return UpdateCellularFieldsWithAdminApns(onc_object, admin_apn_by_id);
  }

  // The function takes any ONC object and recursively searches until it finds a
  // Cellular dictionary to set the Custom APN List.
  for (auto it : onc_object) {
    if (!it.second.is_dict()) {
      continue;
    }

    const OncFieldSignature* field_signature =
        GetFieldSignature(signature, it.first);
    if (!field_signature) {
      continue;
    }

    if (!ApplyAdminApnsToOncObject(*field_signature->value_signature,
                                   it.second.GetDict(), admin_apn_by_id)) {
      return false;
    }
  }
  return true;
}

// Processes a list of network configurations, identifying those of cellular
// type. For each cellular configuration, it associates and embeds the
// corresponding admin defined APN details found in |admin_apn_by_id|. This is
// achieved by recursively traversing the cellular configuration's structure and
// updating the APN information where applicable.
//
// The function relies on a top-level ONC configuration that contains a list of
// APNs provided by administrators. Cellular networks within the configuration
// may reference these APNs using unique identifiers (IDs).
//
// Ultimately, this function ensures that the cellular networks in the provided
// |network_configs| list are populated with the complete APN configurations
// that they are associated with. Otherwise, it returns false.
bool ConfigureAdminApnsInCellularNetworks(base::Value::List& network_configs,
                                          const IdToAPNMap& admin_apn_by_id) {
  if (admin_apn_by_id.empty()) {
    return true;
  }
  for (auto& network : network_configs) {
    if (!ApplyAdminApnsToOncObject(kNetworkConfigurationSignature,
                                   network.GetDict(), admin_apn_by_id)) {
      return false;
    }
  }
  return true;
}

// Fills HexSSID fields in all entries in the |network_configs| list.
void FillInHexSSIDFieldsInNetworks(base::Value::List& network_configs) {
  for (auto& network : network_configs) {
    FillInHexSSIDFieldsInOncObject(kNetworkConfigurationSignature,
                                   network.GetDict());
  }
}

// Sets HiddenSSID fields in all entries in the |network_configs| list.
void SetHiddenSSIDFieldsInNetworks(base::Value::List& network_configs) {
  for (auto& network : network_configs) {
    SetHiddenSSIDFieldInOncObject(kNetworkConfigurationSignature,
                                  network.GetDict());
  }
}

// Given a GUID->PEM certificate mapping |certs_by_guid|, looks up the PEM
// encoded certificate referenced by |guid_ref|. If a match is found, sets
// |*pem_encoded| to the PEM encoded certificate and returns true. Otherwise,
// returns false.
bool GUIDRefToPEMEncoding(const CertPEMsByGUIDMap& certs_by_guid,
                          const std::string& guid_ref,
                          std::string* pem_encoded) {
  CertPEMsByGUIDMap::const_iterator it = certs_by_guid.find(guid_ref);
  if (it == certs_by_guid.end()) {
    LOG(ERROR) << "Couldn't resolve certificate reference " << guid_ref;
    return false;
  }
  *pem_encoded = it->second;
  if (pem_encoded->empty()) {
    LOG(ERROR) << "Couldn't PEM-encode certificate with GUID " << guid_ref;
    return false;
  }
  return true;
}

// Given a GUID-> PM certificate mapping |certs_by_guid|, attempts to resolve
// the certificate referenced by the |key_guid_ref| field in |onc_object|.
// * If |onc_object| has no |key_guid_ref| field, returns true.
// * If no matching certificate is found in |certs_by_guid|, returns false.
// * If a matching certificate is found, removes the |key_guid_ref| field,
//   fills the |key_pem| field in |onc_object| and returns true.
bool ResolveSingleCertRef(const CertPEMsByGUIDMap& certs_by_guid,
                          const std::string& key_guid_ref,
                          const std::string& key_pem,
                          base::Value::Dict& onc_object) {
  std::string* guid_ref = onc_object.FindString(key_guid_ref);
  if (!guid_ref) {
    return true;
  }

  std::string pem_encoded;
  if (!GUIDRefToPEMEncoding(certs_by_guid, *guid_ref, &pem_encoded)) {
    return false;
  }

  onc_object.Remove(key_guid_ref);
  onc_object.Set(key_pem, pem_encoded);
  return true;
}

// Given a GUID-> PM certificate mapping |certs_by_guid|, attempts to resolve
// the certificates referenced by the list-of-strings field |key_guid_ref_list|
// in |onc_object|.
// * If |key_guid_ref_list| does not exist in |onc_object|, returns true.
// * If any element |key_guid_ref_list| can not be found in |certs_by_guid|,
//   aborts processing and returns false. |onc_object| is unchanged in this
//   case.
// * Otherwise, sets |key_pem_list| to be a list-of-strings field in
//   |onc_object|, containing all PEM encoded resolved certificates in order and
//   returns true.
bool ResolveCertRefList(const CertPEMsByGUIDMap& certs_by_guid,
                        const std::string& key_guid_ref_list,
                        const std::string& key_pem_list,
                        base::Value::Dict& onc_object) {
  const base::Value::List* guid_ref_list =
      onc_object.FindList(key_guid_ref_list);
  if (!guid_ref_list) {
    return true;
  }

  base::Value::List pem_list;
  for (const auto& entry : *guid_ref_list) {
    std::string pem_encoded;
    if (!GUIDRefToPEMEncoding(certs_by_guid, entry.GetString(), &pem_encoded)) {
      return false;
    }

    pem_list.Append(pem_encoded);
  }

  onc_object.Remove(key_guid_ref_list);
  onc_object.Set(key_pem_list, std::move(pem_list));
  return true;
}

// Same as |ResolveSingleCertRef|, but the output |key_pem_list| will be set to
// a list with exactly one value when resolution succeeds.
bool ResolveSingleCertRefToList(const CertPEMsByGUIDMap& certs_by_guid,
                                const std::string& key_guid_ref,
                                const std::string& key_pem_list,
                                base::Value::Dict& onc_object) {
  std::string* guid_ref = onc_object.FindString(key_guid_ref);
  if (!guid_ref) {
    return true;
  }

  std::string pem_encoded;
  if (!GUIDRefToPEMEncoding(certs_by_guid, *guid_ref, &pem_encoded)) {
    return false;
  }

  base::Value::List pem_list;
  pem_list.Append(pem_encoded);
  onc_object.Remove(key_guid_ref);
  onc_object.Set(key_pem_list, std::move(pem_list));
  return true;
}

// Resolves the reference list at |key_guid_refs| if present and otherwise the
// single reference at |key_guid_ref|. Returns whether the respective resolving
// was successful.
bool ResolveCertRefsOrRefToList(const CertPEMsByGUIDMap& certs_by_guid,
                                const std::string& key_guid_refs,
                                const std::string& key_guid_ref,
                                const std::string& key_pem_list,
                                base::Value::Dict& onc_dict) {
  if (onc_dict.contains(key_guid_refs)) {
    if (onc_dict.contains(key_guid_ref)) {
      LOG(ERROR) << "Found both " << key_guid_refs << " and " << key_guid_ref
                 << ". Ignoring and removing the latter.";
      onc_dict.Remove(key_guid_ref);
    }
    return ResolveCertRefList(certs_by_guid, key_guid_refs, key_pem_list,
                              onc_dict);
  }

  // Only resolve |key_guid_ref| if |key_guid_refs| isn't present.
  return ResolveSingleCertRefToList(certs_by_guid, key_guid_ref, key_pem_list,
                                    onc_dict);
}

// Resolve known server and authority certificate reference fields in
// |onc_object|.
bool ResolveServerCertRefsInObject(const CertPEMsByGUIDMap& certs_by_guid,
                                   const OncValueSignature& signature,
                                   base::Value::Dict& onc_object) {
  if (&signature == &kCertificatePatternSignature) {
    if (!ResolveCertRefList(certs_by_guid, ::onc::client_cert::kIssuerCARef,
                            ::onc::client_cert::kIssuerCAPEMs, onc_object)) {
      return false;
    }
  } else if (&signature == &kEAPSignature) {
    if (!ResolveCertRefsOrRefToList(certs_by_guid, ::onc::eap::kServerCARefs,
                                    ::onc::eap::kServerCARef,
                                    ::onc::eap::kServerCAPEMs, onc_object)) {
      return false;
    }
  } else if (&signature == &kIPsecSignature) {
    if (!ResolveCertRefsOrRefToList(certs_by_guid, ::onc::ipsec::kServerCARefs,
                                    ::onc::ipsec::kServerCARef,
                                    ::onc::ipsec::kServerCAPEMs, onc_object)) {
      return false;
    }
  } else if (&signature == &kIPsecSignature ||
             &signature == &kOpenVPNSignature) {
    if (!ResolveSingleCertRef(certs_by_guid, ::onc::openvpn::kServerCertRef,
                              ::onc::openvpn::kServerCertPEM, onc_object) ||
        !ResolveCertRefsOrRefToList(
            certs_by_guid, ::onc::openvpn::kServerCARefs,
            ::onc::openvpn::kServerCARef, ::onc::openvpn::kServerCAPEMs,
            onc_object)) {
      return false;
    }
  }

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

    const OncFieldSignature* field_signature =
        GetFieldSignature(signature, it.first);
    if (!field_signature) {
      continue;
    }

    if (!ResolveServerCertRefsInObject(certs_by_guid,
                                       *field_signature->value_signature,
                                       it.second.GetDict())) {
      return false;
    }
  }
  return true;
}

}  // namespace

std::optional<base::Value::Dict> ReadDictionaryFromJson(
    const std::string& json) {
  if (json.empty()) {
    // Policy may contain empty values, just log a debug message.
    NET_LOG(DEBUG) << "Empty json string";
    return std::nullopt;
  }
  auto parsed_json = base::JSONReader::ReadAndReturnValueWithError(
      json,
      base::JSON_PARSE_CHROMIUM_EXTENSIONS | base::JSON_ALLOW_TRAILING_COMMAS);
  if (!parsed_json.has_value()) {
    NET_LOG(ERROR) << "Invalid JSON Dictionary: "
                   << parsed_json.error().message;
    return std::nullopt;
  }
  if (!parsed_json->is_dict()) {
    NET_LOG(ERROR) << "Invalid JSON Dictionary: Expected a dictionary.";
    return std::nullopt;
  }
  return std::move(*parsed_json).TakeDict();
}

std::optional<base::Value::Dict> Decrypt(const std::string& passphrase,
                                         const base::Value::Dict& root) {
  const int kKeySizeInBits = 256;
  const int kMaxIterationCount = 500000;
  std::string onc_type;
  std::string initial_vector;
  std::string salt;
  std::string cipher;
  std::string stretch_method;
  std::string hmac_method;
  std::string hmac;
  int iterations;
  std::string ciphertext;

  if (!GetString(root, ::onc::encrypted::kCiphertext, &ciphertext) ||
      !GetString(root, ::onc::encrypted::kCipher, &cipher) ||
      !GetString(root, ::onc::encrypted::kHMAC, &hmac) ||
      !GetString(root, ::onc::encrypted::kHMACMethod, &hmac_method) ||
      !GetString(root, ::onc::encrypted::kIV, &initial_vector) ||
      !GetInt(root, ::onc::encrypted::kIterations, &iterations) ||
      !GetString(root, ::onc::encrypted::kSalt, &salt) ||
      !GetString(root, ::onc::encrypted::kStretch, &stretch_method) ||
      !GetString(root, ::onc::toplevel_config::kType, &onc_type) ||
      onc_type != ::onc::toplevel_config::kEncryptedConfiguration) {
    NET_LOG(ERROR) << "Encrypted ONC malformed.";
    return std::nullopt;
  }

  if (hmac_method != ::onc::encrypted::kSHA1 ||
      cipher != ::onc::encrypted::kAES256 ||
      stretch_method != ::onc::encrypted::kPBKDF2) {
    NET_LOG(ERROR) << "Encrypted ONC unsupported encryption scheme.";
    return std::nullopt;
  }

  // Make sure iterations != 0, since that's not valid.
  if (iterations == 0) {
    NET_LOG(ERROR) << kUnableToDecrypt;
    return std::nullopt;
  }

  // Simply a sanity check to make sure we can't lock up the machine
  // for too long with a huge number (or a negative number).
  if (iterations < 0 || iterations > kMaxIterationCount) {
    NET_LOG(ERROR) << "Too many iterations in encrypted ONC";
    return std::nullopt;
  }

  if (!base::Base64Decode(salt, &salt)) {
    NET_LOG(ERROR) << kUnableToDecode;
    return std::nullopt;
  }

  std::unique_ptr<crypto::SymmetricKey> key(
      crypto::SymmetricKey::DeriveKeyFromPasswordUsingPbkdf2(
          crypto::SymmetricKey::AES, passphrase, salt, iterations,
          kKeySizeInBits));

  if (!base::Base64Decode(initial_vector, &initial_vector)) {
    NET_LOG(ERROR) << kUnableToDecode;
    return std::nullopt;
  }
  if (!base::Base64Decode(ciphertext, &ciphertext)) {
    NET_LOG(ERROR) << kUnableToDecode;
    return std::nullopt;
  }
  if (!base::Base64Decode(hmac, &hmac)) {
    NET_LOG(ERROR) << kUnableToDecode;
    return std::nullopt;
  }

  crypto::HMAC hmac_verifier(crypto::HMAC::SHA1);
  if (!hmac_verifier.Init(key.get()) ||
      !hmac_verifier.Verify(ciphertext, hmac)) {
    NET_LOG(ERROR) << kUnableToDecrypt;
    return std::nullopt;
  }

  crypto::Encryptor decryptor;
  if (!decryptor.Init(key.get(), crypto::Encryptor::CBC, initial_vector)) {
    NET_LOG(ERROR) << kUnableToDecrypt;
    return std::nullopt;
  }

  std::string plaintext;
  if (!decryptor.Decrypt(ciphertext, &plaintext)) {
    NET_LOG(ERROR) << kUnableToDecrypt;
    return std::nullopt;
  }

  std::optional<base::Value::Dict> new_root = ReadDictionaryFromJson(plaintext);
  if (!new_root) {
    NET_LOG(ERROR) << "Property dictionary malformed.";
  }
  return new_root;
}

std::string GetSourceAsString(::onc::ONCSource source) {
  switch (source) {
    case ::onc::ONC_SOURCE_UNKNOWN:
      return "unknown";
    case ::onc::ONC_SOURCE_NONE:
      return "none";
    case ::onc::ONC_SOURCE_DEVICE_POLICY:
      return "device policy";
    case ::onc::ONC_SOURCE_USER_POLICY:
      return "user policy";
    case ::onc::ONC_SOURCE_USER_IMPORT:
      return "user import";
  }
  NOTREACHED_IN_MIGRATION();
  return "unknown";
}

void ExpandStringsInOncObject(const OncValueSignature& signature,
                              const VariableExpander& variable_expander,
                              base::Value::Dict* onc_object) {
  if (&signature == &kEAPSignature) {
    ExpandField(::onc::eap::kAnonymousIdentity, variable_expander, onc_object);
    ExpandField(::onc::eap::kIdentity, variable_expander, onc_object);
  } else if (&signature == &kL2TPSignature ||
             &signature == &kOpenVPNSignature) {
    ExpandField(::onc::vpn::kUsername, variable_expander, onc_object);
  }

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

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

    ExpandStringsInOncObject(*field_signature->value_signature,
                             variable_expander, &it.second.GetDict());
  }
}

void ExpandStringsInNetworks(const VariableExpander& variable_expander,
                             base::Value::List& network_configs) {
  for (auto& network : network_configs) {
    ExpandStringsInOncObject(kNetworkConfigurationSignature, variable_expander,
                             &network.GetDict());
  }
}

void FillInCellularCustomAPNListField(
    base::Value::Dict& cellular_fields,
    const base::Value::List* custom_apn_list) {
  if (cellular_fields.Find(::onc::cellular::kCustomAPNList)) {
    NET_LOG(DEBUG) << "kCustomAPNList found, skipping";
    return;
  }

  NET_LOG(DEBUG) << "Filling in kCustomAPNList with "
                 << custom_apn_list->DebugString();
  cellular_fields.Set(::onc::cellular::kCustomAPNList,
                      custom_apn_list->Clone());
}

void FillInCellularCustomAPNListFieldsInOncObject(
    const OncValueSignature& signature,
    base::Value::Dict& onc_object,
    const base::Value::List* custom_apn_list) {
  if (&signature == &kCellularSignature) {
    FillInCellularCustomAPNListField(onc_object, custom_apn_list);
  }

  for (auto it : onc_object) {
    if (!it.second.is_dict()) {
      continue;
    }

    const OncFieldSignature* field_signature =
        GetFieldSignature(signature, it.first);
    if (!field_signature) {
      continue;
    }

    FillInCellularCustomAPNListFieldsInOncObject(
        *field_signature->value_signature, it.second.GetDict(),
        custom_apn_list);
  }
}

void FillInHexSSIDFieldsInOncObject(const OncValueSignature& signature,
                                    base::Value::Dict& onc_object) {
  if (&signature == &kWiFiSignature)
    FillInHexSSIDField(onc_object);

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

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

    FillInHexSSIDFieldsInOncObject(*field_signature->value_signature,
                                   it.second.GetDict());
  }
}

void FillInHexSSIDField(base::Value::Dict& wifi_fields) {
  if (wifi_fields.Find(::onc::wifi::kHexSSID)) {
    return;
  }
  std::string* ssid = wifi_fields.FindString(::onc::wifi::kSSID);
  if (!ssid) {
    return;
  }
  if (ssid->empty()) {
    NET_LOG(ERROR) << "Found empty SSID field.";
    return;
  }
  wifi_fields.Set(::onc::wifi::kHexSSID, base::HexEncode(*ssid));
}

void SetHiddenSSIDFieldInOncObject(const OncValueSignature& signature,
                                   base::Value::Dict& onc_object) {
  if (&signature == &kWiFiSignature) {
    SetHiddenSSIDField(onc_object);
  }

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

    const OncFieldSignature* field_signature =
        GetFieldSignature(signature, it.first);
    if (!field_signature) {
      continue;
    }

    SetHiddenSSIDFieldInOncObject(*field_signature->value_signature,
                                  it.second.GetDict());
  }
}

void SetHiddenSSIDField(base::Value::Dict& wifi_fields) {
  if (wifi_fields.Find(::onc::wifi::kHiddenSSID)) {
    return;
  }
  wifi_fields.Set(::onc::wifi::kHiddenSSID, false);
}

base::Value::Dict MaskCredentialsInOncObject(
    const OncValueSignature& signature,
    const base::Value::Dict& onc_object,
    const std::string& mask) {
  return OncMaskValues::Mask(signature, onc_object, mask);
}

std::string DecodePEM(const std::string& pem_encoded) {
  // The PEM block header used for DER certificates
  const char kCertificateHeader[] = "CERTIFICATE";

  // This is an older PEM marker for DER certificates.
  const char kX509CertificateHeader[] = "X509 CERTIFICATE";

  std::vector<std::string> pem_headers;
  pem_headers.push_back(kCertificateHeader);
  pem_headers.push_back(kX509CertificateHeader);

  bssl::PEMTokenizer pem_tokenizer(pem_encoded, pem_headers);
  std::string decoded;
  if (pem_tokenizer.GetNext()) {
    decoded = pem_tokenizer.data();
  } else {
    // If we failed to read the data as a PEM file, then try plain base64 decode
    // in case the PEM marker strings are missing. For this to work, there has
    // to be no white space, and it has to only contain the base64-encoded data.
    if (!base::Base64Decode(pem_encoded, &decoded)) {
      LOG(ERROR) << "Unable to base64 decode X509 data: " << pem_encoded;
      return std::string();
    }
  }
  return decoded;
}

bool ParseAndValidateOncForImport(const std::string& onc_blob,
                                  ::onc::ONCSource onc_source,
                                  const std::string& passphrase,
                                  base::Value::List* network_configs,
                                  base::Value::Dict* global_network_config,
                                  base::Value::List* certificates) {
  if (network_configs) {
    network_configs->clear();
  }
  if (global_network_config) {
    global_network_config->clear();
  }
  if (certificates) {
    certificates->clear();
  }
  if (onc_blob.empty()) {
    return true;
  }

  std::optional<base::Value::Dict> toplevel_onc =
      ReadDictionaryFromJson(onc_blob);
  if (!toplevel_onc) {
    NET_LOG(ERROR) << "Not a valid ONC JSON dictionary: "
                   << GetSourceAsString(onc_source);
    return false;
  }

  // Check and see if this is an encrypted ONC file. If so, decrypt it.
  std::string onc_type;
  if (GetString(toplevel_onc.value(), ::onc::toplevel_config::kType,
                &onc_type) &&
      onc_type == ::onc::toplevel_config::kEncryptedConfiguration) {
    toplevel_onc = Decrypt(passphrase, toplevel_onc.value());
    if (!toplevel_onc.has_value()) {
      NET_LOG(ERROR) << "Unable to decrypt ONC from "
                     << GetSourceAsString(onc_source);
      return false;
    }
  }

  bool from_policy = (onc_source == ::onc::ONC_SOURCE_USER_POLICY ||
                      onc_source == ::onc::ONC_SOURCE_DEVICE_POLICY);

  // Validate the ONC dictionary. We are liberal and ignore unknown field
  // names and ignore invalid field names in kRecommended arrays.
  Validator validator(/*error_on_unknown_field=*/false,
                      /*error_on_wrong_recommended=*/false,
                      /*error_on_missing_field=*/true,
                      /*managed_onc=*/from_policy,
                      /*log_warnings=*/true);
  validator.SetOncSource(onc_source);

  Validator::Result validation_result;
  std::optional<base::Value::Dict> validated_toplevel_onc =
      validator.ValidateAndRepairObject(&kToplevelConfigurationSignature,
                                        toplevel_onc.value(),
                                        &validation_result);

  bool success = true;
  if (validation_result == Validator::VALID_WITH_WARNINGS) {
    NET_LOG(DEBUG) << "ONC validation produced warnings: "
                   << GetSourceAsString(onc_source);
    success = false;
  } else if (validation_result == Validator::INVALID ||
             !validated_toplevel_onc.has_value()) {
    NET_LOG(ERROR) << "ONC is invalid and couldn't be repaired: "
                   << GetSourceAsString(onc_source);
    return false;
  }

  if (certificates) {
    base::Value::List* validated_certs =
        validated_toplevel_onc->FindList(::onc::toplevel_config::kCertificates);
    if (validated_certs)
      *certificates = std::move(*validated_certs);
  }

  // Note that this processing is performed even if |network_configs| is
  // nullptr, because ResolveServerCertRefsInNetworks could affect the return
  // value of the function (which is supposed to aggregate validation issues in
  // all segments of the ONC blob).
  base::Value::List* validated_networks_list = validated_toplevel_onc->FindList(
      ::onc::toplevel_config::kNetworkConfigurations);

  base::Value::Dict* validated_global_config = validated_toplevel_onc->FindDict(
      ::onc::toplevel_config::kGlobalNetworkConfiguration);

  const IdToAPNMap id_to_apn_map = BuildIdToAPNMap(
      validated_toplevel_onc->FindList(::onc::toplevel_config::kAdminAPNList));

  if (validated_networks_list) {
    FillInHexSSIDFieldsInNetworks(*validated_networks_list);

    bool allow_apn_modification = true;
    if (validated_global_config) {
      allow_apn_modification =
          (validated_global_config->FindBool(
               ::onc::global_network_config::kAllowAPNModification))
              .value_or(allow_apn_modification);
    }

    FillInCellularDefaultsInNetworks(*validated_networks_list,
                                     allow_apn_modification);

    // Sets the CustomAPNList for cellular networks if an AdminAPNList and
    // AdminAssignedAPNIds have been specified for a cellular network.
    if (!ConfigureAdminApnsInCellularNetworks(*validated_networks_list,
                                              id_to_apn_map)) {
      success = false;
    }

    // Set HiddenSSID to default value to solve the issue crbug.com/1171837
    SetHiddenSSIDFieldsInNetworks(*validated_networks_list);

    CertPEMsByGUIDMap server_and_ca_certs =
        GetServerAndCACertsByGUID(*certificates);

    if (!ResolveServerCertRefsInNetworks(server_and_ca_certs,
                                         *validated_networks_list)) {
      NET_LOG(ERROR) << "Some certificate references in the ONC policy could "
                        "not be resolved: "
                     << GetSourceAsString(onc_source);
      success = false;
    }

    if (network_configs) {
      *network_configs = std::move(*validated_networks_list);
    }
  }

  if (global_network_config) {
    if (validated_global_config) {
      // Constructs and sets the PSIMAdminAssignedAPNs global network
      // configuration field if an AdminAPNList and PSIMAdminAssignedAPNIds have
      // been specified.
      if (!ConstructAndSetPSIMAdminAPNs(*validated_global_config,
                                        id_to_apn_map)) {
        success = false;
      }
      *global_network_config = std::move(*validated_global_config);
    }
  }

  return success;
}

bool ResolveServerCertRefsInNetworks(const CertPEMsByGUIDMap& certs_by_guid,
                                     base::Value::List& network_configs) {
  bool success = true;
  base::Value::List filtered_configs;
  for (base::Value& network : network_configs) {
    if (!ResolveServerCertRefsInNetwork(certs_by_guid, network.GetDict())) {
      std::string* guid =
          network.GetDict().FindString(::onc::network_config::kGUID);
      // This might happen even with correct validation, if the referenced
      // certificate couldn't be imported.
      LOG(ERROR) << "Couldn't resolve some certificate reference of network "
                 << (guid ? *guid : "(unable to find GUID)");
      success = false;
      continue;
    }

    filtered_configs.Append(std::move(network));
  }
  network_configs = std::move(filtered_configs);
  return success;
}

bool ResolveServerCertRefsInNetwork(const CertPEMsByGUIDMap& certs_by_guid,
                                    base::Value::Dict& network_config) {
  return ResolveServerCertRefsInObject(
      certs_by_guid, kNetworkConfigurationSignature, network_config);
}

}  // namespace chromeos::onc