chromium/ash/webui/multidevice_debug/proximity_auth_webui_handler.cc

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

#include "ash/webui/multidevice_debug/proximity_auth_webui_handler.h"

#include <algorithm>
#include <memory>
#include <sstream>
#include <utility>

#include "base/base64url.h"
#include "base/functional/bind.h"
#include "base/i18n/time_formatting.h"
#include "base/time/default_clock.h"
#include "base/time/default_tick_clock.h"
#include "base/values.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/multidevice/software_feature_state.h"
#include "chromeos/ash/services/device_sync/proto/enum_util.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_ui.h"
#include "device/bluetooth/public/cpp/bluetooth_uuid.h"

namespace ash {

namespace multidevice {

namespace {

constexpr const multidevice::SoftwareFeature kAllSoftareFeatures[] = {
    multidevice::SoftwareFeature::kBetterTogetherHost,
    multidevice::SoftwareFeature::kBetterTogetherClient,
    multidevice::SoftwareFeature::kSmartLockHost,
    multidevice::SoftwareFeature::kSmartLockClient,
    multidevice::SoftwareFeature::kInstantTetheringHost,
    multidevice::SoftwareFeature::kInstantTetheringClient,
    multidevice::SoftwareFeature::kMessagesForWebHost,
    multidevice::SoftwareFeature::kMessagesForWebClient,
    multidevice::SoftwareFeature::kPhoneHubHost,
    multidevice::SoftwareFeature::kPhoneHubClient,
    multidevice::SoftwareFeature::kWifiSyncHost,
    multidevice::SoftwareFeature::kWifiSyncClient,
    multidevice::SoftwareFeature::kEcheHost,
    multidevice::SoftwareFeature::kEcheClient,
    multidevice::SoftwareFeature::kPhoneHubCameraRollHost,
    multidevice::SoftwareFeature::kPhoneHubCameraRollClient};

// Keys in the JSON representation of a log message.
const char kLogMessageTextKey[] = "text";
const char kLogMessageTimeKey[] = "time";
const char kLogMessageFileKey[] = "file";
const char kLogMessageLineKey[] = "line";
const char kLogMessageSeverityKey[] = "severity";

// Keys in the JSON representation of a SyncState object for enrollment or
// device sync.
const char kSyncStateLastSuccessTime[] = "lastSuccessTime";
const char kSyncStateNextRefreshTime[] = "nextRefreshTime";
const char kSyncStateRecoveringFromFailure[] = "recoveringFromFailure";
const char kSyncStateOperationInProgress[] = "operationInProgress";

// 9999 days in milliseconds.
const double kFakeInfinityMillis = 863913600000;

double ConvertNextAttemptTimeToDouble(base::TimeDelta delta) {
  // If no future attempt is scheduled, the next-attempt time is
  // base::TimeDelta::Max(), which corresponds to an infinite double value. In
  // order to store the next-attempt time as a double base::Value,
  // std::isfinite() must be true. So, here we use 9999 days to represent the
  // max next-attempt time to allow use with base::Value.
  if (delta.is_max())
    return kFakeInfinityMillis;

  return delta.InMillisecondsF();
}

// Converts |log_message| to a raw dictionary value used as a JSON argument to
// JavaScript functions.
base::Value::Dict LogMessageToDictionary(
    const multidevice::LogBuffer::LogMessage& log_message) {
  base::Value::Dict dictionary;
  dictionary.Set(kLogMessageTextKey, log_message.text);
  dictionary.Set(kLogMessageTimeKey,
                 base::TimeFormatTimeOfDayWithMilliseconds(log_message.time));
  dictionary.Set(kLogMessageFileKey, log_message.file);
  dictionary.Set(kLogMessageLineKey, log_message.line);
  dictionary.Set(kLogMessageSeverityKey,
                 static_cast<int>(log_message.severity));
  return dictionary;
}

// Keys in the JSON representation of an ExternalDeviceInfo proto.
const char kExternalDevicePublicKey[] = "publicKey";
const char kExternalDevicePublicKeyTruncated[] = "publicKeyTruncated";
const char kExternalDeviceFriendlyName[] = "friendlyDeviceName";
const char kExternalDeviceNoPiiName[] = "noPiiName";
const char kExternalDeviceUnlockKey[] = "unlockKey";
const char kExternalDeviceMobileHotspot[] = "hasMobileHotspot";
const char kExternalDeviceFeatureStates[] = "featureStates";

// Creates a SyncState JSON object that can be passed to the WebUI.
base::Value::Dict CreateSyncStateDictionary(double last_success_time,
                                            double next_refresh_time,
                                            bool is_recovering_from_failure,
                                            bool is_enrollment_in_progress) {
  base::Value::Dict sync_state;
  sync_state.Set(kSyncStateLastSuccessTime, last_success_time);
  sync_state.Set(kSyncStateNextRefreshTime, next_refresh_time);
  sync_state.Set(kSyncStateRecoveringFromFailure, is_recovering_from_failure);
  sync_state.Set(kSyncStateOperationInProgress, is_enrollment_in_progress);
  return sync_state;
}

std::string GenerateFeaturesString(const multidevice::RemoteDeviceRef& device) {
  std::stringstream ss;
  ss << "{";

  bool logged_feature = false;
  for (const auto& software_feature : kAllSoftareFeatures) {
    multidevice::SoftwareFeatureState state =
        device.GetSoftwareFeatureState(software_feature);

    // Only log features with values.
    if (state == multidevice::SoftwareFeatureState::kNotSupported)
      continue;

    logged_feature = true;
    ss << software_feature << ": " << state << ", ";
  }

  if (logged_feature)
    ss.seekp(-2, ss.cur);  // Remove last ", " from the stream.

  ss << "}";
  return ss.str();
}

}  // namespace

ProximityAuthWebUIHandler::ProximityAuthWebUIHandler(
    device_sync::DeviceSyncClient* device_sync_client)
    : device_sync_client_(device_sync_client),
      web_contents_initialized_(false) {
  CHECK(device_sync_client_);
}

ProximityAuthWebUIHandler::~ProximityAuthWebUIHandler() {
  multidevice::LogBuffer::GetInstance()->RemoveObserver(this);

  device_sync_client_->RemoveObserver(this);
}

void ProximityAuthWebUIHandler::RegisterMessages() {
  web_ui()->RegisterMessageCallback(
      "onWebContentsInitialized",
      base::BindRepeating(&ProximityAuthWebUIHandler::OnWebContentsInitialized,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "clearLogBuffer",
      base::BindRepeating(&ProximityAuthWebUIHandler::ClearLogBuffer,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "getLogMessages",
      base::BindRepeating(&ProximityAuthWebUIHandler::GetLogMessages,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "getLocalState",
      base::BindRepeating(&ProximityAuthWebUIHandler::GetLocalState,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "forceEnrollment",
      base::BindRepeating(&ProximityAuthWebUIHandler::ForceEnrollment,
                          base::Unretained(this)));

  web_ui()->RegisterMessageCallback(
      "forceDeviceSync",
      base::BindRepeating(&ProximityAuthWebUIHandler::ForceDeviceSync,
                          base::Unretained(this)));
}

void ProximityAuthWebUIHandler::OnLogMessageAdded(
    const multidevice::LogBuffer::LogMessage& log_message) {
  web_ui()->CallJavascriptFunctionUnsafe("LogBufferInterface.onLogMessageAdded",
                                         LogMessageToDictionary(log_message));
}

void ProximityAuthWebUIHandler::OnLogBufferCleared() {
  web_ui()->CallJavascriptFunctionUnsafe(
      "LogBufferInterface.onLogBufferCleared");
}

void ProximityAuthWebUIHandler::OnEnrollmentFinished() {
  // OnGetDebugInfo() will call NotifyOnEnrollmentFinished() with the enrollment
  // state info.
  enrollment_update_waiting_for_debug_info_ = true;
  device_sync_client_->GetDebugInfo(
      base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ProximityAuthWebUIHandler::OnNewDevicesSynced() {
  // OnGetDebugInfo() will call NotifyOnSyncFinished() with the device sync
  // state info.
  sync_update_waiting_for_debug_info_ = true;
  device_sync_client_->GetDebugInfo(
      base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ProximityAuthWebUIHandler::OnWebContentsInitialized(
    const base::Value::List& args) {
  if (!web_contents_initialized_) {
    device_sync_client_->AddObserver(this);
    multidevice::LogBuffer::GetInstance()->AddObserver(this);
    web_contents_initialized_ = true;
  }
}

void ProximityAuthWebUIHandler::GetLogMessages(const base::Value::List& args) {
  base::Value::List json_logs;
  for (const auto& log : *multidevice::LogBuffer::GetInstance()->logs()) {
    json_logs.Append(LogMessageToDictionary(log));
  }
  web_ui()->CallJavascriptFunctionUnsafe("LogBufferInterface.onGotLogMessages",
                                         json_logs);
}

void ProximityAuthWebUIHandler::ClearLogBuffer(const base::Value::List& args) {
  // The OnLogBufferCleared() observer function will be called after the buffer
  // is cleared.
  multidevice::LogBuffer::GetInstance()->Clear();
}

void ProximityAuthWebUIHandler::ForceEnrollment(const base::Value::List& args) {
  device_sync_client_->ForceEnrollmentNow(
      base::BindOnce(&ProximityAuthWebUIHandler::OnForceEnrollmentNow,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ProximityAuthWebUIHandler::ForceDeviceSync(const base::Value::List& args) {
  device_sync_client_->ForceSyncNow(
      base::BindOnce(&ProximityAuthWebUIHandler::OnForceSyncNow,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ProximityAuthWebUIHandler::GetLocalState(const base::Value::List& args) {
  // OnGetDebugInfo() will call NotifyGotLocalState() with the enrollment and
  // device sync state info.
  get_local_state_update_waiting_for_debug_info_ = true;
  device_sync_client_->GetDebugInfo(
      base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo,
                     weak_ptr_factory_.GetWeakPtr()));
}

base::Value ProximityAuthWebUIHandler::GetTruncatedLocalDeviceId() {
  std::optional<multidevice::RemoteDeviceRef> local_device_metadata =
      device_sync_client_->GetLocalDeviceMetadata();

  std::string device_id =
      local_device_metadata
          ? local_device_metadata->GetTruncatedDeviceIdForLogs()
          : "Missing Device ID";

  return base::Value(device_id);
}

base::Value::List ProximityAuthWebUIHandler::GetRemoteDevicesList() {
  base::Value::List devices_list_value;

  for (const auto& remote_device : device_sync_client_->GetSyncedDevices()) {
    devices_list_value.Append(RemoteDeviceToDictionary(remote_device));
  }

  return devices_list_value;
}

base::Value::Dict ProximityAuthWebUIHandler::RemoteDeviceToDictionary(
    const multidevice::RemoteDeviceRef& remote_device) {
  // Set the fields in the ExternalDeviceInfo proto.
  base::Value::Dict dictionary;
  dictionary.Set(kExternalDevicePublicKey, remote_device.GetDeviceId());
  dictionary.Set(kExternalDevicePublicKeyTruncated,
                 remote_device.GetTruncatedDeviceIdForLogs());
  dictionary.Set(kExternalDeviceFriendlyName, remote_device.name());
  dictionary.Set(kExternalDeviceNoPiiName, remote_device.pii_free_name());
  dictionary.Set(kExternalDeviceUnlockKey,
                 remote_device.GetSoftwareFeatureState(
                     multidevice::SoftwareFeature::kSmartLockHost) ==
                     multidevice::SoftwareFeatureState::kEnabled);
  dictionary.Set(kExternalDeviceMobileHotspot,
                 remote_device.GetSoftwareFeatureState(
                     multidevice::SoftwareFeature::kInstantTetheringHost) ==
                     multidevice::SoftwareFeatureState::kSupported);
  dictionary.Set(kExternalDeviceFeatureStates,
                 GenerateFeaturesString(remote_device));

  return dictionary;
}

void ProximityAuthWebUIHandler::OnForceEnrollmentNow(bool success) {
  PA_LOG(VERBOSE) << "Force enrollment result: " << success;
}

void ProximityAuthWebUIHandler::OnForceSyncNow(bool success) {
  PA_LOG(VERBOSE) << "Force sync result: " << success;
}

void ProximityAuthWebUIHandler::OnSetSoftwareFeatureState(
    const std::string public_key,
    device_sync::mojom::NetworkRequestResult result_code) {
  std::string device_id = RemoteDevice::GenerateDeviceId(public_key);

  if (result_code == device_sync::mojom::NetworkRequestResult::kSuccess) {
    PA_LOG(VERBOSE) << "Successfully set SoftwareFeature state for device: "
                    << device_id;
  } else {
    PA_LOG(ERROR) << "Failed to set SoftwareFeature state for device: "
                  << device_id << ", error code: " << result_code;
  }
}

void ProximityAuthWebUIHandler::OnGetDebugInfo(
    device_sync::mojom::DebugInfoPtr debug_info_ptr) {
  // If enrollment is not yet complete, no debug information is available.
  if (!debug_info_ptr)
    return;

  if (enrollment_update_waiting_for_debug_info_) {
    enrollment_update_waiting_for_debug_info_ = false;
    NotifyOnEnrollmentFinished(
        true /* success */,
        CreateSyncStateDictionary(
            debug_info_ptr->last_enrollment_time
                .InMillisecondsFSinceUnixEpoch(),
            ConvertNextAttemptTimeToDouble(
                debug_info_ptr->time_to_next_enrollment_attempt),
            debug_info_ptr->is_recovering_from_enrollment_failure,
            debug_info_ptr->is_enrollment_in_progress));
  }

  if (sync_update_waiting_for_debug_info_) {
    sync_update_waiting_for_debug_info_ = false;
    NotifyOnSyncFinished(
        true /* was_sync_successful */, true /* changed */,
        CreateSyncStateDictionary(
            debug_info_ptr->last_sync_time.InMillisecondsFSinceUnixEpoch(),
            ConvertNextAttemptTimeToDouble(
                debug_info_ptr->time_to_next_sync_attempt),
            debug_info_ptr->is_recovering_from_sync_failure,
            debug_info_ptr->is_sync_in_progress));
  }

  if (get_local_state_update_waiting_for_debug_info_) {
    get_local_state_update_waiting_for_debug_info_ = false;
    NotifyGotLocalState(
        GetTruncatedLocalDeviceId(),
        CreateSyncStateDictionary(
            debug_info_ptr->last_enrollment_time
                .InMillisecondsFSinceUnixEpoch(),
            ConvertNextAttemptTimeToDouble(
                debug_info_ptr->time_to_next_enrollment_attempt),
            debug_info_ptr->is_recovering_from_enrollment_failure,
            debug_info_ptr->is_enrollment_in_progress),
        CreateSyncStateDictionary(
            debug_info_ptr->last_sync_time.InMillisecondsFSinceUnixEpoch(),
            ConvertNextAttemptTimeToDouble(
                debug_info_ptr->time_to_next_sync_attempt),
            debug_info_ptr->is_recovering_from_sync_failure,
            debug_info_ptr->is_sync_in_progress),
        GetRemoteDevicesList());
  }
}

void ProximityAuthWebUIHandler::NotifyOnEnrollmentFinished(
    bool success,
    base::Value::Dict enrollment_state) {
  PA_LOG(VERBOSE) << "Enrollment attempt completed with success=" << success
                  << ":\n"
                  << enrollment_state;
  web_ui()->CallJavascriptFunctionUnsafe(
      "LocalStateInterface.onEnrollmentStateChanged", enrollment_state);
}

void ProximityAuthWebUIHandler::NotifyOnSyncFinished(
    bool was_sync_successful,
    bool changed,
    base::Value::Dict device_sync_state) {
  PA_LOG(VERBOSE) << "Device sync completed with result=" << was_sync_successful
                  << ":\n"
                  << device_sync_state;
  web_ui()->CallJavascriptFunctionUnsafe(
      "LocalStateInterface.onDeviceSyncStateChanged", device_sync_state);

  if (changed) {
    base::Value::List synced_devices = GetRemoteDevicesList();
    PA_LOG(VERBOSE) << "New unlock keys obtained after device sync:\n"
                    << synced_devices;
    web_ui()->CallJavascriptFunctionUnsafe(
        "LocalStateInterface.onRemoteDevicesChanged", synced_devices);
  }
}

void ProximityAuthWebUIHandler::NotifyGotLocalState(
    base::Value truncated_local_device_id,
    base::Value::Dict enrollment_state,
    base::Value::Dict device_sync_state,
    base::Value::List synced_devices) {
  PA_LOG(VERBOSE) << "==== Got Local State ====\n"
                  << "Device ID (truncated): " << truncated_local_device_id
                  << "\nEnrollment State: \n"
                  << enrollment_state << "Device Sync State: \n"
                  << device_sync_state << "Synced devices: \n"
                  << synced_devices;
  web_ui()->CallJavascriptFunctionUnsafe(
      "LocalStateInterface.onGotLocalState", truncated_local_device_id,
      enrollment_state, device_sync_state, synced_devices);
}

}  // namespace multidevice

}  // namespace ash