// 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.

#include "chrome/browser/safe_browsing/download_protection/file_analyzer.h"

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/file_util_service.h"
#include "chrome/browser/safe_browsing/download_protection/download_protection_util.h"
#include "chrome/common/safe_browsing/archive_analyzer_results.h"
#include "chrome/common/safe_browsing/download_type_util.h"
#include "components/enterprise/obfuscation/core/utils.h"
#include "components/safe_browsing/content/common/file_type_policies.h"
#include "components/safe_browsing/content/common/proto/download_file_types.pb.h"
#include "components/safe_browsing/core/common/features.h"
#include "content/public/browser/browser_thread.h"
#include "url/gurl.h"

namespace safe_browsing {

namespace {

using content::BrowserThread;

FileAnalyzer::Results ExtractFileFeatures(
    scoped_refptr<BinaryFeatureExtractor> binary_feature_extractor,
    base::FilePath file_path) {
  FileAnalyzer::Results results;
  binary_feature_extractor->CheckSignature(file_path, &results.signature_info);

  if (!binary_feature_extractor->ExtractImageFeatures(
          file_path, BinaryFeatureExtractor::kDefaultOptions,
          &results.image_headers, nullptr)) {
    results.image_headers = ClientDownloadRequest::ImageHeaders();
  }

  return results;
}

}  // namespace

FileAnalyzer::Results::Results() = default;
FileAnalyzer::Results::~Results() = default;
FileAnalyzer::Results::Results(const FileAnalyzer::Results& other) = default;

FileAnalyzer::FileAnalyzer(
    scoped_refptr<BinaryFeatureExtractor> binary_feature_extractor,
    bool is_obfuscated)
    : binary_feature_extractor_(binary_feature_extractor) {
#if !BUILDFLAG(IS_ANDROID)
  is_obfuscated_ = is_obfuscated;
#endif
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
}

FileAnalyzer::~FileAnalyzer() = default;

void FileAnalyzer::Start(const base::FilePath& target_file_name,
                         const base::FilePath& tmp_path,
                         base::optional_ref<const std::string> password,
                         base::OnceCallback<void(Results)> callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  target_file_name_ = target_file_name;
  tmp_path_ = tmp_path;
  password_ = password.CopyAsOptional();
  callback_ = std::move(callback);
  start_time_ = base::Time::Now();

#if !BUILDFLAG(IS_ANDROID)
  DownloadFileType::InspectionType inspection_type =
      FileTypePolicies::GetInstance()
          ->PolicyForFile(target_file_name_, GURL{}, nullptr)
          .inspection_type();

  if (inspection_type == DownloadFileType::ZIP) {
    StartExtractZipFeatures();
    return;
  }

  if (inspection_type == DownloadFileType::RAR) {
    StartExtractRarFeatures();
    return;
  }

#if BUILDFLAG(IS_MAC)
  if (inspection_type == DownloadFileType::DMG) {
    StartExtractDmgFeatures();
    return;
  }
#endif  // BUILDFLAG(IS_MAC)

  if (inspection_type == DownloadFileType::SEVEN_ZIP) {
    StartExtractSevenZipFeatures();
    return;
  }
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_MAC)
  // Checks for existence of "koly" signature even if file doesn't have
  // archive-type extension, then calls ExtractFileOrDmgFeatures() with
  // result.
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock(), base::TaskPriority::USER_VISIBLE},
      base::BindOnce(DiskImageTypeSnifferMac::IsAppleDiskImage, tmp_path_),
      base::BindOnce(&FileAnalyzer::ExtractFileOrDmgFeatures,
                     weakptr_factory_.GetWeakPtr()));
#else   // BUILDFLAG(IS_MAC)
  StartExtractFileFeatures();
#endif  // BUILDFLAG(IS_MAC)
}

void FileAnalyzer::StartExtractFileFeatures() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE,
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
       base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN},
      base::BindOnce(&ExtractFileFeatures, binary_feature_extractor_,
                     tmp_path_),
      base::BindOnce(&FileAnalyzer::OnFileAnalysisFinished,
                     weakptr_factory_.GetWeakPtr()));
}

void FileAnalyzer::OnFileAnalysisFinished(FileAnalyzer::Results results) {
  LogAnalysisDurationWithAndWithoutSuffix("Executable");
  results.type = download_type_util::GetDownloadType(target_file_name_);
  results.inspection_performed = DownloadFileType::NONE;
  std::move(callback_).Run(results);
}

#if !BUILDFLAG(IS_ANDROID)
void FileAnalyzer::StartExtractZipFeatures() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto callback = base::BindOnce(&FileAnalyzer::OnZipAnalysisFinished,
                                 weakptr_factory_.GetWeakPtr());
  // We give the zip analyzer a weak pointer to this object.
  if (is_obfuscated_ &&
      base::FeatureList::IsEnabled(
          enterprise_obfuscation::kEnterpriseFileObfuscationArchiveAnalyzer)) {
    zip_analyzer_ = SandboxedZipAnalyzer::CreateObfuscatedAnalyzer(
        tmp_path_, password_, std::move(callback), LaunchFileUtilService());
  } else {
    zip_analyzer_ = SandboxedZipAnalyzer::CreateAnalyzer(
        tmp_path_, password_, std::move(callback), LaunchFileUtilService());
  }
  zip_analyzer_->Start();
}

