// 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 "components/enterprise/obfuscation/core/utils.h"

#include <array>
#include <utility>

#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/no_destructor.h"
#include "base/numerics/byte_conversions.h"
#include "crypto/aead.h"
#include "crypto/hash.h"
#include "crypto/kdf.h"
#include "crypto/random.h"

namespace enterprise_obfuscation {

HeaderData::HeaderData() = default;
HeaderData::HeaderData(base::span<const uint8_t, kKeySize> key,
                       std::vector<uint8_t> prefix)
    : nonce_prefix(std::move(prefix)) {
  base::span(derived_key).copy_from(key);
}

HeaderData::HeaderData(const HeaderData& other) = default;
HeaderData& HeaderData::operator=(const HeaderData& other) = default;

HeaderData::HeaderData(HeaderData&& other) noexcept = default;
HeaderData& HeaderData::operator=(HeaderData&& other) noexcept = default;

HeaderData::~HeaderData() = default;

namespace {

// Generates a random base key, which will be combined with a file-specific salt
// to generate a file obfuscation key. The base key is kept for the lifetime of
// the browser process, so after a restart files can no longer be deobfuscated.
base::span<const uint8_t, kKeySize> GetBaseKey() {
  static const auto key = crypto::RandBytesAsArray<kKeySize>();

  return key;
}

// Computes nonce. The structure is: noncePrefix | counter (4 bytes) | b (1
// byte). The last byte is to ensure that the ciphertext is different for the
// last chunk of a file.
const std::vector<uint8_t> ComputeNonce(base::span<const uint8_t> nonce_prefix,
                                        uint32_t counter,
                                        bool is_last_chunk) {
  std::array<uint8_t, sizeof(uint32_t)> encoded_counter =
      base::U32ToBigEndian(counter);

  std::vector<uint8_t> nonce;
  nonce.reserve(kNonceSize);
  nonce.insert(nonce.end(), nonce_prefix.begin(), nonce_prefix.end());
  nonce.insert(nonce.end(), encoded_counter.begin(), encoded_counter.end());
  nonce.push_back(is_last_chunk ? 0x01 : 0x00);

  return nonce;
}

}  // namespace

BASE_FEATURE(kEnterpriseFileObfuscation, base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE(kEnterpriseFileObfuscationArchiveAnalyzer,
             base::FEATURE_ENABLED_BY_DEFAULT);

bool IsFileObfuscationEnabled() {
  return base::FeatureList::IsEnabled(kEnterpriseFileObfuscation);
}

base::expected<std::vector<uint8_t>, Error> CreateHeader(
    std::array<uint8_t, kKeySize>* derived_key,
    std::vector<uint8_t>* nonce_prefix) {
  if (!IsFileObfuscationEnabled()) {
    return base::unexpected(Error::kDisabled);
  }

  if (!derived_key || !nonce_prefix) {
    return base::unexpected(Error::kObfuscationFailed);
  }

  // Create header and allocate space.
  std::vector<uint8_t> header;
  header.reserve(kHeaderSize);
  header.push_back(kHeaderSize);

  // Generate salt.
  std::vector<uint8_t> salt = crypto::RandBytesAsVector(kSaltSize);
  header.insert(header.end(), salt.begin(), salt.end());

  // Generate file-specific key.
  *derived_key = crypto::kdf::Hkdf<kKeySize>(crypto::hash::kSha256,
                                             GetBaseKey(), salt, {});

  // Generate nonce prefix.
  *nonce_prefix = crypto::RandBytesAsVector(kNoncePrefixSize);
  header.insert(header.end(), nonce_prefix->begin(), nonce_prefix->end());

  if (header.size() != kHeaderSize) {
    return base::unexpected(Error::kObfuscationFailed);
  }

  return base::ok(std::move(header));
}

base::expected<std::vector<uint8_t>, Error> ObfuscateDataChunk(
    base::span<const uint8_t> data,
    base::span<const uint8_t> derived_key,
    base::span<const uint8_t> nonce_prefix,
    uint32_t counter,
    bool is_last_chunk) {
  if (!IsFileObfuscationEnabled()) {
    return base::unexpected(Error::kDisabled);
  }

  // Compute nonce.
  if (crypto::aead::NonceSizeFor(crypto::aead::AES_256_GCM) != kNonceSize) {
    return base::unexpected(Error::kSchemeError);
  }
  std::vector<uint8_t> nonce =
      ComputeNonce(nonce_prefix, counter, is_last_chunk);

  // Encrypt the data and prepend the encrypted size.
  std::vector<uint8_t> encrypted_data =
      crypto::aead::Seal(crypto::aead::AES_256_GCM, derived_key, data, nonce,
                         /*associated_data=*/{});

  std::array<uint8_t, kChunkSizePrefixSize> size =
      base::U32ToBigEndian(static_cast<uint32_t>(encrypted_data.size()));
  encrypted_data.insert(encrypted_data.begin(), size.begin(), size.end());

  return base::ok(std::move(encrypted_data));
}

base::expected<size_t, Error> GetObfuscatedChunkSize(
    base::span<const uint8_t> data) {
  if (data.size() < kChunkSizePrefixSize) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }

  std::array<uint8_t, kChunkSizePrefixSize> size;
  std::copy_n(data.begin(), kChunkSizePrefixSize, size.begin());
  size_t chunk_size = base::U32FromBigEndian(size);
  if (chunk_size < kAuthTagSize || chunk_size > kMaxChunkSize) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }
  return base::ok(chunk_size);
}

