chromium/chrome/credential_provider/setup/setup_lib.cc

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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/credential_provider/setup/setup_lib.h"

#include <shlobj.h>

#include <iomanip>
#include <string>

#include "base/command_line.h"
#include "base/file_version_info.h"
#include "base/files/file.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/scoped_native_library.h"
#include "base/strings/string_number_conversions.h"
#include "base/win/atl.h"
#include "base/win/registry.h"
#include "base/win/scoped_handle.h"
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/extension/extension_strings.h"
#include "chrome/credential_provider/extension/extension_utils.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
#include "chrome/credential_provider/setup/gcpw_files.h"
#include "chrome/credential_provider/setup/setup_utils.h"
#include "chrome/installer/util/delete_after_reboot_helper.h"

namespace credential_provider {

namespace {

// Creates the directory where GCP is to be installed.
base::FilePath CreateInstallDirectory() {
  base::FilePath dest_path = GetInstallDirectory();

  if (dest_path.empty()) {
    return dest_path;
  }

  if (!base::CreateDirectory(dest_path)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "base::CreateDirectory hr=" << putHR(hr);
    return base::FilePath();
  }

  return dest_path;
}

// Copies the files specified in |names| from |src_path| to |dest_path| creating
// any intermedidate subdirectories of |dest_path| as needed.  Both |src_path|
// and |dest_path| are full paths.  |names| are paths relative to |src_path|
// and are copied to the same relative path under |dest_path|.
HRESULT InstallFiles(const base::FilePath& src_path,
                     const base::FilePath& dest_path,
                     const base::FilePath::StringType names[],
                     size_t length) {
  for (size_t i = 0; i < length; ++i) {
    base::FilePath src = src_path.Append(names[i]);
    base::FilePath dest = dest_path.Append(names[i]);

    // Make sure parent of destination file exists.
    if (!base::CreateDirectory(dest.DirName())) {
      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "CreateDirectory hr=" << putHR(hr)
                   << " name=" << names[i];
      return hr;
    }

    if (!base::CopyFile(src, dest)) {
      HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
      LOGFN(ERROR) << "CopyFile hr=" << putHR(hr) << " name=" << names[i];
      return hr;
    }
    LOGFN(INFO) << "Installed name=" << names[i];
  }

  return S_OK;
}

// Registers the named DLLs by calling the DllRegisterServer entrypoint.  The
// DLLs are specified with the |names| argument which are paths relative to
// |dest_path|.  |fakes| is non-null during unit tests to install fakes into
// the loaded DLL.
HRESULT RegisterDlls(const base::FilePath& dest_path,
                     const base::FilePath::StringType names[],
                     size_t length,
                     FakesForTesting* fakes) {
  bool has_failures = false;

  for (size_t i = 0; i < length; ++i) {
    base::ScopedNativeLibrary library(dest_path.Append(names[i]));

    if (fakes) {
      SetFakesForTestingFn set_fakes_for_testing_fn =
          reinterpret_cast<SetFakesForTestingFn>(
              library.GetFunctionPointer("SetFakesForTesting"));
      if (set_fakes_for_testing_fn)
        (*set_fakes_for_testing_fn)(fakes);
    }

    FARPROC register_server_fn = reinterpret_cast<FARPROC>(
        library.GetFunctionPointer("DllRegisterServer"));
    HRESULT hr = S_OK;

    if (register_server_fn) {
      hr = static_cast<HRESULT>((*register_server_fn)());
      LOGFN(VERBOSE) << "Registered name=" << names[i] << " hr=" << putHR(hr);
    } else {
      LOGFN(ERROR) << "Failed to register name=" << names[i];
      hr = E_NOTIMPL;
    }
    has_failures |= FAILED(hr);
  }

  return has_failures ? E_UNEXPECTED : S_OK;
}

// Unregisters the named DLLs by calling the DllUneegisterServer entrypoint.
// The DLLs are specified with the |names| argument which are paths relative to
// |dest_path|.  |fakes| is non-null during unit tests to install fakes into
// the loaded DLL.
HRESULT UnregisterDlls(const base::FilePath& dest_path,
                       const base::FilePath::StringType names[],
                       size_t length,
                       FakesForTesting* fakes) {
  bool has_failures = false;

  for (size_t i = 0; i < length; ++i) {
    base::ScopedNativeLibrary library(dest_path.Append(names[i]));

    if (fakes) {
      SetFakesForTestingFn pmfn = reinterpret_cast<SetFakesForTestingFn>(
          library.GetFunctionPointer("SetFakesForTesting"));
      if (pmfn)
        (*pmfn)(fakes);
    }

    FARPROC pfn = reinterpret_cast<FARPROC>(
        library.GetFunctionPointer("DllUnregisterServer"));
    HRESULT hr = pfn ? static_cast<HRESULT>((*pfn)()) : E_UNEXPECTED;
    LOGFN(VERBOSE) << "Unregistered name=" << names[i] << " hr=" << putHR(hr);
    has_failures |= FAILED(hr);
  }

  return has_failures ? E_UNEXPECTED : S_OK;
}

}  // namespace

HRESULT DoInstall(const base::FilePath& installer_path,
                  const std::wstring& product_version,
                  FakesForTesting* fakes) {
  const base::FilePath gcp_path = CreateInstallDirectory();
  if (gcp_path.empty())
    return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND);

