chromium/chrome/browser/component_updater/widevine_cdm_component_installer.cc

// Copyright 2013 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/component_updater/widevine_cdm_component_installer.h"

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

#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/native_library.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "base/version.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "components/cdm/common/cdm_manifest.h"
#include "components/component_updater/component_installer.h"
#include "components/component_updater/component_updater_service.h"
#include "components/version_info/version_info.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/cdm_registry.h"
#include "content/public/common/cdm_info.h"
#include "content/public/common/content_paths.h"
#include "crypto/sha2.h"
#include "media/base/cdm_capability.h"
#include "media/cdm/cdm_paths.h"
#include "third_party/widevine/cdm/buildflags.h"
#include "third_party/widevine/cdm/widevine_cdm_common.h"

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#include "base/path_service.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/media/component_widevine_cdm_hint_file_linux.h"
#endif

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chromeos/ash/components/dbus/image_loader/image_loader_client.h"
#endif

#if !BUILDFLAG(ENABLE_WIDEVINE_CDM_COMPONENT)
#error This file should only be compiled when Widevine CDM component is enabled
#endif

namespace component_updater {

namespace {

// CRX hash. The extension id is: oimompecagnajdejgnnjijobebaeigek.
const uint8_t kWidevineSha2Hash[] = {
    0xe8, 0xce, 0xcf, 0x42, 0x06, 0xd0, 0x93, 0x49, 0x6d, 0xd9, 0x89,
    0xe1, 0x41, 0x04, 0x86, 0x4a, 0x8f, 0xbd, 0x86, 0x12, 0xb9, 0x58,
    0x9b, 0xfb, 0x4f, 0xbb, 0x1b, 0xa9, 0xd3, 0x85, 0x37, 0xef};
static_assert(std::size(kWidevineSha2Hash) == crypto::kSHA256Length,
              "Wrong hash length");

#if BUILDFLAG(IS_CHROMEOS_ASH)
// On ChromeOS the component updated CDM comes as a disk image which must be
// registered and then mounted in order to access the files. The startup
// script that mounts the image (widevine-cdm.conf) also uses this name.
const char ImageLoaderComponentName[] = "WidevineCdm";
#endif

#if !BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)
// On Linux and ChromeOS the Widevine CDM is loaded at startup before the
// zygote is locked down. As a result there is no need to register the CDM
// with Chrome as it can't be used until Chrome is restarted.
void RegisterWidevineCdmWithChrome(const base::Version& cdm_version,
                                   const base::FilePath& cdm_path,
                                   base::Value::Dict manifest) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // This check must be a subset of the check in VerifyInstallation() to
  // avoid the case where the CDM is accepted by the component updater
  // but not registered.
  media::CdmCapability capability;
  if (!ParseCdmManifest(manifest, &capability)) {
    VLOG(1) << "Not registering Widevine CDM due to malformed manifest.";
    return;
  }

  VLOG(1) << "Registering Widevine CDM " << cdm_version << " with Chrome";

  content::CdmInfo cdm_info(
      kWidevineKeySystem, content::CdmInfo::Robustness::kSoftwareSecure,
      std::move(capability), /*supports_sub_key_systems=*/false,
      kWidevineCdmDisplayName, kWidevineCdmType, cdm_version, cdm_path);
  content::CdmRegistry::GetInstance()->RegisterCdm(cdm_info);
}
#endif  // !BUILDFLAG(IS_LINUX) && !BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
// On Linux and ChromeOS the Widevine CDM is loaded at startup before the
// zygote is locked down. To locate the Widevine CDM at startup, a hint file
// is used. Update the hint file with the new Widevine CDM path.
bool UpdateHintFile(const base::FilePath& cdm_base_path) {
  // Also record the current bundled Widevine CDMs version, if a bundled
  // Widevine CDM is supported and it exists.
  std::optional<base::Version> bundled_version;

#if BUILDFLAG(BUNDLE_WIDEVINE_CDM)
  base::FilePath bundled_cdm_file_path;
  CHECK(base::PathService::Get(chrome::DIR_BUNDLED_WIDEVINE_CDM,
                               &bundled_cdm_file_path));

  auto manifest_path =
      bundled_cdm_file_path.Append(FILE_PATH_LITERAL("manifest.json"));
  base::Version version;
  media::CdmCapability capability;
  if (ParseCdmManifestFromPath(manifest_path, &version, &capability)) {
    bundled_version = version;
  }
#endif  // BUILDFLAG(BUNDLE_WIDEVINE_CDM)

  return UpdateWidevineCdmHintFile(cdm_base_path, bundled_version);
}

#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)

// Determine the full path to the Widevine CDM binary.
base::FilePath GetCdmPathFromInstallDir(const base::FilePath& install_dir) {
  base::FilePath cdm_platform_dir =
      media::GetPlatformSpecificDirectory(install_dir);
  std::string cdm_lib_name =
      base::GetNativeLibraryName(kWidevineCdmLibraryName);
  base::FilePath cdm_path = cdm_platform_dir.AppendASCII(cdm_lib_name);
  DVLOG(1) << __func__ << ": cdm_path=" << cdm_path;
  return cdm_path;
}

#if BUILDFLAG(IS_CHROMEOS_ASH)
// This is called when ImageLoaderClient::RegisterComponent() is done.
void OnImageRegistered(std::optional<bool> result) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // `result` is false if the component fails verification, nullopt if an error
  // occurred. If registration fails there is not much we can do other than
  // log a message.
  if (!result.value_or(false)) {
    VLOG(1) << "Component Widevine registration failed.";
    return;
  }
}

