chromium/chrome/credential_provider/gaiacp/gem_device_details_manager.cc

// Copyright 2019 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/credential_provider/gaiacp/gem_device_details_manager.h"

#include <windows.h>

#include <lm.h>  // Needed for LSA_UNICODE_STRING
#include <process.h>
#include <winternl.h>

#define _NTDEF_  // Prevent redefition errors, must come after <winternl.h>
#include <ntsecapi.h>  // For POLICY_ALL_ACCESS types

#include <algorithm>
#include <memory>

#include "base/containers/span.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/gcpw_strings.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/os_user_manager.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
#include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h"

namespace credential_provider {

const base::TimeDelta
    GemDeviceDetailsManager::kDefaultUploadDeviceDetailsRequestTimeout =
        base::Milliseconds(12000);

namespace {

// Constants used for contacting the gem service.
const char kGemServiceUploadDeviceDetailsPath[] = "/v1/uploadDeviceDetails";
const char kUploadDeviceDetailsRequestSerialNumberParameterName[] =
    "device_serial_number";
const char kUploadDeviceDetailsRequestMachineGuidParameterName[] =
    "machine_guid";
const char kUploadDeviceDetailsRequestDeviceResourceIdParameterName[] =
    "device_resource_id";
const char kUploadDeviceDetailsRequestUserSidParameterName[] = "user_sid";
const char kUploadDeviceDetailsRequestUsernameParameterName[] =
    "account_username";
const char kUploadDeviceDetailsRequestDomainParameterName[] = "device_domain";
const char kIsAdJoinedUserParameterName[] = "is_ad_joined_user";
const char kMacAddressParameterName[] = "wlan_mac_addr";
const char kUploadDeviceDetailsResponseDeviceResourceIdParameterName[] =
    "deviceResourceId";
const char kOsVersion[] = "os_edition";
const char kBuiltInAdminNameParameterName[] = "built_in_admin_name";
const char kAdminGroupNameParameterName[] = "admin_group_name";
const char kDmToken[] = "dm_token";
const char kObfuscatedGaiaId[] = "obfuscated_gaia_id";

// Registry key to control whether upload device details from ESA feature is
// enabled.
const wchar_t kUploadDeviceDetailsFromEsaEnabledRegKey[] =
    L"upload_device_details_from_esa";

// The period of uploading device details to the backend.
const base::TimeDelta kUploadDeviceDetailsExecutionPeriod = base::Hours(3);

// True when upload device details from ESA feature  is enabled.
bool g_upload_device_details_from_esa_enabled = false;

// Maximum number of retries if a HTTP call to the backend fails.
constexpr unsigned int kMaxNumHttpRetries = 3;

// Defines a task that is called by the ESA to upload device details.
class UploadDeviceDetailsTask : public extension::Task {
 public:
  static std::unique_ptr<extension::Task> Create() {
    std::unique_ptr<extension::Task> esa_task(new UploadDeviceDetailsTask());
    return esa_task;
  }

  // ESA calls this to retrieve a configuration for the task execution. Return
  // 3 hours period for uploading device details.
  extension::Config GetConfig() final {
    extension::Config config;
    config.execution_period = kUploadDeviceDetailsExecutionPeriod;
    return config;
  }

  // ESA calls this to set all the user-device contexts for the execution of the
  // task.
  HRESULT SetContext(const std::vector<extension::UserDeviceContext>& c) final {
    context_ = c;
    return S_OK;
  }

  // ESA calls execute function to perform the actual task.
  HRESULT Execute() final {
    HRESULT task_status = S_OK;
    for (const auto& c : context_) {
      HRESULT hr = GemDeviceDetailsManager::Get()->UploadDeviceDetails(c);
      if (FAILED(hr)) {
        LOGFN(ERROR) << "Failed uploading device details for " << c.user_sid
                     << ". hr=" << putHR(hr);
        task_status = hr;
      }
    }
    return task_status;
  }

