Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions src/lib/bfc_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,91 @@ static size_t read_compressed_file(bfc_t* r, bfc_reader_entry_t* entry, uint64_t
return copy_size;
}

// Read an encrypted, uncompressed object. AEAD covers the whole object, so the
// entire payload has to be decrypted before any slice can be returned.
static size_t read_encrypted_file(bfc_t* r, bfc_reader_entry_t* entry, uint64_t offset, void* buf,
size_t len) {
if (!r->has_encryption_key) {
return 0; // No decryption key available
}

// Calculate content start position
if (bfc_os_seek(r->file, (int64_t) entry->obj_offset, SEEK_SET) != BFC_OK) {
return 0;
}

struct bfc_obj_hdr obj_hdr;
if (fread(&obj_hdr, 1, sizeof(obj_hdr), r->file) != sizeof(obj_hdr)) {
return 0;
}

// Skip name and padding to get to content
uint16_t name_len = obj_hdr.name_len;
if (fseek(r->file, name_len, SEEK_CUR) != 0) {
return 0;
}

size_t hdr_name_size = sizeof(obj_hdr) + name_len;
size_t padding = bfc_padding_size(hdr_name_size, BFC_ALIGN);
if (padding > 0 && fseek(r->file, (long) padding, SEEK_CUR) != 0) {
return 0;
}

void* encrypted_data = malloc(obj_hdr.enc_size);
if (!encrypted_data) {
return 0;
}

size_t encrypted_read = fread(encrypted_data, 1, obj_hdr.enc_size, r->file);
if (encrypted_read != obj_hdr.enc_size) {
free(encrypted_data);
return 0;
}

// Create decryption key structure
bfc_encrypt_key_t decrypt_key;
if (bfc_encrypt_key_from_bytes(r->encryption_key, &decrypt_key) != BFC_OK) {
free(encrypted_data);
return 0;
}

bfc_decrypt_result_t decrypt_result = bfc_decrypt_data(
&decrypt_key, encrypted_data, obj_hdr.enc_size, entry->path, strlen(entry->path), 0);
bfc_encrypt_key_clear(&decrypt_key);
free(encrypted_data);

if (decrypt_result.error != BFC_OK || !decrypt_result.data) {
return 0;
}

// Validate CRC of decrypted data
bfc_crc32c_ctx_t crc_ctx;
bfc_crc32c_reset(&crc_ctx);
bfc_crc32c_update(&crc_ctx, decrypt_result.data, decrypt_result.decrypted_size);
uint32_t calculated_crc = bfc_crc32c_final(&crc_ctx);

if (calculated_crc != entry->crc32c) {
free(decrypt_result.data);
return 0;
}

if (offset >= decrypt_result.decrypted_size) {
free(decrypt_result.data);
return 0;
}

// Copy requested portion to output buffer
size_t copy_size = len;
if (offset + copy_size > decrypt_result.decrypted_size) {
copy_size = decrypt_result.decrypted_size - offset;
}

memcpy(buf, (uint8_t*) decrypt_result.data + offset, copy_size);
free(decrypt_result.data);

return copy_size;
}