// This is called on the UI thread to register the image that has been
// downloaded.
void RegisterImage(const std::string& version,
                   const base::FilePath& install_dir) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DVLOG(1) << __func__ << ": version=" << version << ", dir=" << install_dir;

  auto* loader = ash::ImageLoaderClient::Get();
  if (!loader) {
    VLOG(1) << "ImageLoader not available.";
    return;
  }

  // Registering the component allows it to be mounted by name later. The name
  // is used by the startup script widevine-cdm.conf to mount the image so the
  // contained files are available when Chrome starts. The name is also used by
  // UpdateCdmPath() to mount the image so the hint file can be updated.
  loader->RegisterComponent(ImageLoaderComponentName, version,
                            install_dir.value(),
                            base::BindOnce(&OnImageRegistered));
}

// Called to verify the manifest and update the hint file if everything looks
// valid. The directory `image_dir` should be a valid directory.
void VerifyManifestAndUpdateHintFile(const std::string& image_dir) {
  // Image loaded, so check that the manifest is valid.
  base::FilePath mount_point(image_dir);
  auto manifest_path = mount_point.Append(FILE_PATH_LITERAL("manifest.json"));
  base::Version version;
  media::CdmCapability capability;
  if (!ParseCdmManifestFromPath(manifest_path, &version, &capability)) {
    VLOG(1) << "Widevine image does not contain expected manifest.";
    return;
  }

  // Mounted image should also contain the actual binary.
  base::FilePath cdm_path = GetCdmPathFromInstallDir(mount_point);
  if (!base::PathExists(cdm_path)) {
    VLOG(1) << "Widevine image does not contain expected binary.";
    return;
  }

  // As we're happy with the contents, update the hint file so this version can
  // be used next time the device restarts.
  if (!UpdateHintFile(mount_point)) {
    VLOG(1) << "Failed to update Widevine CDM hint path.";
  }
}

// This is called when an image has been loaded, and `image_dir` is the
// directory where it has been mounted. This directory should contain the
// directory structure expected for Widevine (in particular "manifest.json"
// at the top level, binary in "_platform_specific/<platform>"). If the image
// was successfully loaded, register it with Chrome via the hint file so that
// it can be loaded next time ChromeOS restarts.
void OnImageLoaded(std::optional<std::string> image_dir) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // Mounting should not fail, but if it does simply log a message. This will
  // be tried again next time the device reboots.
  if (!image_dir.has_value()) {
    VLOG(1) << "Failed to load image for Widevine.";
    return;
  }

  // As reading the manifest and writing the hint file cause I/O, run on a
  // thread that allows blocking.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock()},
      base::BindOnce(&VerifyManifestAndUpdateHintFile, image_dir.value()));
}

// This is called on the UI thread to load the latest registered image for
// Widevine.
void LoadImage() {
  auto* loader = ash::ImageLoaderClient::Get();
  if (!loader) {
    VLOG(1) << "ImageLoader not available.";
    return;
  }

  loader->LoadComponent(ImageLoaderComponentName,
                        base::BindOnce(&OnImageLoaded));
}
#endif  // BUILDFLAG(IS_CHROMEOS_ASH)

}  // namespace