 private:
  std::vector<extension::UserDeviceContext> context_;
};
}  // namespace

// static
GemDeviceDetailsManager* GemDeviceDetailsManager::Get() {
  return *GetInstanceStorage();
}

// static
GemDeviceDetailsManager** GemDeviceDetailsManager::GetInstanceStorage() {
  static GemDeviceDetailsManager instance(
      kDefaultUploadDeviceDetailsRequestTimeout);
  static GemDeviceDetailsManager* instance_storage = &instance;
  return &instance_storage;
}

// static
extension::TaskCreator
GemDeviceDetailsManager::UploadDeviceDetailsTaskCreator() {
  return base::BindRepeating(&UploadDeviceDetailsTask::Create);
}

GemDeviceDetailsManager::GemDeviceDetailsManager(
    base::TimeDelta upload_device_details_request_timeout)
    : upload_device_details_request_timeout_(
          upload_device_details_request_timeout) {
  g_upload_device_details_from_esa_enabled =
      GetGlobalFlagOrDefault(kUploadDeviceDetailsFromEsaEnabledRegKey, 1) == 1;
}

GemDeviceDetailsManager::~GemDeviceDetailsManager() = default;

GURL GemDeviceDetailsManager::GetGemServiceUploadDeviceDetailsUrl() {
  GURL gem_service_url = GetGcpwServiceUrl();
  return gem_service_url.Resolve(kGemServiceUploadDeviceDetailsPath);
}

bool GemDeviceDetailsManager::UploadDeviceDetailsFromEsaFeatureEnabled() const {
  return g_upload_device_details_from_esa_enabled;
}

// Uploads the device details into GEM database using |dm_token|
// for authentication and authorization. The GEM service would use
// |serial_number| and |machine_guid| for identifying the device
// entry in GEM database.
HRESULT GemDeviceDetailsManager::UploadDeviceDetails(
    const extension::UserDeviceContext& context) {
  std::wstring obfuscated_user_id;
  HRESULT status = GetIdFromSid(context.user_sid.c_str(), &obfuscated_user_id);
  if (FAILED(status)) {
    LOGFN(ERROR) << "Could not get user id from sid " << context.user_sid;
    return status;
  }

  wchar_t found_username[kWindowsUsernameBufferLength] = {};
  wchar_t found_domain[kWindowsDomainBufferLength] = {};

  status = OSUserManager::Get()->FindUserBySidWithFallback(
      context.user_sid.c_str(), found_username, std::size(found_username),
      found_domain, std::size(found_domain));
  if (FAILED(status)) {
    LOGFN(ERROR) << "Could not get username and domain from sid "
                 << context.user_sid;
  }

  return UploadDeviceDetailsInternal(
      /* access_token= */ std::string(), obfuscated_user_id, context.dm_token,
      context.user_sid, context.device_resource_id, found_username,
      found_domain);
}

// Uploads the device details into GEM database using |access_token|
// for authentication and authorization. The GEM service would use
// |serial_number| and |machine_guid| for identifying the device
// entry in GEM database.
HRESULT GemDeviceDetailsManager::UploadDeviceDetails(
    const std::string& access_token,
    const std::wstring& sid,
    const std::wstring& username,
    const std::wstring& domain) {
  return UploadDeviceDetailsInternal(access_token,
                                     /* obfuscated_user_id= */ L"",
                                     /* dm_token= */ L"", sid,
                                     /* device_resource_id= */ L"", username,
                                     domain);
}

HRESULT GemDeviceDetailsManager::UploadDeviceDetailsInternal(
    const std::string access_token,
    const std::wstring obfuscated_user_id,
    const std::wstring dm_token,
    const std::wstring sid,
    const std::wstring device_resource_id,
    const std::wstring username,
    const std::wstring domain) {
  std::wstring serial_number = GetSerialNumber();
  std::wstring machine_guid;
  HRESULT hr = GetMachineGuid(&machine_guid);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "Failed fetching machine guid. hr=" << putHR(hr);
    return hr;
  }
  std::vector<std::string> mac_addresses = GetMacAddresses();

  // Get OS version of the windows device.
  std::string version;
  GetOsVersion(&version);