base::expected<HeaderData, Error> GetHeaderData(
    base::span<const uint8_t> header) {
  if (!IsFileObfuscationEnabled()) {
    return base::unexpected(Error::kDisabled);
  }

  if (header.size() != kHeaderSize) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }

  const uint8_t stored_header_size = header[0];
  if (stored_header_size != header.size()) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }

  // Extract salt and nonce_prefix.
  header = header.subspan<1>();
  const auto& [salt, nonce_prefix] = header.split_at<kSaltSize>();

  // Generate file-specific key.
  std::array<uint8_t, kKeySize> derived_key = crypto::kdf::Hkdf<kKeySize>(
      crypto::hash::kSha256, GetBaseKey(), salt, {});

  return base::ok(
      HeaderData(std::move(derived_key), base::ToVector(nonce_prefix)));
}

base::expected<std::vector<uint8_t>, Error> DeobfuscateDataChunk(
    base::span<const uint8_t> data,
    base::span<const uint8_t> derived_key,
    base::span<const uint8_t> nonce_prefix,
    uint32_t counter,
    bool is_last_chunk) {
  if (!IsFileObfuscationEnabled()) {
    return base::unexpected(Error::kDisabled);
  }
  if (data.size() < kAuthTagSize) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }

  // Construct nonce.
  if (crypto::aead::NonceSizeFor(crypto::aead::AES_256_GCM) != kNonceSize) {
    return base::unexpected(Error::kSchemeError);
  }
  std::vector<uint8_t> nonce =
      ComputeNonce(nonce_prefix, counter, is_last_chunk);

  auto plaintext = crypto::aead::Open(crypto::aead::AES_256_GCM, derived_key,
                                      data, nonce, /*associated_data=*/{});
  if (!plaintext) {
    return base::unexpected(Error::kDeobfuscationFailed);
  }
  return base::ok(std::move(plaintext.value()));
}

base::expected<void, Error> DeobfuscateFileInPlace(
    const base::FilePath& file_path) {
  if (!IsFileObfuscationEnabled()) {
    return RecordAndReturn<void>(base::unexpected(Error::kDisabled));
  }

  // Open the obfuscated file in read-only mode.
  base::File file(file_path, base::File::FLAG_OPEN | base::File::FLAG_READ);
  if (!file.IsValid() || file.GetLength() == 0) {
    return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
  }

  // Create and open a temporary file for deobfuscation.
  base::FilePath temp_path;
  base::FilePath temp_dir;
#if BUILDFLAG(IS_CHROMEOS)
  // On ChromeOS, virtual/cloud filesystems (like ODFS and DriveFS) do not
  // support atomic renames and can have issues with creating temporary files in
  // the same directory. For virtual filesystems (paths under /media/fuse),
  // we create the temporary file in the local temp directory instead.
  if (IsVirtualFilesystem(file_path)) {
    if (!base::GetTempDir(&temp_dir)) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kFileOperationError));
    }
  } else {
    temp_dir = file_path.DirName();
  }