class WidevineCdmComponentInstallerPolicy : public ComponentInstallerPolicy {
 public:
  WidevineCdmComponentInstallerPolicy();

  WidevineCdmComponentInstallerPolicy(
      const WidevineCdmComponentInstallerPolicy&) = delete;
  WidevineCdmComponentInstallerPolicy& operator=(
      const WidevineCdmComponentInstallerPolicy&) = delete;

  ~WidevineCdmComponentInstallerPolicy() override = default;

 private:
  // The following methods override ComponentInstallerPolicy.
  bool SupportsGroupPolicyEnabledComponentUpdates() const override;
  bool RequiresNetworkEncryption() const override;
  update_client::CrxInstaller::Result OnCustomInstall(
      const base::Value::Dict& manifest,
      const base::FilePath& install_dir) override;
  void OnCustomUninstall() override;
  bool VerifyInstallation(const base::Value::Dict& manifest,
                          const base::FilePath& install_dir) const override;
  void ComponentReady(const base::Version& version,
                      const base::FilePath& path,
                      base::Value::Dict manifest) override;
  base::FilePath GetRelativeInstallDir() const override;
  void GetHash(std::vector<uint8_t>* hash) const override;
  std::string GetName() const override;
  update_client::InstallerAttributes GetInstallerAttributes() const override;

  // Updates CDM path if necessary.
  void UpdateCdmPath(const base::Version& cdm_version,
                     const base::FilePath& cdm_install_dir,
                     base::Value::Dict manifest);
};

WidevineCdmComponentInstallerPolicy::WidevineCdmComponentInstallerPolicy() =
    default;

bool WidevineCdmComponentInstallerPolicy::
    SupportsGroupPolicyEnabledComponentUpdates() const {
  return true;
}

bool WidevineCdmComponentInstallerPolicy::RequiresNetworkEncryption() const {
  return false;
}

update_client::CrxInstaller::Result
WidevineCdmComponentInstallerPolicy::OnCustomInstall(
    const base::Value::Dict& manifest,
    const base::FilePath& install_dir) {
  DVLOG(1) << __func__ << ": install_dir=" << install_dir
           << ", manifest=" << manifest;

#if BUILDFLAG(IS_CHROMEOS_ASH)
  // On ASH ChromeOS, anything downloaded by Component Updater is an image
  // that needs to be mounted before the files it contains can be used. So
  // simply register the image, so that it can be mounted next time the
  // device boots. It will also be mounted by UpdateCdmPath() so that the hint
  // file can be updated.
  auto* version = manifest.FindString("version");
  if (!version) {
    return update_client::CrxInstaller::Result(
        update_client::InstallError::BAD_MANIFEST);
  }

  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(&RegisterImage, *version, install_dir));
#endif

  return update_client::CrxInstaller::Result(update_client::InstallError::NONE);
}

void WidevineCdmComponentInstallerPolicy::OnCustomUninstall() {}

// Once the CDM is ready, update the CDM path.
void WidevineCdmComponentInstallerPolicy::ComponentReady(
    const base::Version& version,
    const base::FilePath& path,
    base::Value::Dict manifest) {
  DVLOG(1) << __func__ << ": version=" << version << ", path=" << path;
  if (!IsCdmManifestCompatibleWithChrome(manifest)) {
    VLOG(1) << "Widevine CDM component " << version << " is incompatible.";
    return;
  }

  // Widevine CDM affects encrypted media playback, hence USER_VISIBLE.
  // See http://crbug.com/900169 for the context.
  base::ThreadPool::PostTask(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(&WidevineCdmComponentInstallerPolicy::UpdateCdmPath,
                     base::Unretained(this), version, path,
                     std::move(manifest)));
}

bool WidevineCdmComponentInstallerPolicy::VerifyInstallation(
    const base::Value::Dict& manifest,
    const base::FilePath& install_dir) const {
#if !BUILDFLAG(IS_CHROMEOS_ASH)
  // On ChromeOS, what gets downloaded is an image rather than the directory
  // structure expected. As a result, we can not check that there is an
  // library contained until the image is loaded. But on all other systems
  // we can check for the library.
  base::FilePath cdm_path = GetCdmPathFromInstallDir(install_dir);
  if (!base::PathExists(cdm_path)) {
    return false;
  }
#endif

  // Validate that the manifest looks reasonable.
  media::CdmCapability capability;
  return IsCdmManifestCompatibleWithChrome(manifest) &&
         ParseCdmManifest(manifest, &capability);
}