  // Extract built-in administrator and administrator group name
  // in device locale.
  std::wstring admin_group_name = L"";
  hr = LookupLocalizedNameForWellKnownSid(WinBuiltinAdministratorsSid,
                                          &admin_group_name);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "LookupLocalizedNameForWellKnownSid  hr=" << putHR(hr);
    hr = S_OK;
  }

  std::wstring built_in_admin_name = L"";
  hr = GetLocalizedNameBuiltinAdministratorAccount(&built_in_admin_name);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "GetLocalizedNameBuiltinAdministratorAccount  hr="
                 << putHR(hr);
    hr = S_OK;
  }

  base::Value::List mac_address_value_list;
  for (const std::string& mac_address : mac_addresses)
    mac_address_value_list.Append(mac_address);

  std::wstring dm_token_value = dm_token;
  if (dm_token_value.empty()) {
    hr = GetGCPWDmToken(sid, &dm_token_value);
    if (FAILED(hr)) {
      LOGFN(WARNING) << "Failed to fetch DmToken hr=" << putHR(hr);
      hr = S_OK;
    }
  }

  request_dict_ = std::make_unique<base::Value::Dict>();
  request_dict_->Set(kUploadDeviceDetailsRequestSerialNumberParameterName,
                     base::WideToUTF8(serial_number));
  request_dict_->Set(kUploadDeviceDetailsRequestMachineGuidParameterName,
                     base::WideToUTF8(machine_guid));
  request_dict_->Set(kUploadDeviceDetailsRequestUserSidParameterName,
                     base::WideToUTF8(sid));

  if (!username.empty()) {
    request_dict_->Set(kUploadDeviceDetailsRequestUsernameParameterName,
                       base::WideToUTF8(username));
  }

  if (!domain.empty()) {
    request_dict_->Set(kUploadDeviceDetailsRequestDomainParameterName,
                       base::WideToUTF8(domain));
  }

  request_dict_->Set(kIsAdJoinedUserParameterName,
                     OSUserManager::Get()->IsUserDomainJoined(sid));
  request_dict_->Set(kMacAddressParameterName,
                     std::move(mac_address_value_list));
  request_dict_->Set(kOsVersion, version);
  request_dict_->Set(kBuiltInAdminNameParameterName,
                     base::WideToUTF8(built_in_admin_name));
  request_dict_->Set(kAdminGroupNameParameterName,
                     base::WideToUTF8(admin_group_name));
  request_dict_->Set(kDmToken, base::WideToUTF8(dm_token_value));

  if (!obfuscated_user_id.empty()) {
    request_dict_->Set(kObfuscatedGaiaId, base::WideToUTF8(obfuscated_user_id));
  }

  std::wstring known_resource_id = device_resource_id.empty()
                                       ? GetUserDeviceResourceId(sid)
                                       : device_resource_id;
  if (!known_resource_id.empty()) {
    request_dict_->Set(kUploadDeviceDetailsRequestDeviceResourceIdParameterName,
                       base::WideToUTF8(known_resource_id));
  }

  std::optional<base::Value> request_result;

  hr = WinHttpUrlFetcher::BuildRequestAndFetchResultFromHttpService(
      GemDeviceDetailsManager::Get()->GetGemServiceUploadDeviceDetailsUrl(),
      access_token, {}, *request_dict_, upload_device_details_request_timeout_,
      kMaxNumHttpRetries, &request_result);

  if (FAILED(hr)) {
    LOGFN(ERROR) << "BuildRequestAndFetchResultFromHttpService hr="
                 << putHR(hr);
    return E_FAIL;
  }

  auto* resource_id = request_result->GetDict().FindString(
      kUploadDeviceDetailsResponseDeviceResourceIdParameterName);
  if (resource_id) {
    hr = SetUserProperty(sid, kRegUserDeviceResourceId,
                         base::UTF8ToWide(*resource_id));
  } else {
    LOGFN(ERROR) << "Server response does not contain "
                 << kUploadDeviceDetailsResponseDeviceResourceIdParameterName;
    hr = E_FAIL;
  }

  return hr;
}

void GemDeviceDetailsManager::
    SetUploadDeviceDetailsFromEsaFeatureEnabledForTesting(bool value) {
  g_upload_device_details_from_esa_enabled = value;
}

}  // namespace credential_provider