size_t bfc_read(bfc_t* r, const char* container_path, uint64_t offset, void* buf, size_t len) {
if (!r || !container_path || !buf || len == 0) {
return 0;
Expand Down Expand Up @@ -575,6 +660,13 @@ size_t bfc_read(bfc_t* r, const char* container_path, uint64_t offset, void* buf
return read_compressed_file(r, entry, offset, buf, to_read);
}

// Handle encrypted (but uncompressed) files. Without this the raw ciphertext
// would be returned to the caller, and a wrong key would go unnoticed because
// the AEAD tag would never be checked.
if (entry->enc != BFC_ENC_NONE) {
return read_encrypted_file(r, entry, offset, buf, to_read);
}

// Calculate file position
// Find content start by parsing object header
if (bfc_os_seek(r->file, (int64_t) entry->obj_offset, SEEK_SET) != BFC_OK) {
Expand Down
101 changes: 101 additions & 0 deletions tests/unit/test_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,103 @@ static int test_symlink_partial_read(void) {
return 0;
}

#ifdef BFC_WITH_SODIUM
// Regression: bfc_read used to skip decryption for encrypted-but-uncompressed
// objects, handing raw ciphertext back to the caller. A wrong key went
// unnoticed too, because the AEAD tag was never verified.
static int test_read_encrypted_uncompressed(void) {
const char* filename = "reader_test_encrypted_plain.bfc";
const char* content = "encrypted but not compressed payload";
const size_t content_len = strlen(content);

// Raw keys rather than passwords: this exercises the same decryption path in
// bfc_read, but skips Argon2id, which takes minutes in a sanitised -O0 build.
uint8_t key[32];
uint8_t wrong_key[32];
for (size_t i = 0; i < sizeof(key); i++) {
key[i] = (uint8_t) (i + 1);
wrong_key[i] = (uint8_t) (0xFF - i);
}

unlink(filename);

bfc_t* writer = NULL;
int result = bfc_create(filename, 4096, 0, &writer);
assert(result == BFC_OK);

result = bfc_set_encryption_key(writer, key);
assert(result == BFC_OK);

// Compression stays off, so this exercises the uncompressed read path.
assert(bfc_get_compression(writer) == BFC_COMP_NONE);

FILE* temp = tmpfile();
assert(temp != NULL);
fwrite(content, 1, content_len, temp);
rewind(temp);

result = bfc_add_file(writer, "secret.txt", temp, 0644, bfc_os_current_time_ns(), NULL);
assert(result == BFC_OK);
fclose(temp);

result = bfc_finish(writer);
assert(result == BFC_OK);
bfc_close(writer);

char buffer[256];

// Without a key, nothing may be returned.
bfc_t* reader = NULL;
result = bfc_open(filename, &reader);
assert(result == BFC_OK);
assert(bfc_has_encryption(reader) == 1);

size_t bytes_read = bfc_read(reader, "secret.txt", 0, buffer, sizeof(buffer));
assert(bytes_read == 0);
bfc_close_read(reader);

// With the wrong key the AEAD tag must fail, not silently succeed.
reader = NULL;
result = bfc_open(filename, &reader);
assert(result == BFC_OK);
result = bfc_reader_set_encryption_key(reader, wrong_key);
assert(result == BFC_OK);

bytes_read = bfc_read(reader, "secret.txt", 0, buffer, sizeof(buffer));
assert(bytes_read == 0);
bfc_close_read(reader);

// With the right key the plaintext comes back verbatim.
reader = NULL;
result = bfc_open(filename, &reader);
assert(result == BFC_OK);
result = bfc_reader_set_encryption_key(reader, key);
assert(result == BFC_OK);

memset(buffer, 0, sizeof(buffer));
bytes_read = bfc_read(reader, "secret.txt", 0, buffer, sizeof(buffer));
assert(bytes_read == content_len);
buffer[bytes_read] = '\0';
assert(strcmp(buffer, content) == 0);

// Partial reads must slice the decrypted plaintext, not the ciphertext.
memset(buffer, 0, sizeof(buffer));
bytes_read = bfc_read(reader, "secret.txt", 10, buffer, 6);
assert(bytes_read == 6);
buffer[bytes_read] = '\0';
assert(memcmp(buffer, content + 10, 6) == 0);

// Reading past the end yields nothing.
bytes_read = bfc_read(reader, "secret.txt", content_len, buffer, sizeof(buffer));
assert(bytes_read == 0);

bfc_close_read(reader);
unlink(filename);

return 0;
}
#endif

int test_reader(void) {
if (test_open_container() != 0)
return 1;
Expand Down Expand Up @@ -1319,6 +1416,10 @@ int test_reader(void) {
return 1;
if (test_symlink_partial_read() != 0)
return 1;
#ifdef BFC_WITH_SODIUM
if (test_read_encrypted_uncompressed() != 0)
return 1;
#endif

return 0;
}
Loading