chromium/chrome/browser/ash/exo/chrome_security_delegate.cc

// Copyright 2024 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/exo/chrome_security_delegate.h"

#include <memory>
#include <string>

#include "ash/components/arc/arc_util.h"
#include "ash/public/cpp/app_types_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/exo/chrome_data_exchange_delegate.h"
#include "chrome/browser/ash/extensions/file_manager/event_router.h"
#include "chrome/browser/ash/extensions/file_manager/event_router_factory.h"
#include "chrome/browser/ash/file_manager/fileapi_util.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/fusebox/fusebox_server.h"
#include "chrome/browser/ash/guest_os/guest_os_session_tracker.h"
#include "chrome/browser/ash/guest_os/guest_os_share_path.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_files.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "components/exo/shell_surface_util.h"
#include "content/public/common/drop_data.h"
#include "storage/browser/file_system/external_mount_points.h"
#include "storage/browser/file_system/file_system_context.h"
#include "storage/browser/file_system/file_system_url.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "ui/base/clipboard/file_info.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"

namespace ash {

namespace {

constexpr char kUriListSeparator[] = "\r\n";
constexpr char kVmFileScheme[] = "vmfile";
constexpr char kDefaultVmMount[] = "/mnt/shared";

void SendArcUrls(exo::SecurityDelegate::SendDataCallback callback,
                 const std::vector<GURL>& urls) {
  std::vector<std::string> lines;
  for (const GURL& url : urls) {
    if (!url.is_valid()) {
      continue;
    }
    lines.push_back(url.spec());
  }
  // Arc requires UTF16 for data.
  std::move(callback).Run(base::MakeRefCounted<base::RefCountedString16>(
      base::UTF8ToUTF16(base::JoinString(lines, kUriListSeparator))));
}

void SendAfterShare(ui::EndpointType target,
                    exo::SecurityDelegate::SendDataCallback callback,
                    std::vector<std::string> file_urls) {
  std::string joined = base::JoinString(file_urls, kUriListSeparator);
  scoped_refptr<base::RefCountedMemory> data;
  if (target == ui::EndpointType::kArc) {
    // Arc uses utf-16 data.
    data = base::MakeRefCounted<base::RefCountedString16>(
        base::UTF8ToUTF16(joined));
  } else {
    data = base::MakeRefCounted<base::RefCountedString>(std::move(joined));
  }

  std::move(callback).Run(data);
}

struct FileInfo {
  const base::FilePath path;
  const storage::FileSystemURL url;
};

// Returns true if path is shared with the specified VM, or for crostini if path
// is homedir or dir within.
bool IsPathShared(Profile* profile,
                  std::string vm_name,
                  bool is_crostini,
                  base::FilePath path) {
  auto* share_path = guest_os::GuestOsSharePath::GetForProfile(profile);
  if (share_path->IsPathShared(vm_name, path)) {
    return true;
  }
  if (is_crostini) {
    base::FilePath mount =
        file_manager::util::GetCrostiniMountDirectory(profile);
    return path == mount || mount.IsParent(path);
  }
  return false;
}

// Return VM mount path for specified `vm_name`.
base::FilePath GetVmMount(const std::string& vm_name) {
  if (vm_name == crostini::kCrostiniDefaultVmName) {
    return crostini::ContainerChromeOSBaseDirectory();
  }
  if (vm_name == plugin_vm::kPluginVmName) {
    return plugin_vm::ChromeOSBaseDirectory();
  }
  return base::FilePath(std::string(kDefaultVmMount));
}

// Translate |vm_paths| from |source| VM to host paths.
std::vector<FileInfo> TranslateVMToHost(const std::string vm_name,
                                        std::vector<ui::FileInfo> vm_paths) {
  std::vector<FileInfo> file_infos;
  Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
  bool is_crostini = vm_name == crostini::kCrostiniDefaultVmName;

  for (ui::FileInfo& info : vm_paths) {
    base::FilePath path = std::move(info.path);
    storage::FileSystemURL url;

    // Convert the VM path to a path in the host if possible (in homedir or
    // /mnt/chromeos for crostini; in //ChromeOS for Plugin VM), otherwise
    // prefix with 'vmfile:<vm_name>:' to avoid VMs spoofing host paths.
    // E.g. crostini /etc/mime.types => vmfile:termina:/etc/mime.types.
    if (!vm_name.empty() && vm_name != arc::kArcVmName) {
      if (file_manager::util::ConvertPathInsideVMToFileSystemURL(
              primary_profile, path, GetVmMount(vm_name),
              /*map_crostini_home=*/is_crostini, &url)) {
        path = url.path();
        // Only allow write to clipboard for paths that are shared.
        if (!IsPathShared(primary_profile, vm_name, is_crostini, path)) {
          LOG(WARNING) << "Unshared file path: " << path;
          continue;
        }
      } else {
        path = base::FilePath(
            base::StrCat({kVmFileScheme, ":", vm_name, ":", path.value()}));
      }
    }
    file_infos.push_back({std::move(path), std::move(url)});
  }
  return file_infos;
}

// Crack paths and get FileSystemURL.
std::vector<FileInfo> CrackPaths(std::vector<base::FilePath> paths) {
  storage::ExternalMountPoints* mount_points =
      storage::ExternalMountPoints::GetSystemInstance();
  base::FilePath virtual_path;
  std::vector<FileInfo> file_infos;

  for (auto& path : paths) {
    // Convert absolute host path to FileSystemURL if possible.
    storage::FileSystemURL url;
    if (mount_points->GetVirtualPath(path, &virtual_path)) {
      url = mount_points->CreateCrackedFileSystemURL(
          blink::StorageKey(), storage::kFileSystemTypeExternal, virtual_path);
    }
    file_infos.push_back({std::move(path), std::move(url)});
  }
  return file_infos;
}

// Share |files| with |target| VM and invoke |callback| with translated file:
// URLs.
void ShareAndTranslateHostToVM(
    const std::string& vm_name,
    const std::vector<FileInfo>& file_infos,
    base::OnceCallback<void(std::vector<std::string>)> callback) {
  Profile* primary_profile = ProfileManager::GetPrimaryUserProfile();
  bool is_crostini = vm_name == crostini::kCrostiniDefaultVmName;

  const std::string vm_prefix =
      base::StrCat({kVmFileScheme, ":", vm_name, ":"});
  std::vector<std::string> file_urls;
  auto* share_path = guest_os::GuestOsSharePath::GetForProfile(primary_profile);
  std::vector<base::FilePath> paths_to_share;

  for (auto& info : file_infos) {
    std::string file_url;
    bool share_required = false;
    if (vm_name == arc::kArcVmName) {
      GURL arc_url;
      if (!file_manager::util::ConvertPathToArcUrl(info.path, &arc_url,
                                                   &share_required)) {
        LOG(WARNING) << "Could not convert arc path " << info.path;
        continue;
      }
      file_url = arc_url.spec();
    } else if (!vm_name.empty()) {
      base::FilePath path;
      // Check if it is a path inside the VM: 'vmfile:<vm_name>:'.
      if (base::StartsWith(info.path.value(), vm_prefix,
                           base::CompareCase::SENSITIVE)) {
        file_url = ui::FilePathToFileURL(
            base::FilePath(info.path.value().substr(vm_prefix.size())));
      } else if (file_manager::util::ConvertFileSystemURLToPathInsideVM(
                     primary_profile, info.url, GetVmMount(vm_name),
                     /*map_crostini_home=*/is_crostini, &path)) {
        // Convert to path inside the VM.
        file_url = ui::FilePathToFileURL(path);
        share_required = true;
      } else {
        LOG(WARNING) << "Could not convert into VM path " << info.path;
        continue;
      }
    } else {
      // Use path without conversion as default.
      file_url = ui::FilePathToFileURL(info.path);
    }
    file_urls.push_back(std::move(file_url));
    if (share_required && !share_path->IsPathShared(vm_name, info.path)) {
      paths_to_share.push_back(std::move(info.path));
    }
  }

  if (!paths_to_share.empty()) {
    if (vm_name != plugin_vm::kPluginVmName) {
      auto vm_info =
          guest_os::GuestOsSessionTracker::GetForProfile(primary_profile)
              ->GetVmInfo(vm_name);
      if (!vm_info) {
        // VM must be running for copy-paste or drag-drop to be happening so
        // something's gone wrong, skip trying to share and just send the data.
        std::move(callback).Run(std::move(file_urls));
        return;
      }
      share_path->SharePaths(
          vm_name, vm_info->seneschal_server_handle(),
          std::move(paths_to_share),
          base::BindOnce(
              [](base::OnceCallback<void(std::vector<std::string>)> callback,
                 std::vector<std::string> file_urls, bool success,
                 const std::string& failure_reason) {
                if (!success) {
                  LOG(ERROR) << "Error sharing paths: " << failure_reason;
                }

                // Still send the data, even if sharing failed.
                std::move(callback).Run(std::move(file_urls));
              },
              std::move(callback), std::move(file_urls)));
      return;
    }

    // Show FilesApp move-to-windows-files dialog when Plugin VM is not shared.
    if (auto* event_router =
            file_manager::EventRouterFactory::GetForProfile(primary_profile)) {
      event_router->DropFailedPluginVmDirectoryNotShared();
    }
    file_urls.clear();
  }

  std::move(callback).Run(std::move(file_urls));
}

}  // namespace

// static
std::vector<base::FilePath> TranslateVMPathsToHost(
    const std::string& vm_name,
    const std::vector<ui::FileInfo>& vm_paths) {
  std::vector<FileInfo> translated = TranslateVMToHost(vm_name, vm_paths);
  std::vector<base::FilePath> result;
  for (auto& info : translated) {
    result.push_back(std::move(info.path));
  }
  return result;
}

// static
void ShareWithVMAndTranslateToFileUrls(
    const std::string& vm_name,
    const std::vector<base::FilePath>& files,
    base::OnceCallback<void(std::vector<std::string>)> callback) {
  ShareAndTranslateHostToVM(vm_name, CrackPaths(files), std::move(callback));
}

ChromeSecurityDelegate::ChromeSecurityDelegate() = default;

ChromeSecurityDelegate::~ChromeSecurityDelegate() = default;

bool ChromeSecurityDelegate::CanSelfActivate(aura::Window* window) const {
  // TODO(b/233691818): The default should be "false", and clients should
  // override that if they need to self-activate.
  //
  // Unfortunately, several clients don't have their own SecurityDelegate yet,
  // so we will continue to use the old exo::Permissions stuff until they do.
  return exo::HasPermissionToActivate(window);
}

bool ChromeSecurityDelegate::CanLockPointer(aura::Window* window) const {
  // TODO(b/200896773): Move this out from exo's default security delegate
  // define in client's security delegates.
  return ash::IsArcWindow(window) || ash::IsLacrosWindow(window);
}

ChromeSecurityDelegate::SetBoundsPolicy ChromeSecurityDelegate::CanSetBounds(
    aura::Window* window) const {
  // TODO(b/200896773): Move into LacrosSecurityDelegate when it exists.
  if (ash::IsLacrosWindow(window)) {
    return SetBoundsPolicy::DCHECK_IF_DECORATED;
  } else if (ash::IsArcWindow(window)) {
    // TODO(b/285252684): Move into ArcSecurityDelegate when it exists.
    return SetBoundsPolicy::ADJUST_IF_DECORATED;
  } else {
    return SetBoundsPolicy::IGNORE;
  }
}

std::vector<ui::FileInfo> ChromeSecurityDelegate::GetFilenames(
    ui::EndpointType source,
    const std::vector<uint8_t>& data) const {
  std::vector<ui::FileInfo> result;
  std::vector<FileInfo> file_infos = TranslateVMToHost(
      GetVmName(source),
      ui::URIListToFileInfos(std::string(data.begin(), data.end())));
  for (auto& info : file_infos) {
    result.push_back(ui::FileInfo(std::move(info.path), base::FilePath()));
  }
  return result;
}

void ChromeSecurityDelegate::SendFileInfo(
    ui::EndpointType target,
    const std::vector<ui::FileInfo>& files,
    SendDataCallback callback) const {
  std::vector<base::FilePath> paths;
  for (const auto& file : files) {
    paths.push_back(file.path);
  }

  ShareAndTranslateHostToVM(
      GetVmName(target), CrackPaths(std::move(paths)),
      base::BindOnce(&SendAfterShare, target, std::move(callback)));
}

void ChromeSecurityDelegate::SendPickle(ui::EndpointType target,
                                        const base::Pickle& pickle,
                                        SendDataCallback callback) {
  std::vector<storage::FileSystemURL> file_system_urls =
      GetFileSystemUrlsFromPickle(pickle);
  // ARC FileSystemURLs are converted to Content URLs.
  if (target == ui::EndpointType::kArc) {
    if (file_system_urls.empty()) {
      std::move(callback).Run(nullptr);
      return;
    }
    arc::ConvertToContentUrlsAndShare(
        ProfileManager::GetPrimaryUserProfile(), file_system_urls,
        base::BindOnce(&SendArcUrls, std::move(callback)));
    return;
  }

  std::vector<FileInfo> file_infos;
  for (auto& url : file_system_urls) {
    if (url.TypeImpliesPathIsReal()) {
      base::FilePath path = url.path();
      file_infos.emplace_back(std::move(path), std::move(url));
    } else if (base::FilePath path =
                   fusebox::Server::SubstituteFuseboxFilePath(url);
               !path.empty()) {
      file_infos.emplace_back(std::move(path), std::move(url));
    }
  }

  ShareAndTranslateHostToVM(
      GetVmName(target), std::move(file_infos),
      base::BindOnce(&SendAfterShare, target, std::move(callback)));
}

std::string ChromeSecurityDelegate::GetVmName(ui::EndpointType target) const {
  if (target == ui::EndpointType::kArc) {
    return arc::kArcVmName;
  } else if (target == ui::EndpointType::kPluginVm) {
    return plugin_vm::kPluginVmName;
  }
  return std::string();
}

}  // namespace ash