blob: 558caec428b4e0374c72f6151f48638f08cc61cd [file] [log] [blame] [edit]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/browser/sandboxed_unpacker.h"
#include <stddef.h>
#include <stdint.h>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/containers/to_vector.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/metrics/histogram_macros.h"
#include "base/path_service.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/timer/elapsed_timer.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "components/crx_file/crx_verifier.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/api/declarative_net_request/file_backed_ruleset_source.h"
#include "extensions/browser/api/declarative_net_request/install_index_helper.h"
#include "extensions/browser/api/declarative_net_request/ruleset_source.h"
#include "extensions/browser/computed_hashes.h"
#include "extensions/browser/content_verifier/content_verifier_key.h"
#include "extensions/browser/extension_file_task_runner.h"
#include "extensions/browser/install/crx_install_error.h"
#include "extensions/browser/install/sandboxed_unpacker_failure_reason.h"
#include "extensions/browser/install_stage.h"
#include "extensions/browser/json_file_sanitizer.h"
#include "extensions/browser/ruleset_parse_result.h"
#include "extensions/browser/verified_contents.h"
#include "extensions/browser/zipfile_installer.h"
#include "extensions/common/api/declarative_net_request/dnr_manifest_data.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_id.h"
#include "extensions/common/extension_l10n_util.h"
#include "extensions/common/extension_resource_path_normalizer.h"
#include "extensions/common/extension_utility_types.h"
#include "extensions/common/extensions_client.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/file_util.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_handlers/default_locale_handler.h"
#include "extensions/common/manifest_handlers/icons_handler.h"
#include "extensions/common/switches.h"
#include "extensions/strings/grit/extensions_strings.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/gfx/codec/png_codec.h"
using base::ASCIIToUTF16;
using content::BrowserThread;
namespace extensions {
namespace {
// Normalize the file path. If the call to base::NormalizeFilePath fails then we
// return the original path.
base::FilePath NormalizeFilePath(const base::FilePath& path) {
base::FilePath normalized;
if (!base::NormalizeFilePath(path, &normalized)) {
LOG(WARNING) << path.value() << " couldn't be normalized.";
return path;
}
return normalized;
}
// Work horse for FindWritableTempLocation. Creates a temp file in the folder
// tries to normalize the path.
bool VerifyWritableTempLocation(base::FilePath* temp_dir) {
if (temp_dir->empty()) {
return false;
}
base::FilePath temp_file;
if (!base::CreateTemporaryFileInDir(*temp_dir, &temp_file)) {
LOG(ERROR) << temp_dir->value() << " is not writable";
return false;
}
// NormalizeFilePath requires a non-empty file, so write some data.
// If you change the exit points of this function please make sure all
// exit points delete this temp file!
if (!base::WriteFile(temp_file, ".")) {
base::DeleteFile(temp_file);
return false;
}
*temp_dir = NormalizeFilePath(temp_file).DirName();
// Clean up the temp file.
base::DeleteFile(temp_file);
return true;
}
// This function tries to find a location for unpacking the extension archive
// that is writable. If no such location exists we can not proceed and should
// fail.
// The result will be written to |temp_dir|. The function will write to this
// parameter even if it returns false.
bool FindWritableTempLocation(const base::FilePath& extensions_dir,
base::FilePath* temp_dir) {
// On ChromeOS, we will only attempt to unpack extension in cryptohome (profile)
// directory to provide additional security/privacy and speed up the rest of
// the extension install process.
#if !BUILDFLAG(IS_CHROMEOS)
base::PathService::Get(base::DIR_TEMP, temp_dir);
if (VerifyWritableTempLocation(temp_dir)) {
return true;
}
#endif
*temp_dir = file_util::GetInstallTempDir(extensions_dir);
if (VerifyWritableTempLocation(temp_dir)) {
return true;
}
// Neither paths is link free chances are good installation will fail.
LOG(ERROR) << "Both the %TEMP% folder and the profile seem to be on "
<< "remote drives or read-only. Installation can not complete!";
return false;
}
std::set<base::FilePath> GetMessageCatalogPathsToBeSanitized(
const base::FilePath& locales_path) {
// Not all folders under _locales have to be valid locales.
base::FileEnumerator locales(locales_path, /*recursive=*/false,
base::FileEnumerator::DIRECTORIES);
std::set<base::FilePath> message_catalog_paths;
std::set<std::string> all_locales;
extension_l10n_util::GetAllLocales(&all_locales);
base::FilePath locale_path;
while (!(locale_path = locales.Next()).empty()) {
if (!extension_l10n_util::ShouldSkipValidation(locales_path, locale_path,
all_locales)) {
message_catalog_paths.insert(locale_path.Append(kMessagesFilename));
}
}
return message_catalog_paths;
}
// Callback for ComputedHashes::Create, compute hashes for all files except
// _metadata directory (e.g. computed_hashes.json itself).
bool ShouldComputeHashesForResource(
const base::FilePath& relative_resource_path) {
std::vector<base::FilePath::StringType> components =
relative_resource_path.GetComponents();
return !components.empty() && components[0] != kMetadataFolder;
}
std::optional<crx_file::VerifierFormat> g_verifier_format_override_for_test;
} // namespace
class SandboxedUnpacker::IOThreadState {
public:
IOThreadState() = default;
IOThreadState(const IOThreadState&) = delete;
IOThreadState& operator=(const IOThreadState&) = delete;
~IOThreadState() = default;
void CleanUp() {
image_sanitizer_.reset();
json_file_sanitizer_.reset();
}
data_decoder::DataDecoder* GetDataDecoder() { return &data_decoder_; }
void CreateImangeSanitizer(
const extensions::Extension* extension,
const base::FilePath& extension_root,
scoped_refptr<ImageSanitizer::Client> client,
const scoped_refptr<base::SequencedTaskRunner>& unpacker_io_task_runner) {
DCHECK(!image_sanitizer_);
std::set<base::FilePath> image_paths =
ExtensionsClient::Get()->GetBrowserImagePaths(extension);
image_sanitizer_ = ImageSanitizer::CreateAndStart(
client, extension_root, image_paths, unpacker_io_task_runner);
}
void CreateJsonFileSanitizer(
std::set<base::FilePath> message_catalog_paths,
JsonFileSanitizer::Callback callback,
const scoped_refptr<base::SequencedTaskRunner>& unpacker_io_task_runner) {
json_file_sanitizer_ = JsonFileSanitizer::CreateAndStart(
std::move(message_catalog_paths), std::move(callback),
unpacker_io_task_runner);
}
private:
// Controls our own lazily started, isolated instance of the Data Decoder
// service so that multiple decode operations related to this
// SandboxedUnpacker can share a single instance. Only used for image
// sanitization.
data_decoder::DataDecoder data_decoder_;
// The ImageSanitizer used to clean-up images.
std::unique_ptr<ImageSanitizer> image_sanitizer_;
std::unique_ptr<JsonFileSanitizer> json_file_sanitizer_;
};
SandboxedUnpackerClient::SandboxedUnpackerClient()
: RefCountedDeleteOnSequence<SandboxedUnpackerClient>(
content::GetUIThreadTaskRunner({})) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
}
void SandboxedUnpackerClient::ShouldComputeHashesForOffWebstoreExtension(
scoped_refptr<const Extension> extension,
base::OnceCallback<void(bool)> callback) {
std::move(callback).Run(false);
}
void SandboxedUnpackerClient::GetContentVerifierKey(
base::OnceCallback<void(ContentVerifierKey)> callback) {
std::move(callback).Run(kWebstoreSignaturesPublicKey);
}
SandboxedUnpacker::ScopedVerifierFormatOverrideForTest::
ScopedVerifierFormatOverrideForTest(crx_file::VerifierFormat format) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
DCHECK(!g_verifier_format_override_for_test.has_value());
g_verifier_format_override_for_test = format;
}
SandboxedUnpacker::ScopedVerifierFormatOverrideForTest::
~ScopedVerifierFormatOverrideForTest() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
g_verifier_format_override_for_test.reset();
}
SandboxedUnpacker::SandboxedUnpacker(
mojom::ManifestLocation location,
int creation_flags,
const base::FilePath& extensions_dir,
const scoped_refptr<base::SequencedTaskRunner>& unpacker_io_task_runner,
SandboxedUnpackerClient* client)
: client_(client),
extensions_dir_(extensions_dir),
location_(location),
creation_flags_(creation_flags),
format_verifier_override_(g_verifier_format_override_for_test),
unpacker_io_task_runner_(unpacker_io_task_runner),
io_thread_state_(std::make_unique<IOThreadState>()) {
// Tracking for crbug.com/692069. The location must be valid. If it's invalid,
// the utility process kills itself for a bad IPC.
CHECK_GT(location, mojom::ManifestLocation::kInvalidLocation);
CHECK_LE(location, mojom::ManifestLocation::kMaxValue);
}
bool SandboxedUnpacker::CreateTempDirectory() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
base::FilePath temp_dir;
if (!FindWritableTempLocation(extensions_dir_, &temp_dir)) {
ReportFailure(
SandboxedUnpackerFailureReason::COULD_NOT_GET_TEMP_DIRECTORY,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"COULD_NOT_GET_TEMP_DIRECTORY"));
return false;
}
if (!temp_dir_.CreateUniqueTempDirUnderPath(temp_dir)) {
ReportFailure(
SandboxedUnpackerFailureReason::COULD_NOT_CREATE_TEMP_DIRECTORY,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"COULD_NOT_CREATE_TEMP_DIRECTORY"));
return false;
}
return true;
}
void SandboxedUnpacker::StartWithCrx(const CRXFileInfo& crx_info) {
// We assume that we are started on the thread that the client wants us
// to do file IO on.
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
client_->OnStageChanged(InstallationStage::kVerification);
std::string expected_hash;
if (!crx_info.expected_hash.empty() &&
base::CommandLine::ForCurrentProcess()->HasSwitch(
extensions::switches::kEnableCrxHashCheck)) {
expected_hash = base::ToLowerASCII(crx_info.expected_hash);
}
if (!CreateTempDirectory()) {
return; // ReportFailure() already called.
}
// Initialize the path that will eventually contain the unpacked extension.
extension_root_ = temp_dir_.GetPath().AppendASCII(kTempExtensionName);
// Extract the public key and validate the package.
if (!ValidateSignature(
crx_info.path, expected_hash,
format_verifier_override_.value_or(crx_info.required_format))) {
return; // ValidateSignature() already reported the error.
}
client_->OnStageChanged(InstallationStage::kCopying);
// Copy the crx file into our working directory.
base::FilePath temp_crx_path =
temp_dir_.GetPath().Append(crx_info.path.BaseName());
if (!base::CopyFile(crx_info.path, temp_crx_path)) {
// Failed to copy extension file to temporary directory.
ReportFailure(SandboxedUnpackerFailureReason::
FAILED_TO_COPY_EXTENSION_FILE_TO_TEMP_DIRECTORY,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"FAILED_TO_COPY_EXTENSION_FILE_TO_TEMP_DIRECTORY"));
return;
}
base::FilePath normalized_crx_path = NormalizeFilePath(temp_crx_path);
client_->OnStageChanged(InstallationStage::kUnpacking);
// Make sure to create the directory where the extension will be unzipped, as
// the unzipper service requires it.
base::FilePath unzipped_dir =
normalized_crx_path.DirName().AppendASCII(kTempExtensionName);
base::File::Error error;
if (!base::CreateDirectoryAndGetError(unzipped_dir, &error)) {
LOG(ERROR) << "Failed to created directory " << unzipped_dir.value()
<< " with error " << error;
ReportFailure(SandboxedUnpackerFailureReason::UNZIP_FAILED,
l10n_util::GetStringUTF16(IDS_EXTENSION_PACKAGE_UNZIP_ERROR));
return;
}
Unzip(normalized_crx_path, unzipped_dir);
}
void SandboxedUnpacker::StartWithDirectory(const ExtensionId& extension_id,
const std::string& public_key,
const base::FilePath& directory) {
// We assume that we are started on the thread that the client wants us
// to do file IO on.
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
extension_id_ = extension_id;
public_key_ = public_key;
if (!CreateTempDirectory()) {
return; // ReportFailure() already called.
}
extension_root_ = temp_dir_.GetPath().AppendASCII(kTempExtensionName);
if (!base::Move(directory, extension_root_)) {
LOG(ERROR) << "Could not move " << directory.value() << " to "
<< extension_root_.value();
ReportFailure(
SandboxedUnpackerFailureReason::DIRECTORY_MOVE_FAILED,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"DIRECTORY_MOVE_FAILED"));
return;
}
Unpack(extension_root_);
}
SandboxedUnpacker::~SandboxedUnpacker() {
// To avoid blocking shutdown, don't delete temporary directory here if it
// hasn't been cleaned up or passed on to another owner yet.
// This is OK because ExtensionGarbageCollector will take care of the leaked
// |temp_dir_| eventually.
temp_dir_.Take();
unpacker_io_task_runner_->DeleteSoon(FROM_HERE, std::move(io_thread_state_));
}
void SandboxedUnpacker::Unzip(const base::FilePath& crx_path,
const base::FilePath& unzipped_dir) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
DCHECK(crx_path.DirName() == temp_dir_.GetPath());
ZipFileInstaller::Create(unpacker_io_task_runner_,
base::BindOnce(&SandboxedUnpacker::UnzipDone, this))
->LoadFromZipFileInDir(crx_path, unzipped_dir);
}
void SandboxedUnpacker::UnzipDone(const base::FilePath& zip_file,
const base::FilePath& unzip_dir,
const std::string& error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!error.empty()) {
ReportFailure(SandboxedUnpackerFailureReason::UNZIP_FAILED,
l10n_util::GetStringUTF16(IDS_EXTENSION_PACKAGE_UNZIP_ERROR));
return;
}
base::FilePath verified_contents_path =
file_util::GetVerifiedContentsPath(extension_root_);
// If the verified contents are already present in the _metadata folder, we
// can ignore the verified contents in the header.
if (compressed_verified_contents_.empty() ||
base::PathExists(verified_contents_path)) {
Unpack(unzip_dir);
return;
}
GetDataDecoder()->GzipUncompress(
compressed_verified_contents_,
base::BindOnce(&SandboxedUnpacker::OnVerifiedContentsUncompressed, this,
unzip_dir));
}
void SandboxedUnpacker::OnVerifiedContentsUncompressed(
const base::FilePath& unzip_dir,
base::expected<mojo_base::BigBuffer, std::string> result) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!result.has_value()) {
ReportFailure(SandboxedUnpackerFailureReason::
CRX_HEADER_VERIFIED_CONTENTS_UNCOMPRESSING_FAILURE,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"CRX_HEADER_VERIFIED_CONTENTS_UNCOMPRESSING_FAILURE"));
return;
}
// Make a copy, since |result| may store data in shared memory, accessible by
// some other processes.
std::vector<uint8_t> verified_contents = base::ToVector(*result);
client_->GetContentVerifierKey(
base::BindOnce(&SandboxedUnpacker::StoreVerifiedContentsInExtensionDir,
this, unzip_dir, std::move(verified_contents)));
}
void SandboxedUnpacker::StoreVerifiedContentsInExtensionDir(
const base::FilePath& unzip_dir,
base::span<const uint8_t> verified_contents,
ContentVerifierKey content_verifier_key) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!VerifiedContents::Create(
content_verifier_key,
{reinterpret_cast<const char*>(verified_contents.data()),
verified_contents.size()})) {
ReportFailure(
SandboxedUnpackerFailureReason::MALFORMED_VERIFIED_CONTENTS,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"MALFORMED_VERIFIED_CONTENTS"));
return;
}
base::FilePath metadata_path = extension_root_.Append(kMetadataFolder);
if (!base::CreateDirectory(metadata_path)) {
ReportFailure(
SandboxedUnpackerFailureReason::COULD_NOT_CREATE_METADATA_DIRECTORY,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"COULD_NOT_CREATE_METADATA_DIRECTORY"));
return;
}
base::FilePath verified_contents_path =
file_util::GetVerifiedContentsPath(extension_root_);
// Cannot write the verified contents file.
if (!base::WriteFile(verified_contents_path, verified_contents)) {
ReportFailure(SandboxedUnpackerFailureReason::
COULD_NOT_WRITE_VERIFIED_CONTENTS_INTO_FILE,
l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"COULD_NOT_WRITE_VERIFIED_CONTENTS_INTO_FILE"));
return;
}
Unpack(unzip_dir);
}
void SandboxedUnpacker::Unpack(const base::FilePath& directory) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
DCHECK(directory.DirName() == temp_dir_.GetPath());
base::FilePath manifest_path = extension_root_.Append(kManifestFilename);
// This calls `ReadManifestDone()` on completion.
ParseJsonFile(manifest_path);
}
void SandboxedUnpacker::ReadManifestDone(
base::expected<base::Value, std::string> result) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!result.has_value()) {
ReportUnpackExtensionFailed(result.error());
return;
}
const base::Value::Dict* dict = result->GetIfDict();
if (!dict) {
ReportUnpackExtensionFailed(manifest_errors::kInvalidManifest);
return;
}
std::string error_msg;
scoped_refptr<Extension> extension(
Extension::Create(extension_root_, location_, *dict, creation_flags_,
extension_id_, &error_msg));
if (!extension) {
ReportUnpackExtensionFailed(error_msg);
return;
}
std::vector<InstallWarning> warnings;
if (!file_util::ValidateExtension(extension.get(), &error_msg, &warnings)) {
ReportUnpackExtensionFailed(error_msg);
return;
}
extension->AddInstallWarnings(std::move(warnings));
UnpackExtensionSucceeded(std::move(result).value().TakeDict());
}
void SandboxedUnpacker::UnpackExtensionSucceeded(base::Value::Dict manifest) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
std::optional<base::Value::Dict> final_manifest(
RewriteManifestFile(manifest));
if (!final_manifest) {
return;
}
// Create an extension object that refers to the temporary location the
// extension was unpacked to. We use this until the extension is finally
// installed. For example, the install UI shows images from inside the
// extension.
// Localize manifest now, so confirm UI gets correct extension name.
// TODO(rdevlin.cronin): Continue removing std::string errors and replacing
// with std::u16string
std::string utf8_error;
if (!extension_l10n_util::LocalizeExtension(
extension_root_, &final_manifest.value(),
extension_l10n_util::GzippedMessagesPermission::kDisallow,
&utf8_error)) {
ReportFailure(
SandboxedUnpackerFailureReason::COULD_NOT_LOCALIZE_EXTENSION,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_MESSAGE,
base::UTF8ToUTF16(utf8_error)));
return;
}
extension_ =
Extension::Create(extension_root_, location_, final_manifest.value(),
Extension::REQUIRE_KEY | creation_flags_, &utf8_error);
if (!extension_.get()) {
ReportFailure(SandboxedUnpackerFailureReason::INVALID_MANIFEST,
u"Manifest is invalid: " + ASCIIToUTF16(utf8_error));
return;
}
// The install icon path may be empty, which is OK, but if it is not it should
// be normalized successfully.
const std::string& original_install_icon_path =
IconsInfo::GetIcons(extension_.get())
.Get(extension_misc::EXTENSION_ICON_LARGE,
ExtensionIconSet::Match::kBigger);
if (!original_install_icon_path.empty() &&
!NormalizeExtensionResourcePath(
base::FilePath::FromUTF8Unsafe(original_install_icon_path),
&install_icon_path_)) {
// Invalid path for browser image.
ReportFailure(
SandboxedUnpackerFailureReason::INVALID_PATH_FOR_BROWSER_IMAGE,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"INVALID_PATH_FOR_BROWSER_IMAGE"));
return;
}
manifest_ = std::move(manifest);
io_thread_state_->CreateImangeSanitizer(extension_.get(), extension_root_,
this, unpacker_io_task_runner_);
}
data_decoder::DataDecoder* SandboxedUnpacker::GetDataDecoder() {
return io_thread_state_->GetDataDecoder();
}
void SandboxedUnpacker::OnImageDecoded(const base::FilePath& path,
SkBitmap image) {
if (path == install_icon_path_) {
install_icon_ = image;
}
}
void SandboxedUnpacker::OnImageSanitizationDone(
ImageSanitizer::Status status,
const base::FilePath& file_path_for_error) {
if (status == ImageSanitizer::Status::kSuccess) {
// Next step is to sanitize the message catalogs.
ReadMessageCatalogs();
return;
}
SandboxedUnpackerFailureReason failure_reason =
SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED;
std::u16string error;
switch (status) {
case ImageSanitizer::Status::kImagePathError:
failure_reason =
SandboxedUnpackerFailureReason::INVALID_PATH_FOR_BROWSER_IMAGE;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"INVALID_PATH_FOR_BROWSER_IMAGE");
break;
case ImageSanitizer::Status::kFileReadError:
case ImageSanitizer::Status::kDecodingError:
error = l10n_util::GetStringFUTF16(
IDS_EXTENSION_PACKAGE_IMAGE_ERROR,
base::i18n::GetDisplayStringInLTRDirectionality(
file_path_for_error.BaseName().LossyDisplayName()));
break;
case ImageSanitizer::Status::kFileDeleteError:
failure_reason =
SandboxedUnpackerFailureReason::ERROR_REMOVING_OLD_IMAGE_FILE;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_REMOVING_OLD_IMAGE_FILE");
break;
case ImageSanitizer::Status::kEncodingError:
failure_reason =
SandboxedUnpackerFailureReason::ERROR_RE_ENCODING_THEME_IMAGE;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_RE_ENCODING_THEME_IMAGE");
break;
case ImageSanitizer::Status::kFileWriteError:
failure_reason = SandboxedUnpackerFailureReason::ERROR_SAVING_THEME_IMAGE;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_SAVING_THEME_IMAGE");
break;
default:
NOTREACHED();
}
ReportFailure(failure_reason, error);
}
void SandboxedUnpacker::ReadMessageCatalogs() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (LocaleInfo::GetDefaultLocale(extension_.get()).empty()) {
MessageCatalogsSanitized(base::ok());
return;
}
// Get the paths to the message catalogs we should sanitize on the file task
// runner.
base::FilePath locales_path = extension_root_.Append(kLocaleFolder);
extensions::GetExtensionFileTaskRunner()->PostTaskAndReplyWithResult(
FROM_HERE,
base::BindOnce(&GetMessageCatalogPathsToBeSanitized, locales_path),
base::BindOnce(&SandboxedUnpacker::SanitizeMessageCatalogs, this));
}
void SandboxedUnpacker::SanitizeMessageCatalogs(
const std::set<base::FilePath>& message_catalog_paths) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
io_thread_state_->CreateJsonFileSanitizer(
message_catalog_paths,
base::BindOnce(&SandboxedUnpacker::MessageCatalogsSanitized, this),
unpacker_io_task_runner_);
}
void SandboxedUnpacker::MessageCatalogsSanitized(
base::expected<void, JsonFileSanitizer::Error> result) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (result.has_value()) {
IndexAndPersistJSONRulesetsIfNeeded();
return;
}
SandboxedUnpackerFailureReason failure_reason =
SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED;
std::u16string error;
switch (result.error()) {
case JsonFileSanitizer::Error::kFileReadError:
case JsonFileSanitizer::Error::kDecodingError:
failure_reason = SandboxedUnpackerFailureReason::INVALID_CATALOG_DATA;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"INVALID_CATALOG_DATA");
break;
case JsonFileSanitizer::Error::kSerializingError:
failure_reason =
SandboxedUnpackerFailureReason::ERROR_SERIALIZING_CATALOG;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_SERIALIZING_CATALOG");
break;
case JsonFileSanitizer::Error::kFileDeleteError:
case JsonFileSanitizer::Error::kFileWriteError:
failure_reason = SandboxedUnpackerFailureReason::ERROR_SAVING_CATALOG;
error = l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_SAVING_CATALOG");
break;
}
ReportFailure(failure_reason, error);
}
void SandboxedUnpacker::IndexAndPersistJSONRulesetsIfNeeded() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
DCHECK(extension_);
// Defer ruleset indexing for disabled rulesets to speed up extension
// installation.
auto ruleset_filter = declarative_net_request::FileBackedRulesetSource::
RulesetFilter::kIncludeManifestEnabled;
// Ignore rule parsing errors since ruleset indexing (and therefore rule
// parsing) is deferred until the ruleset is enabled for packed extensions.
auto parse_flags = declarative_net_request::RulesetSource::kNone;
declarative_net_request::InstallIndexHelper::IndexStaticRulesets(
*extension_, ruleset_filter, parse_flags,
base::BindOnce(&SandboxedUnpacker::OnJSONRulesetsIndexed, this));
}
void SandboxedUnpacker::OnJSONRulesetsIndexed(RulesetParseResult result) {
if (result.error) {
ReportFailure(
SandboxedUnpackerFailureReason::ERROR_INDEXING_DNR_RULESET,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_MESSAGE,
base::UTF8ToUTF16(*result.error)));
return;
}
if (!result.warnings.empty()) {
extension_->AddInstallWarnings(std::move(result.warnings));
}
ruleset_install_prefs_ = std::move(result.ruleset_install_prefs);
CheckComputeHashes();
}
void SandboxedUnpacker::CheckComputeHashes() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
client_->ShouldComputeHashesForOffWebstoreExtension(
extension_, base::BindOnce(&SandboxedUnpacker::MaybeComputeHashes, this));
}
void SandboxedUnpacker::MaybeComputeHashes(bool should_compute) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (!should_compute) {
ReportSuccess();
return;
}
base::ElapsedTimer timer;
std::optional<ComputedHashes::Data> computed_hashes_data =
ComputedHashes::Compute(
extension_->path(),
extension_misc::kContentVerificationDefaultBlockSize,
IsCancelledCallback(),
base::BindRepeating(&ShouldComputeHashesForResource));
bool success =
computed_hashes_data &&
ComputedHashes(std::move(*computed_hashes_data))
.WriteToFile(file_util::GetComputedHashesPath(extension_->path()));
UMA_HISTOGRAM_BOOLEAN(
"Extensions.ContentVerification.ComputeHashesOnInstallResult", success);
if (success) {
UMA_HISTOGRAM_TIMES(
"Extensions.ContentVerification.ComputeHashesOnInstallTime",
timer.Elapsed());
} else {
LOG(ERROR) << "[extension " << extension_->id()
<< "] Failed to create computed_hashes.json";
}
ReportSuccess();
}
void SandboxedUnpacker::ReportUnpackExtensionFailed(std::string_view error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
ReportFailure(SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_MESSAGE,
base::UTF8ToUTF16(error)));
}
std::u16string SandboxedUnpacker::FailureReasonToString16(
const SandboxedUnpackerFailureReason reason) {
switch (reason) {
case SandboxedUnpackerFailureReason::COULD_NOT_GET_TEMP_DIRECTORY:
return u"COULD_NOT_GET_TEMP_DIRECTORY";
case SandboxedUnpackerFailureReason::COULD_NOT_CREATE_TEMP_DIRECTORY:
return u"COULD_NOT_CREATE_TEMP_DIRECTORY";
case SandboxedUnpackerFailureReason::
FAILED_TO_COPY_EXTENSION_FILE_TO_TEMP_DIRECTORY:
return u"FAILED_TO_COPY_EXTENSION_FILE_TO_TEMP_DIRECTORY";
case SandboxedUnpackerFailureReason::COULD_NOT_GET_SANDBOX_FRIENDLY_PATH:
return u"COULD_NOT_GET_SANDBOX_FRIENDLY_PATH";
case SandboxedUnpackerFailureReason::COULD_NOT_LOCALIZE_EXTENSION:
return u"COULD_NOT_LOCALIZE_EXTENSION";
case SandboxedUnpackerFailureReason::INVALID_MANIFEST:
return u"INVALID_MANIFEST";
case SandboxedUnpackerFailureReason::UNPACKER_CLIENT_FAILED:
return u"UNPACKER_CLIENT_FAILED";
case SandboxedUnpackerFailureReason::
UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL:
return u"UTILITY_PROCESS_CRASHED_WHILE_TRYING_TO_INSTALL";
case SandboxedUnpackerFailureReason::CRX_FILE_NOT_READABLE:
return u"CRX_FILE_NOT_READABLE";
case SandboxedUnpackerFailureReason::CRX_HEADER_INVALID:
return u"CRX_HEADER_INVALID";
case SandboxedUnpackerFailureReason::CRX_MAGIC_NUMBER_INVALID:
return u"CRX_MAGIC_NUMBER_INVALID";
case SandboxedUnpackerFailureReason::CRX_VERSION_NUMBER_INVALID:
return u"CRX_VERSION_NUMBER_INVALID";
case SandboxedUnpackerFailureReason::CRX_EXCESSIVELY_LARGE_KEY_OR_SIGNATURE:
return u"CRX_EXCESSIVELY_LARGE_KEY_OR_SIGNATURE";
case SandboxedUnpackerFailureReason::CRX_ZERO_KEY_LENGTH:
return u"CRX_ZERO_KEY_LENGTH";
case SandboxedUnpackerFailureReason::CRX_ZERO_SIGNATURE_LENGTH:
return u"CRX_ZERO_SIGNATURE_LENGTH";
case SandboxedUnpackerFailureReason::CRX_PUBLIC_KEY_INVALID:
return u"CRX_PUBLIC_KEY_INVALID";
case SandboxedUnpackerFailureReason::CRX_SIGNATURE_INVALID:
return u"CRX_SIGNATURE_INVALID";
case SandboxedUnpackerFailureReason::
CRX_SIGNATURE_VERIFICATION_INITIALIZATION_FAILED:
return u"CRX_SIGNATURE_VERIFICATION_INITIALIZATION_FAILED";
case SandboxedUnpackerFailureReason::CRX_SIGNATURE_VERIFICATION_FAILED:
return u"CRX_SIGNATURE_VERIFICATION_FAILED";
case SandboxedUnpackerFailureReason::CRX_FILE_IS_DELTA_UPDATE:
return u"CRX_FILE_IS_DELTA_UPDATE";
case SandboxedUnpackerFailureReason::CRX_EXPECTED_HASH_INVALID:
return u"CRX_EXPECTED_HASH_INVALID";
case SandboxedUnpackerFailureReason::ERROR_SERIALIZING_MANIFEST_JSON:
return u"ERROR_SERIALIZING_MANIFEST_JSON";
case SandboxedUnpackerFailureReason::ERROR_SAVING_MANIFEST_JSON:
return u"ERROR_SAVING_MANIFEST_JSON";
case SandboxedUnpackerFailureReason::INVALID_PATH_FOR_BROWSER_IMAGE:
return u"INVALID_PATH_FOR_BROWSER_IMAGE";
case SandboxedUnpackerFailureReason::ERROR_REMOVING_OLD_IMAGE_FILE:
return u"ERROR_REMOVING_OLD_IMAGE_FILE";
case SandboxedUnpackerFailureReason::INVALID_PATH_FOR_BITMAP_IMAGE:
return u"INVALID_PATH_FOR_BITMAP_IMAGE";
case SandboxedUnpackerFailureReason::ERROR_RE_ENCODING_THEME_IMAGE:
return u"ERROR_RE_ENCODING_THEME_IMAGE";
case SandboxedUnpackerFailureReason::ERROR_SAVING_THEME_IMAGE:
return u"ERROR_SAVING_THEME_IMAGE";
case SandboxedUnpackerFailureReason::INVALID_CATALOG_DATA:
return u"INVALID_CATALOG_DATA";
case SandboxedUnpackerFailureReason::ERROR_SERIALIZING_CATALOG:
return u"ERROR_SERIALIZING_CATALOG";
case SandboxedUnpackerFailureReason::ERROR_SAVING_CATALOG:
return u"ERROR_SAVING_CATALOG";
case SandboxedUnpackerFailureReason::CRX_HASH_VERIFICATION_FAILED:
return u"CRX_HASH_VERIFICATION_FAILED";
case SandboxedUnpackerFailureReason::UNZIP_FAILED:
return u"UNZIP_FAILED";
case SandboxedUnpackerFailureReason::DIRECTORY_MOVE_FAILED:
return u"DIRECTORY_MOVE_FAILED";
case SandboxedUnpackerFailureReason::ERROR_INDEXING_DNR_RULESET:
return u"ERROR_INDEXING_DNR_RULESET";
case SandboxedUnpackerFailureReason::CRX_REQUIRED_PROOF_MISSING:
return u"CRX_REQUIRED_PROOF_MISSING";
case SandboxedUnpackerFailureReason::DEPRECATED_ABORTED_DUE_TO_SHUTDOWN:
case SandboxedUnpackerFailureReason::DEPRECATED_ERROR_PARSING_DNR_RULESET:
case SandboxedUnpackerFailureReason::NUM_FAILURE_REASONS:
default:
NOTREACHED();
}
}
void SandboxedUnpacker::FailWithPackageError(
const SandboxedUnpackerFailureReason reason) {
ReportFailure(reason,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_ERROR_CODE,
FailureReasonToString16(reason)));
}
bool SandboxedUnpacker::ValidateSignature(
const base::FilePath& crx_path,
const std::string& expected_hash,
const crx_file::VerifierFormat required_format) {
std::vector<uint8_t> hash;
if (!expected_hash.empty()) {
if (!base::HexStringToBytes(expected_hash, &hash)) {
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_EXPECTED_HASH_INVALID);
return false;
}
}
const crx_file::VerifierResult result = crx_file::Verify(
crx_path, required_format, std::vector<std::vector<uint8_t>>(), hash,
&public_key_, &extension_id_, &compressed_verified_contents_);
switch (result) {
case crx_file::VerifierResult::OK_FULL: {
return true;
}
case crx_file::VerifierResult::OK_DELTA:
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_FILE_IS_DELTA_UPDATE);
break;
case crx_file::VerifierResult::ERROR_FILE_NOT_READABLE:
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_FILE_NOT_READABLE);
break;
case crx_file::VerifierResult::ERROR_HEADER_INVALID:
FailWithPackageError(SandboxedUnpackerFailureReason::CRX_HEADER_INVALID);
break;
case crx_file::VerifierResult::ERROR_SIGNATURE_INITIALIZATION_FAILED:
FailWithPackageError(
SandboxedUnpackerFailureReason::
CRX_SIGNATURE_VERIFICATION_INITIALIZATION_FAILED);
break;
case crx_file::VerifierResult::ERROR_SIGNATURE_VERIFICATION_FAILED:
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_SIGNATURE_VERIFICATION_FAILED);
break;
case crx_file::VerifierResult::ERROR_EXPECTED_HASH_INVALID:
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_EXPECTED_HASH_INVALID);
break;
case crx_file::VerifierResult::ERROR_REQUIRED_PROOF_MISSING:
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_REQUIRED_PROOF_MISSING);
break;
case crx_file::VerifierResult::ERROR_FILE_HASH_FAILED:
// We should never get this result unless we had specifically asked for
// verification of the crx file's hash.
CHECK(!expected_hash.empty());
FailWithPackageError(
SandboxedUnpackerFailureReason::CRX_HASH_VERIFICATION_FAILED);
break;
}
return false;
}
void SandboxedUnpacker::ReportFailure(
const SandboxedUnpackerFailureReason reason,
const std::u16string& error) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
UMA_HISTOGRAM_ENUMERATION(
"Extensions.SandboxUnpackFailureReason2", reason,
SandboxedUnpackerFailureReason::NUM_FAILURE_REASONS);
Cleanup();
client_->OnUnpackFailure(CrxInstallError(reason, error));
}
void SandboxedUnpacker::ReportSuccess() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
UMA_HISTOGRAM_COUNTS_1M("Extensions.SandboxUnpackSuccess", 1);
DCHECK(!temp_dir_.GetPath().empty());
// Client takes ownership of temporary directory, manifest, and extension.
client_->OnUnpackSuccess(
temp_dir_.Take(), extension_root_,
std::make_unique<base::Value::Dict>(std::move(manifest_.value())),
extension_.get(), install_icon_, std::move(ruleset_install_prefs_));
// Interestingly, the C++ standard doesn't guarantee that a moved-from vector
// is empty.
ruleset_install_prefs_.clear();
extension_.reset();
Cleanup();
}
std::optional<base::Value::Dict> SandboxedUnpacker::RewriteManifestFile(
const base::Value::Dict& manifest) {
constexpr int64_t kMaxFingerprintSize = 1024;
// Add the public key extracted earlier to the parsed manifest and overwrite
// the original manifest. We do this to ensure the manifest doesn't contain an
// exploitable bug that could be used to compromise the browser.
DCHECK(!public_key_.empty());
base::Value::Dict final_manifest = manifest.Clone();
final_manifest.Set(manifest_keys::kPublicKey, public_key_);
{
std::string differential_fingerprint;
if (base::ReadFileToStringWithMaxSize(
extension_root_.Append(kDifferentialFingerprintFilename),
&differential_fingerprint, kMaxFingerprintSize)) {
final_manifest.Set(manifest_keys::kDifferentialFingerprint,
std::move(differential_fingerprint));
}
}
std::optional<std::string> manifest_json = base::WriteJsonWithOptions(
final_manifest, base::JSONWriter::OPTIONS_PRETTY_PRINT);
if (!manifest_json) {
// Error serializing manifest.json.
ReportFailure(
SandboxedUnpackerFailureReason::ERROR_SERIALIZING_MANIFEST_JSON,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_SERIALIZING_MANIFEST_JSON"));
return std::nullopt;
}
base::FilePath manifest_path = extension_root_.Append(kManifestFilename);
if (!base::WriteFile(manifest_path, *manifest_json)) {
// Error saving manifest.json.
ReportFailure(
SandboxedUnpackerFailureReason::ERROR_SAVING_MANIFEST_JSON,
l10n_util::GetStringFUTF16(IDS_EXTENSION_PACKAGE_INSTALL_ERROR,
u"ERROR_SAVING_MANIFEST_JSON"));
return std::nullopt;
}
return std::move(final_manifest);
}
void SandboxedUnpacker::Cleanup() {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
if (temp_dir_.IsValid() && !temp_dir_.Delete()) {
LOG(WARNING) << "Can not delete temp directory at "
<< temp_dir_.GetPath().value();
}
io_thread_state_->CleanUp();
}
void SandboxedUnpacker::ParseJsonFile(const base::FilePath& path) {
DCHECK(unpacker_io_task_runner_->RunsTasksInCurrentSequence());
std::string contents;
if (!base::ReadFileToString(path, &contents)) {
ReadManifestDone(base::unexpected("File doesn't exist."));
return;
}
base::JSONReader::Result result =
base::JSONReader::ReadAndReturnValueWithError(contents);
ReadManifestDone(std::move(result).transform_error(
[](const base::JSONReader::Error& error) { return error.ToString(); }));
}
} // namespace extensions