void FileAnalyzer::OnZipAnalysisFinished(
    const ArchiveAnalyzerResults& archive_results) {
  base::UmaHistogramEnumeration("SBClientDownload.ZipArchiveAnalysisResult",
                                archive_results.analysis_result);
  LogAnalysisDurationWithAndWithoutSuffix("Zip");
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Even if !results.success, some of the zip may have been parsed.
  // Some unzippers will successfully unpack archives that we cannot,
  // so we're lenient here.
  if (archive_results.success) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::VALID);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTimeout) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::PARSER_TIMED_OUT);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTooLarge) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::TOO_LARGE);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kDiskError) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::DISK_ERROR);
  }
  results_.archived_executable = archive_results.has_executable;
  results_.archived_archive = archive_results.has_archive;
  results_.archived_binaries =
      SelectArchiveEntries(archive_results.archived_binary);

  if (archive_results.has_executable) {
    results_.type = ClientDownloadRequest::ZIPPED_EXECUTABLE;
  } else if (archive_results.has_archive) {
    results_.type = ClientDownloadRequest::ZIPPED_ARCHIVE;
  } else if (!archive_results.success) {
    // .zip files that look invalid to Chrome can often be successfully
    // unpacked by other archive tools, so they may be a real threat.
    results_.type = ClientDownloadRequest::INVALID_ZIP;
  } else {
    results_.type = download_type_util::GetDownloadType(target_file_name_);
  }

  results_.archive_summary.set_file_count(archive_results.file_count);
  results_.archive_summary.set_directory_count(archive_results.directory_count);
  results_.archive_summary.set_is_encrypted(
      archive_results.encryption_info.is_encrypted);
  results_.encryption_info = archive_results.encryption_info;

  results_.inspection_performed = DownloadFileType::ZIP;
  std::move(callback_).Run(std::move(results_));
}

void FileAnalyzer::StartExtractRarFeatures() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  auto callback = base::BindOnce(&FileAnalyzer::OnRarAnalysisFinished,
                                 weakptr_factory_.GetWeakPtr());
  // We give the rar analyzer a weak pointer to this object. Since the
  // analyzer is refcounted, it might outlive the request.
  if (is_obfuscated_ &&
      base::FeatureList::IsEnabled(
          enterprise_obfuscation::kEnterpriseFileObfuscationArchiveAnalyzer)) {
    rar_analyzer_ = SandboxedRarAnalyzer::CreateObfuscatedAnalyzer(
        tmp_path_, password_, std::move(callback), LaunchFileUtilService());
  } else {
    rar_analyzer_ = SandboxedRarAnalyzer::CreateAnalyzer(
        tmp_path_, password_, std::move(callback), LaunchFileUtilService());
  }
  rar_analyzer_->Start();
}

void FileAnalyzer::OnRarAnalysisFinished(
    const ArchiveAnalyzerResults& archive_results) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::UmaHistogramEnumeration("SBClientDownload.RarArchiveAnalysisResult",
                                archive_results.analysis_result);
  LogAnalysisDurationWithAndWithoutSuffix("Rar");

  if (archive_results.success) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::VALID);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTimeout) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::PARSER_TIMED_OUT);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTooLarge) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::TOO_LARGE);
  }
  results_.archived_executable = archive_results.has_executable;
  results_.archived_archive = archive_results.has_archive;
  results_.archived_binaries =
      SelectArchiveEntries(archive_results.archived_binary);

  if (archive_results.has_executable) {
    results_.type = ClientDownloadRequest::RAR_COMPRESSED_EXECUTABLE;
  } else if (archive_results.has_archive) {
    results_.type = ClientDownloadRequest::RAR_COMPRESSED_ARCHIVE;
  } else if (!archive_results.success) {
    // .rar files that look invalid to Chrome may be successfully unpacked by
    // other archive tools, so they may be a real threat.
    results_.type = ClientDownloadRequest::INVALID_RAR;
  } else {
    results_.type = download_type_util::GetDownloadType(target_file_name_);
  }

  results_.archive_summary.set_file_count(archive_results.file_count);
  results_.archive_summary.set_directory_count(archive_results.directory_count);
  results_.archive_summary.set_is_encrypted(
      archive_results.encryption_info.is_encrypted);
  results_.encryption_info = archive_results.encryption_info;

  results_.inspection_performed = DownloadFileType::RAR;
  std::move(callback_).Run(std::move(results_));
}
#endif  // !BUILDFLAG(IS_ANDROID)

#if BUILDFLAG(IS_MAC)
// This is called for .DMGs and other files that can be parsed by
// SandboxedDMGAnalyzer.
void FileAnalyzer::StartExtractDmgFeatures() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Directly use 'dmg' extension since download file may not have any
  // extension, but has still been deemed a DMG through file type sniffing.
  dmg_analyzer_ = SandboxedDMGAnalyzer::CreateAnalyzer(
      tmp_path_,
      FileTypePolicies::GetInstance()->GetMaxFileSizeToAnalyze("dmg"),
      base::BindRepeating(&FileAnalyzer::OnDmgAnalysisFinished,
                          weakptr_factory_.GetWeakPtr()),
      LaunchFileUtilService());
  dmg_analyzer_->Start();
}