  base::FilePath dest_path = gcp_path.Append(product_version);
  LOGFN(VERBOSE) << "Install to: " << dest_path;

  // Make sure nothing under the destination directory is pending delete
  // after reboot, so that files installed now won't get deleted later.
  if (!RemoveFromMovesPendingReboot(dest_path)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "RemoveFromMovesPendingReboot hr=" << putHR(hr);
    return hr;
  }

  base::FilePath src_path = installer_path.DirName();
  auto install_files =
      credential_provider::GCPWFiles::Get()->GetEffectiveInstallFiles();
  HRESULT hr = InstallFiles(src_path, dest_path, install_files.data(),
                            install_files.size());
  if (FAILED(hr))
    return hr;

  auto register_dlls =
      credential_provider::GCPWFiles::Get()->GetRegistrationFiles();
  hr = RegisterDlls(dest_path, register_dlls.data(), register_dlls.size(),
                    fakes);
  if (FAILED(hr))
    return hr;

  // If all is good, try to delete all other versions on best effort basis.
  if (SUCCEEDED(hr))
    DeleteVersionsExcept(gcp_path, product_version);

  base::FilePath setup_exe_path = dest_path.Append(kCredentialProviderSetupExe);
  hr = WriteUninstallRegistryValues(setup_exe_path);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "WriteUninstallRegistryValues failed hr=" << putHR(hr);
    // Uninstall registry values are written for MSI wrapper. Failing to write
    // them will only impact uninstalling through uninstall shortcuts on
    // Windows. There is still a workaround to uninstall by calling
    // "gcp_setup.exe --uninstall" from a terminal. So, ignoring the failure in
    // this case until we support rollback of installation that fails mid-way
    // through.
  }

  hr = WriteCredentialProviderRegistryValues(dest_path);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "WriteCredentialProviderRegistryValues failed hr="
                 << putHR(hr);
  }

  if (extension::IsGCPWExtensionEnabled()) {
    DWORD error_code = extension::InstallGCPWExtension(
        dest_path.Append(kCredentialProviderExtensionExe));
    if (error_code != ERROR_SUCCESS) {
      LOGFN(ERROR) << "InstallGCPWExtension failed win32=" << error_code;
      return HRESULT_FROM_WIN32(error_code);
    }
  }

  return S_OK;
}

HRESULT DoUninstall(const base::FilePath& installer_path,
                    const base::FilePath& dest_path,
                    FakesForTesting* fakes) {
  bool has_failures = false;

  auto register_dlls =
      credential_provider::GCPWFiles::Get()->GetRegistrationFiles();
  // Do all actions best effort and keep going.
  has_failures |= FAILED(UnregisterDlls(dest_path, register_dlls.data(),
                                        register_dlls.size(), fakes));

  // If the DLLs are unregistered, Credential Provider will not be loaded by
  // Winlogon. Therefore, it is safe to delete the startup sentinel file at this
  // time.
  if (!has_failures)
    DeleteStartupSentinel();

  has_failures |=
      FAILED(HRESULT_FROM_WIN32(extension::UninstallGCPWExtension()));

  // Delete all files in the destination directory.  This directory does not
  // contain any configuration files or anything else user generated.
  if (!base::DeletePathRecursively(dest_path)) {
    has_failures = true;
    ScheduleDirectoryForDeletion(dest_path);
  }

  // |dest_path| is of the form %ProgramFile%\Google\GCP\VERSION.  Now try to
  // delete the parent directory if possible.
  if (base::IsDirectoryEmpty(dest_path.DirName()))
    has_failures |= !base::DeleteFile(dest_path.DirName());

  StandaloneInstallerConfigurator* installer_config =
      StandaloneInstallerConfigurator::Get();
  if (installer_config->IsStandaloneInstallation()) {
    has_failures |= FAILED(HRESULT_FROM_WIN32(
        StandaloneInstallerConfigurator::Get()->RemoveUninstallKey()));
  }

  // TODO(rogerta): ask user to reboot if anything went wrong during uninstall.

  return has_failures ? E_UNEXPECTED : S_OK;
}

