chromium/chrome/browser/ash/tpm_firmware_update.cc

// Copyright 2017 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/tpm_firmware_update.h"

#include <memory>
#include <utility>

#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/files/file_path_watcher.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/policy/core/browser_policy_connector_ash.h"
#include "chrome/browser/ash/policy/enrollment/auto_enrollment_type_checker.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browser_process_platform_part.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chromeos/ash/components/install_attributes/install_attributes.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/policy/proto/chrome_device_policy.pb.h"

namespace ash {
namespace tpm_firmware_update {

namespace {

// Decodes a |settings| dictionary into a set of allowed update modes.
std::set<Mode> GetModesFromSetting(const base::Value* settings) {
  std::set<Mode> modes;
  if (!settings)
    return modes;

  const base::Value::Dict& settings_dict = settings->GetDict();
  std::optional<bool> allow_powerwash =
      settings_dict.FindBool(kSettingsKeyAllowPowerwash);
  if (allow_powerwash && *allow_powerwash) {
    modes.insert(Mode::kPowerwash);
  }
  std::optional<bool> allow_preserve_device_state =
      settings_dict.FindBool(kSettingsKeyAllowPreserveDeviceState);
  if (allow_preserve_device_state && *allow_preserve_device_state) {
    modes.insert(Mode::kPreserveDeviceState);
  }

  return modes;
}

}  // namespace

const char kSettingsKeyAllowPowerwash[] = "allow-user-initiated-powerwash";
const char kSettingsKeyAllowPreserveDeviceState[] =
    "allow-user-initiated-preserve-device-state";
const char kSettingsKeyAutoUpdateMode[] = "auto-update-mode";

base::Value DecodeSettingsProto(
    const enterprise_management::TPMFirmwareUpdateSettingsProto& settings) {
  base::Value::Dict result;

  if (settings.has_allow_user_initiated_powerwash()) {
    result.Set(kSettingsKeyAllowPowerwash,
               settings.allow_user_initiated_powerwash());
  }
  if (settings.has_allow_user_initiated_preserve_device_state()) {
    result.Set(kSettingsKeyAllowPreserveDeviceState,
               settings.allow_user_initiated_preserve_device_state());
  }

  if (settings.has_auto_update_mode()) {
    result.Set(kSettingsKeyAutoUpdateMode, settings.auto_update_mode());
  }

  return base::Value(std::move(result));
}

// AvailabilityChecker tracks TPM firmware update availability information
// exposed by the system via the /run/tpm_firmware_update file. There are three
// states:
//  1. The file isn't present - availability check is still pending.
//  2. The file is present, but empty - no update available.
//  3. The file is present, non-empty - update binary path is in the file.
//
// AvailabilityChecker employs a FilePathWatcher to watch the file and hides
// away all the gory threading details.
class AvailabilityChecker {
 public:
  struct Status {
    bool update_available = false;
    bool srk_vulnerable_roca = false;
  };
  using ResponseCallback = base::OnceCallback<void(const Status&)>;

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

  ~AvailabilityChecker() { Cancel(); }

  static void Start(ResponseCallback callback, base::TimeDelta timeout) {
    // Schedule a task to run when the timeout expires. The task also owns
    // |checker| and thus takes care of eventual deletion.
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &AvailabilityChecker::OnTimeout,
            std::make_unique<AvailabilityChecker>(std::move(callback))),
        timeout);
  }

  // Don't call this directly, but use Start().
  explicit AvailabilityChecker(ResponseCallback callback)
      : callback_(std::move(callback)),
        background_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
            {base::MayBlock(), base::TaskPriority::USER_VISIBLE})),
        watcher_(new base::FilePathWatcher()) {
    auto watch_callback =
        base::BindRepeating(&AvailabilityChecker::OnFilePathChanged,
                            base::SequencedTaskRunner::GetCurrentDefault(),
                            weak_ptr_factory_.GetWeakPtr());
    background_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&AvailabilityChecker::StartOnBackgroundThread,
                                  watcher_.get(), watch_callback));
  }

 private:
  static base::FilePath GetUpdateLocationFilePath() {
    base::FilePath update_location_file;
    CHECK(base::PathService::Get(
        chrome::FILE_CHROME_OS_TPM_FIRMWARE_UPDATE_LOCATION,
        &update_location_file));
    return update_location_file;
  }

  static bool CheckAvailabilityStatus(Status* status) {
    int64_t size;
    if (!base::GetFileSize(GetUpdateLocationFilePath(), &size)) {
      // File doesn't exist or error - can't determine availability status.
      return false;
    }
    status->update_available = size > 0;
    base::FilePath srk_vulnerable_roca_file;
    CHECK(base::PathService::Get(
        chrome::FILE_CHROME_OS_TPM_FIRMWARE_UPDATE_SRK_VULNERABLE_ROCA,
        &srk_vulnerable_roca_file));
    status->srk_vulnerable_roca = base::PathExists(srk_vulnerable_roca_file);
    return true;
  }

  static void StartOnBackgroundThread(
      base::FilePathWatcher* watcher,
      base::FilePathWatcher::Callback watch_callback) {
    watcher->Watch(GetUpdateLocationFilePath(),
                   base::FilePathWatcher::Type::kNonRecursive, watch_callback);
    watch_callback.Run(base::FilePath(), false /* error */);
  }

  static void OnFilePathChanged(
      scoped_refptr<base::SequencedTaskRunner> origin_task_runner,
      base::WeakPtr<AvailabilityChecker> checker,
      const base::FilePath& target,
      bool error) {
    Status status;
    if (CheckAvailabilityStatus(&status) || error) {
      origin_task_runner->PostTask(
          FROM_HERE,
          base::BindOnce(&AvailabilityChecker::Resolve, checker, status));
    }
  }

  void Resolve(const Status& status) {
    Cancel();
    if (callback_) {
      std::move(callback_).Run(status);
    }
  }

  void Cancel() {
    // Neutralize further callbacks from |watcher_| or due to timeout.
    weak_ptr_factory_.InvalidateWeakPtrs();
    background_task_runner_->DeleteSoon(FROM_HERE, std::move(watcher_));
  }

  void OnTimeout() {
    // If |callback_| hasn't been triggered when the timeout task fires, perform
    // a last check and wire the result into a |callback_| execution to make
    // sure a result is delivered in all cases. Note that |OnTimeout()| gets run
    // via a callback that owns |this|, so the object will be destructed after
    // this function terminates. Thus, the final check needs to run independent
    // of |this| and takes |callback_| ownership.
    if (callback_) {
      background_task_runner_->PostTaskAndReplyWithResult(
          FROM_HERE, base::BindOnce([]() {
            Status status;
            CheckAvailabilityStatus(&status);
            return status;
          }),
          std::move(callback_));
    }
  }

  ResponseCallback callback_;
  scoped_refptr<base::SequencedTaskRunner> background_task_runner_;
  std::unique_ptr<base::FilePathWatcher> watcher_;
  base::WeakPtrFactory<AvailabilityChecker> weak_ptr_factory_{this};
};