void FileAnalyzer::ExtractFileOrDmgFeatures(
    bool download_file_has_koly_signature) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (download_file_has_koly_signature) {
    StartExtractDmgFeatures();
  } else {
    StartExtractFileFeatures();
  }
}

void FileAnalyzer::OnDmgAnalysisFinished(
    const safe_browsing::ArchiveAnalyzerResults& archive_results) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  base::UmaHistogramEnumeration("SBClientDownload.DmgArchiveAnalysisResult",
                                archive_results.analysis_result);
  LogAnalysisDurationWithAndWithoutSuffix("Dmg");

  if (archive_results.signature_blob.size() > 0) {
    results_.disk_image_signature =
        std::vector<uint8_t>(archive_results.signature_blob);
  }

  results_.detached_code_signatures.CopyFrom(
      archive_results.detached_code_signatures);

  // Even if !results.success, some of the DMG may have been parsed.
  results_.archived_executable = archive_results.has_executable;
  results_.archived_archive = archive_results.has_archive;
  results_.archived_binaries =
      SelectArchiveEntries(archive_results.archived_binary);

  if (archive_results.success) {
    results_.type = ClientDownloadRequest::MAC_EXECUTABLE;
  } else {
    results_.type = ClientDownloadRequest::MAC_ARCHIVE_FAILED_PARSING;
  }

  if (archive_results.success) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::VALID);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTimeout) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::PARSER_TIMED_OUT);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTooLarge) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::TOO_LARGE);
  }

  results_.archive_summary.set_is_encrypted(
      archive_results.encryption_info.is_encrypted);
  results_.encryption_info = archive_results.encryption_info;

  results_.inspection_performed = DownloadFileType::DMG;
  std::move(callback_).Run(std::move(results_));
}
#endif  // BUILDFLAG(IS_MAC)

#if !BUILDFLAG(IS_ANDROID)
void FileAnalyzer::StartExtractSevenZipFeatures() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  seven_zip_analyzer_ = SandboxedSevenZipAnalyzer::CreateAnalyzer(
      tmp_path_,
      base::BindOnce(&FileAnalyzer::OnSevenZipAnalysisFinished,
                     weakptr_factory_.GetWeakPtr()),
      LaunchFileUtilService());
  seven_zip_analyzer_->Start();
}

void FileAnalyzer::OnSevenZipAnalysisFinished(
    const ArchiveAnalyzerResults& archive_results) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  base::UmaHistogramEnumeration(
      "SBClientDownload.SevenZipArchiveAnalysisResult",
      archive_results.analysis_result);
  LogAnalysisDurationWithAndWithoutSuffix("SevenZip");

  // Even if !results.success, some of the 7z may have been parsed.
  // Some unzippers will successfully unpack archives that we cannot,
  // so we're lenient here.
  if (archive_results.success) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::VALID);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTimeout) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::PARSER_TIMED_OUT);
  } else if (archive_results.analysis_result ==
             ArchiveAnalysisResult::kTooLarge) {
    results_.archive_summary.set_parser_status(
        ClientDownloadRequest::ArchiveSummary::TOO_LARGE);
  }
  results_.archived_executable = archive_results.has_executable;
  results_.archived_archive = archive_results.has_archive;
  results_.archived_binaries =
      SelectArchiveEntries(archive_results.archived_binary);

  if (archive_results.has_executable) {
    results_.type = ClientDownloadRequest::SEVEN_ZIP_COMPRESSED_EXECUTABLE;
  } else if (archive_results.has_archive) {
    results_.type = ClientDownloadRequest::SEVEN_ZIP_COMPRESSED_ARCHIVE;
  } else if (!archive_results.success) {
    // .7z files that look invalid to Chrome can sometimes be successfully
    // unpacked by other archive tools, so they may be a real threat.
    results_.type = ClientDownloadRequest::INVALID_SEVEN_ZIP;
  } else {
    results_.type = download_type_util::GetDownloadType(target_file_name_);
  }

  results_.archive_summary.set_file_count(archive_results.file_count);
  results_.archive_summary.set_directory_count(archive_results.directory_count);
  results_.archive_summary.set_is_encrypted(
      archive_results.encryption_info.is_encrypted);
  results_.encryption_info = archive_results.encryption_info;

  results_.inspection_performed = DownloadFileType::SEVEN_ZIP;
  std::move(callback_).Run(std::move(results_));
}
#endif  // !BUILDFLAG(IS_ANDROID)

void FileAnalyzer::LogAnalysisDurationWithAndWithoutSuffix(
    const std::string& suffix) {
  base::UmaHistogramMediumTimes("SBClientDownload.FileAnalysisDuration",
                                base::Time::Now() - start_time_);
  base::UmaHistogramMediumTimes(
      "SBClientDownload.FileAnalysisDuration." + suffix,
      base::Time::Now() - start_time_);
}

}  // namespace safe_browsing
