blob: 232ceeeae436b10f5c6151cb387889baacf3b8c3 [file] [log] [blame]
// 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/hkdf.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);
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::HkdfSha256<kKeySize>(GetBaseKey(), salt, base::span<uint8_t>());
// 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);
}
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(derived_key);
// Compute nonce.
if (aead.NonceLength() != 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 =
aead.Seal(data, nonce, base::span<uint8_t>());
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 > 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::HkdfSha256<kKeySize>(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);
}
crypto::Aead aead(crypto::Aead::AES_256_GCM);
aead.Init(derived_key);
if (data.size() < kAuthTagSize) {
return base::unexpected(Error::kDeobfuscationFailed);
}
// Construct nonce.
if (aead.NonceLength() != kNonceSize) {
return base::unexpected(Error::kSchemeError);
}
std::vector<uint8_t> nonce =
ComputeNonce(nonce_prefix, counter, is_last_chunk);
auto plaintext = aead.Open(data, nonce, base::span<uint8_t>());
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;
if (!base::CreateTemporaryFileInDir(file_path.DirName(), &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);
// Get header data
if (static_cast<size_t>(file.GetLength()) < 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);
aead.Init(header_data.value().derived_key);
if (aead.NonceLength() != kNonceSize) {
return RecordAndReturn<void>(base::unexpected(Error::kSchemeError));
}
uint32_t counter = 0;
size_t total_bytes_read = header_read.value();
int64_t file_length = file.GetLength();
if (file_length < 0) {
return RecordAndReturn<void>(base::unexpected(Error::kFileOperationError));
}
const size_t file_size = static_cast<size_t>(file_length);
// 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));
}
deobfuscated_file.WriteAtCurrentPos(plaintext.value());
}
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);
}
} // namespace enterprise_obfuscation