HRESULT RelaunchUninstaller(const base::FilePath& installer_path) {
  base::FilePath temp_path;
  if (!base::CreateNewTempDirectory(FILE_PATH_LITERAL("gcp"), &temp_path)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "CreateNewTempDirectory hr=" << putHR(hr);
    return hr;
  }

  base::FilePath new_installer_path =
      temp_path.Append(installer_path.BaseName());

  if (!base::CopyFile(installer_path, new_installer_path)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "Could not copy uninstaller hr=" << putHR(hr) << " '"
                 << installer_path << "' --> '" << new_installer_path << "'";
    return hr;
  }

  base::win::ScopedHandle::Handle this_process_handle_handle;
  if (!::DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
                         GetCurrentProcess(), &this_process_handle_handle, 0,
                         TRUE,  // Inheritable.
                         DUPLICATE_SAME_ACCESS)) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "Error duplicating parent process handle.";
    return hr;
  }
  base::win::ScopedHandle this_process_handle(this_process_handle_handle);

  base::CommandLine cmdline(new_installer_path);
  cmdline.AppendSwitch(switches::kUninstall);
  cmdline.AppendSwitchPath(switches::kInstallPath, installer_path.DirName());
  cmdline.AppendSwitchNative(switches::kParentHandle,
                             base::NumberToWString(base::win::HandleToUint32(
                                 this_process_handle_handle)));

  LOGFN(VERBOSE) << "Cmd: " << cmdline.GetCommandLineString();

  base::LaunchOptions options;
  options.handles_to_inherit.push_back(this_process_handle_handle);
  options.current_directory = temp_path;
  base::Process process(base::LaunchProcess(cmdline, options));

  return process.IsValid() ? S_OK : E_FAIL;
}

int EnableStatsCollection(const base::CommandLine& cmdline) {
  DCHECK(cmdline.HasSwitch(switches::kEnableStats) ||
         cmdline.HasSwitch(switches::kDisableStats));

  bool enable = !cmdline.HasSwitch(switches::kDisableStats);

  base::win::RegKey key;
  LONG status = key.Create(HKEY_LOCAL_MACHINE, kRegUpdaterClientStateAppPath,
                           KEY_SET_VALUE | KEY_WOW64_32KEY);
  if (status != ERROR_SUCCESS) {
    LOGFN(ERROR) << "Unable to open omaha key=" << kRegUpdaterClientStateAppPath
                 << " status=" << status;
  } else {
    status = key.WriteValue(kRegUsageStatsName, enable ? 1 : 0);
    if (status != ERROR_SUCCESS) {
      LOGFN(ERROR) << "Unable to write " << kRegUsageStatsName
                   << " value status=" << status;
    }
  }

  return status == ERROR_SUCCESS ? 0 : -1;
}

HRESULT WriteUninstallRegistryValues(const base::FilePath& setup_exe) {
  base::win::RegKey key;
  LONG status = key.Create(HKEY_LOCAL_MACHINE, kRegUpdaterClientStateAppPath,
                           KEY_SET_VALUE | KEY_WOW64_32KEY);
  if (status != ERROR_SUCCESS) {
    HRESULT hr = HRESULT_FROM_WIN32(status);
    LOGFN(ERROR) << "Unable to open " << kRegUpdaterClientStateAppPath
                 << " hr=" << putHR(hr);
    return hr;
  } else {
    status =
        key.WriteValue(kRegUninstallStringField, setup_exe.value().c_str());
    if (status != ERROR_SUCCESS) {
      HRESULT hr = HRESULT_FROM_WIN32(status);
      LOGFN(ERROR) << "Unable to write " << kRegUninstallStringField
                   << " hr=" << putHR(hr);
      return hr;
    }

    base::CommandLine uninstall_arguments(base::CommandLine::NO_PROGRAM);
    uninstall_arguments.AppendSwitch(switches::kUninstall);

    status = key.WriteValue(kRegUninstallArgumentsField,
                            uninstall_arguments.GetCommandLineString().c_str());
    if (status != ERROR_SUCCESS) {
      HRESULT hr = HRESULT_FROM_WIN32(status);
      LOGFN(ERROR) << "Unable to write " << kRegUninstallArgumentsField
                   << " hr=" << putHR(hr);
      return hr;
    }
  }

  return HRESULT_FROM_WIN32(status);
}

HRESULT WriteCredentialProviderRegistryValues(
    const base::FilePath& install_path) {
  HRESULT hr =
      StandaloneInstallerConfigurator::Get()->AddUninstallKey(install_path);
  if (FAILED(hr)) {
    LOGFN(ERROR) << "AddUninstallKey  hr=" << putHR(hr);
    return hr;
  }

  base::win::RegKey key;
  LONG status = key.Create(HKEY_LOCAL_MACHINE, kGcpRootKeyName, KEY_SET_VALUE);
  if (status != ERROR_SUCCESS) {
    hr = HRESULT_FROM_WIN32(status);
    LOGFN(ERROR) << "Unable to create " << kGcpRootKeyName
                 << " hr=" << putHR(hr);
    return hr;
  }

  return S_OK;
}

}  // namespace credential_provider