// The base directory on Windows looks like:
// <profile>\AppData\Local\Google\Chrome\User Data\WidevineCdm\.
base::FilePath WidevineCdmComponentInstallerPolicy::GetRelativeInstallDir()
    const {
  return base::FilePath::FromUTF8Unsafe(kWidevineCdmBaseDirectory);
}

void WidevineCdmComponentInstallerPolicy::GetHash(
    std::vector<uint8_t>* hash) const {
  hash->assign(kWidevineSha2Hash,
               kWidevineSha2Hash + std::size(kWidevineSha2Hash));
}

std::string WidevineCdmComponentInstallerPolicy::GetName() const {
  return kWidevineCdmDisplayName;
}

update_client::InstallerAttributes
WidevineCdmComponentInstallerPolicy::GetInstallerAttributes() const {
  return update_client::InstallerAttributes();
}

void WidevineCdmComponentInstallerPolicy::UpdateCdmPath(
    const base::Version& cdm_version,
    const base::FilePath& cdm_install_dir,
    base::Value::Dict manifest) {
  // This function is called by ComponentReady() on a separate thread.
  DVLOG(1) << __func__ << ": version=" << cdm_version
           << ", dir=" << cdm_install_dir;

  // On some platforms (e.g. Mac) we use symlinks for paths. Convert paths to
  // absolute paths to avoid unexpected failure. base::MakeAbsoluteFilePath()
  // requires IO so it can only be done in this function.
  const base::FilePath absolute_cdm_install_dir =
      base::MakeAbsoluteFilePath(cdm_install_dir);
  if (absolute_cdm_install_dir.empty()) {
    PLOG(WARNING) << "Failed to get absolute CDM install path.";
    return;
  }

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS)
  VLOG(1) << "Updating hint file with Widevine CDM " << cdm_version;

  // This is running on a thread that allows IO, so simply update the hint file.
  if (!UpdateHintFile(absolute_cdm_install_dir)) {
    PLOG(WARNING) << "Failed to update Widevine CDM hint path.";
  }

#elif BUILDFLAG(IS_CHROMEOS_ASH)
  // On ChromeOS ASH, the selected CDM could be the bundled CDM or an image
  // containing the CDM downloaded by CU. As the CDM is loaded when Chrome
  // starts, there is no need to register it as the new version can't be
  // used until the device restarts. However, we do want to update the hint
  // file to indicate the new version so that it's loaded next time Chrome
  // starts.
  //
  // If CU decides that the bundled CDM is the latest, there is no need to load
  // the image as the bundled CDM is already a directory containing the CDM and
  // not an image. It also doesn't need to update the hint file as
  // cdm_registration.cc checks the bundled directory explicitly.
  //
  // If this is not the bundled CDM, then it is an image and we need to mount
  // the image to know where it will be found the next time the device is
  // restarted (by script widevine-cdm.conf). Mounting the image now lets us
  // verify the contents of the image and update the hint file (if the image
  // contains the necessary files).
  base::FilePath bundled_dir;
  CHECK(base::PathService::Get(chrome::DIR_BUNDLED_WIDEVINE_CDM, &bundled_dir));
  if (absolute_cdm_install_dir != bundled_dir) {
    content::GetUIThreadTaskRunner({})->PostTask(FROM_HERE,
                                                 base::BindOnce(&LoadImage));
  }

#else
  // On other platforms (e.g. Windows, Mac) where the CDM can be dynamically
  // loaded, register the new CDM so that it can be used.
  content::GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE,
      base::BindOnce(&RegisterWidevineCdmWithChrome, cdm_version,
                     GetCdmPathFromInstallDir(absolute_cdm_install_dir),
                     std::move(manifest)));
#endif
}

void RegisterWidevineCdmComponent(ComponentUpdateService* cus) {
  auto installer = base::MakeRefCounted<ComponentInstaller>(
      std::make_unique<WidevineCdmComponentInstallerPolicy>());
  installer->Register(cus, base::OnceClosure());
}

}  // namespace component_updater