#else
  temp_dir = file_path.DirName();
#endif

  if (!base::CreateTemporaryFileInDir(temp_dir, &temp_path)) {
    return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
  }

  // Ensure cleanup of temporary file on all error exits.
  base::ScopedClosureRunner temp_file_cleanup(
      base::BindOnce(base::IgnoreResult(&base::DeleteFile), temp_path));

  base::File deobfuscated_file(temp_path,
                               base::File::FLAG_OPEN | base::File::FLAG_APPEND);
  if (!deobfuscated_file.IsValid()) {
    return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
  }

  // Get header data
  if (file.GetLength() < static_cast<int64_t>(kHeaderSize)) {
    return RecordAndReturn<void>(base::unexpected(Error::kDeobfuscationFailed));
  }
  std::vector<uint8_t> header(kHeaderSize);
  std::optional<size_t> header_read = file.ReadAtCurrentPos(header);
  if (!header_read || header_read != kHeaderSize) {
    return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
  }
  auto header_data = GetHeaderData(header);
  if (!header_data.has_value()) {
    return RecordAndReturn<void>(base::unexpected(header_data.error()));
  }

  // Initialize cipher.
  crypto::Aead aead(crypto::Aead::AES_256_GCM, header_data.value().derived_key);
  if (aead.NonceLength() != kNonceSize) {
    return RecordAndReturn<void>(base::unexpected(Error::kSchemeError));
  }
  uint32_t counter = 0;
  int64_t total_bytes_read = header_read.value();

  int64_t file_size = file.GetLength();
  if (file_size < 0) {
    return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
  }

  // Deobfuscate to temporary file.
  while (total_bytes_read < file_size) {
    // Get the size of the next obfuscated chunk.
    std::array<uint8_t, kChunkSizePrefixSize> size;
    std::optional<size_t> size_read = file.ReadAtCurrentPos(size);
    if (!size_read || size_read.value() != kChunkSizePrefixSize) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kFileOperationError));
    }

    auto chunk_size = GetObfuscatedChunkSize(size);
    if (!chunk_size.has_value()) {
      return RecordAndReturn<void>(base::unexpected(chunk_size.error()));
    }
    total_bytes_read += kChunkSizePrefixSize;

    // Read in obfuscated chunk.
    std::vector<uint8_t> ciphertext(chunk_size.value());
    std::optional<size_t> bytes_read = file.ReadAtCurrentPos(ciphertext);
    if (!bytes_read) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kFileOperationError));
    }
    if (bytes_read.value() != chunk_size) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kDeobfuscationFailed));
    }

    total_bytes_read += bytes_read.value();

    std::vector<uint8_t> nonce =
        ComputeNonce(header_data.value().nonce_prefix, counter++,
                     total_bytes_read == file_size);

    auto plaintext = aead.Open(ciphertext, nonce, base::span<uint8_t>());
    if (!plaintext) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kDeobfuscationFailed));
    }
    if (!deobfuscated_file.WriteAtCurrentPosAndCheck(plaintext.value())) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kFileOperationError));
    }
  }
  file.Close();
  deobfuscated_file.Close();

  // If deobfuscation is successful, replace the original file.
  if (!base::ReplaceFile(temp_path, file_path, /*error=*/nullptr)) {
    // For cross-device errors, fallback to move for copy+delete instead.
    if (!base::Move(temp_path, file_path)) {
      return RecordAndReturn<void>(
          base::unexpected(Error::kFileOperationError));
    }
  }

  std::ignore = temp_file_cleanup.Release();
  return RecordAndReturn<void>(base::ok());
}

void RecordObfuscationResult(Error result) {
  base::UmaHistogramEnumeration(kObfuscationResultHistogram, result);
}

#if BUILDFLAG(IS_CHROMEOS)
bool IsVirtualFilesystem(const base::FilePath& path) {
  const base::FilePath kFusePath("/media/fuse");
  return kFusePath == path || kFusePath.IsParent(path);
}
#endif

}  // namespace enterprise_obfuscation
