chromium/chrome/browser/os_crypt/app_bound_encryption_win.cc

// Copyright 2022 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/os_crypt/app_bound_encryption_win.h"

#include <objbase.h>

#include <windows.h>

#include <userenv.h>
#include <wrl/client.h>

#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/win/com_init_util.h"
#include "base/win/scoped_bstr.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_paths_internal.h"
#include "chrome/common/pref_names.h"
#include "chrome/elevation_service/elevation_service_idl.h"
#include "chrome/install_static/install_util.h"
#include "components/prefs/pref_service.h"
#include "components/sync/base/pref_names.h"

namespace os_crypt {

namespace {
bool g_non_standard_user_data_dir_supported_for_testing = false;
}

SupportLevel GetAppBoundEncryptionSupportLevel(PrefService* local_state) {
  // Must be a system install.
  if (!install_static::IsSystemInstall()) {
    return SupportLevel::kNotSystemLevel;
  }

  // Policy allows disabling App-Bound encryption. Note, this will not disable
  // decryption of existing data.
  if (local_state->HasPrefPath(prefs::kApplicationBoundEncryptionEnabled) &&
      local_state->IsManagedPreference(
          prefs::kApplicationBoundEncryptionEnabled) &&
      !local_state->GetBoolean(prefs::kApplicationBoundEncryptionEnabled)) {
    return SupportLevel::kDisabledByPolicy;
  }

  // Note: this must be pulled from pref rather than calling
  // `IsLocalSyncEnabled` to ensure that it's managed.
  if (local_state->IsManagedPreference(
          syncer::prefs::kEnableLocalSyncBackend) &&
      local_state->GetBoolean(syncer::prefs::kEnableLocalSyncBackend)) {
    return SupportLevel::kDisabledByRoamingChromeProfile;
  }

  DWORD profile_type;
  if (::GetProfileType(&profile_type)) {
    // App-Bound binds the encryption key to the SYSTEM DPAPI key, which does
    // not roam with a roaming profile.
    if (profile_type > 0) {
      return SupportLevel::kDisabledByRoamingWindowsProfile;
    }
  }

  base::FilePath user_data_dir;
  if (!base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)) {
    return SupportLevel::kApiFailed;
  }

  // If the user data dir is on a network drive, then maybe it is shared between
  // multiple machines, which is unsupported since App-Bound more strongly binds
  // data to the local machine.
  if (user_data_dir.IsNetwork()) {
    return SupportLevel::kUserDataDirNotLocalDisk;
  }

  base::FilePath default_user_data_dir;
  if (!chrome::GetDefaultUserDataDirectory(&default_user_data_dir)) {
    return SupportLevel::kApiFailed;
  }

  // Overridden by policy or by a command line option. This might mean that the
  // user data dir could move in future, so disable App-Bound as a matter of
  // caution.
  if (user_data_dir != default_user_data_dir &&
      !g_non_standard_user_data_dir_supported_for_testing) {
    return SupportLevel::kNotUsingDefaultUserDataDir;
  }

  std::string image_path(MAX_PATH, L'\0');
  DWORD path_length = image_path.size();
  BOOL success =
      ::QueryFullProcessImageNameA(::GetCurrentProcess(), PROCESS_NAME_NATIVE,
                                   image_path.data(), &path_length);
  if (!success && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
    // Process name is potentially greater than MAX_PATH, try larger max size.
    // https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation
    image_path.resize(UNICODE_STRING_MAX_CHARS);
    path_length = image_path.size();
    success =
        ::QueryFullProcessImageNameA(::GetCurrentProcess(), PROCESS_NAME_NATIVE,
                                     image_path.data(), &path_length);
  }
  if (!success) {
    return SupportLevel::kApiFailed;
  }
  image_path.resize(path_length);

  // Must be running on a local disk.
  if (!base::StartsWith(image_path, "\\Device\\HarddiskVolume",
                        base::CompareCase::INSENSITIVE_ASCII)) {
    return SupportLevel::kNotLocalDisk;
  }

  return SupportLevel::kSupported;
}

HRESULT EncryptAppBoundString(ProtectionLevel protection_level,
                              const std::string& plaintext,
                              std::string& ciphertext,
                              DWORD& last_error) {
  base::win::AssertComInitialized();
  Microsoft::WRL::ComPtr<IElevator> elevator;
  last_error = ERROR_GEN_FAILURE;
  HRESULT hr = ::CoCreateInstance(
      install_static::GetElevatorClsid(), nullptr, CLSCTX_LOCAL_SERVER,
      install_static::GetElevatorIid(), IID_PPV_ARGS_Helper(&elevator));

  if (FAILED(hr))
    return hr;

  hr = ::CoSetProxyBlanket(
      elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
  if (FAILED(hr))
    return hr;

  base::win::ScopedBstr plaintext_data;
  ::memcpy(plaintext_data.AllocateBytes(plaintext.length()), plaintext.data(),
           plaintext.length());

  base::win::ScopedBstr encrypted_data;
  hr = elevator->EncryptData(protection_level, plaintext_data.Get(),
                             encrypted_data.Receive(), &last_error);
  if (FAILED(hr))
    return hr;

  ciphertext.assign(
      reinterpret_cast<std::string::value_type*>(encrypted_data.Get()),
      encrypted_data.ByteLength());

  last_error = ERROR_SUCCESS;
  return S_OK;
}

HRESULT DecryptAppBoundString(const std::string& ciphertext,
                              std::string& plaintext,
                              DWORD& last_error,
                              std::string* log_message) {
  DCHECK(!ciphertext.empty());
  base::win::AssertComInitialized();
  Microsoft::WRL::ComPtr<IElevator> elevator;
  last_error = ERROR_GEN_FAILURE;
  HRESULT hr = ::CoCreateInstance(
      install_static::GetElevatorClsid(), nullptr, CLSCTX_LOCAL_SERVER,
      install_static::GetElevatorIid(), IID_PPV_ARGS_Helper(&elevator));

  if (FAILED(hr))
    return hr;

  hr = ::CoSetProxyBlanket(
      elevator.Get(), RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
      COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
      RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_DYNAMIC_CLOAKING);
  if (FAILED(hr))
    return hr;

  base::win::ScopedBstr ciphertext_data;
  ::memcpy(ciphertext_data.AllocateBytes(ciphertext.length()),
           ciphertext.data(), ciphertext.length());

  base::win::ScopedBstr plaintext_data;
  hr = elevator->DecryptData(ciphertext_data.Get(), plaintext_data.Receive(),
                             &last_error);
  if (FAILED(hr)) {
    if (log_message) {
      log_message->assign(
          reinterpret_cast<std::string::value_type*>(plaintext_data.Get()),
          plaintext_data.ByteLength());
    }
    return hr;
  }

  plaintext.assign(
      reinterpret_cast<std::string::value_type*>(plaintext_data.Get()),
      plaintext_data.ByteLength());

  ::SecureZeroMemory(plaintext_data.Get(), plaintext_data.ByteLength());

  last_error = ERROR_SUCCESS;
  return S_OK;
}

void SetNonStandardUserDataDirSupportedForTesting(bool supported) {
  g_non_standard_user_data_dir_supported_for_testing = supported;
}

}  // namespace os_crypt