void GetAvailableUpdateModes(
    base::OnceCallback<void(const std::set<Mode>&)> completion,
    base::TimeDelta timeout) {
  if (!base::FeatureList::IsEnabled(features::kTPMFirmwareUpdate)) {
    std::move(completion).Run(std::set<Mode>());
    return;
  }

  std::set<Mode> modes;
  if (g_browser_process->platform_part()
          ->browser_policy_connector_ash()
          ->IsDeviceEnterpriseManaged()) {
    // Split |completion| in two. This is necessary because of the
    // PrepareTrustedValues API, which for some return values invokes the
    // callback passed to it, and for others requires the code here to do so.
    auto split_completion = base::SplitOnceCallback(std::move(completion));

    // For enterprise-managed devices, always honor the device setting.
    CrosSettings* const cros_settings = CrosSettings::Get();
    switch (cros_settings->PrepareTrustedValues(
        base::BindOnce(&GetAvailableUpdateModes,
                       std::move(split_completion.first), timeout))) {
      case CrosSettingsProvider::TEMPORARILY_UNTRUSTED:
        // Retry happens via the callback registered above.
        return;
      case CrosSettingsProvider::PERMANENTLY_UNTRUSTED:
        // No device settings? Default to disallow.
        std::move(split_completion.second).Run(std::set<Mode>());
        return;
      case CrosSettingsProvider::TRUSTED:
        // Setting is present and trusted so respect its value.
        modes = GetModesFromSetting(
            cros_settings->GetPref(kTPMFirmwareUpdateSettings));

        // Reset |completion| here so we can invoke it further down.
        completion = std::move(split_completion.second);
        break;
    }
  } else {
    // Consumer device or still in OOBE.
    if (!InstallAttributes::Get()->IsDeviceLocked()) {
      // Device in OOBE. If FRE is required, enterprise enrollment might still
      // be pending, in which case TPM firmware updates are disallowed until
      // FRE determines that the device is not remotely managed or it does get
      // enrolled and the admin allows TPM firmware updates.
      const auto requirement =
          policy::AutoEnrollmentTypeChecker::GetFRERequirementAccordingToVPD(
              system::StatisticsProvider::GetInstance());
      if (requirement == policy::AutoEnrollmentTypeChecker::FRERequirement::
                             kExplicitlyRequired) {
        std::move(completion).Run(std::set<Mode>());
        return;
      }
    }

    // All modes are available for consumer devices.
    modes.insert(Mode::kPowerwash);
    modes.insert(Mode::kPreserveDeviceState);
  }

  // No need to check for availability if no update modes are allowed.
  if (modes.empty()) {
    std::move(completion).Run(std::set<Mode>());
    return;
  }

  // Some TPM firmware update modes are allowed. Last thing to check is whether
  // there actually is a pending update.
  AvailabilityChecker::Start(
      base::BindOnce(
          [](std::set<Mode> modes,
             base::OnceCallback<void(const std::set<Mode>&)> callback,
             const AvailabilityChecker::Status& status) {
            DCHECK_LT(0U, modes.size());
            DCHECK_EQ(0U, modes.count(Mode::kCleanup));
            if (status.update_available) {
              std::move(callback).Run(modes);
              return;
            }

            // If there is no update, but the SRK is vulnerable, allow cleanup
            // to take place. Note that at least one allowed actual mode is
            // allowed, which is taken to imply cleanup is also allowed.
            if (status.srk_vulnerable_roca) {
              std::move(callback).Run(std::set<Mode>({Mode::kCleanup}));
              return;
            }

            std::move(callback).Run(std::set<Mode>());
          },
          std::move(modes), std::move(completion)),
      timeout);
}

void UpdateAvailable(base::OnceCallback<void(bool)> completion,
                     base::TimeDelta timeout) {
  // Verify if we have updates pending.
  AvailabilityChecker::Start(
      base::BindOnce(
          [](base::OnceCallback<void(bool)> completion,
             const AvailabilityChecker::Status& status) {
            std::move(completion).Run(status.update_available);
          },
          std::move(completion)),
      timeout);
}

}  // namespace tpm_firmware_update
}  // namespace ash