Skip to content

Conversation

ccfelius
Copy link
Contributor

@ccfelius ccfelius commented Jun 23, 2025

Follow up on #17955.

This PR enables to encrypt temporary files by default when an encrypted database is used.

Temporary files can be of three different 'types':

  • Compressed temp files (suffixed with .tmp)
  • Default-sized temp files (256KB, suffixed with .tmp)
  • Larger-then-default temp files (suffixed with .block)

For the latter type, the size is prefixed in plaintext of an encrypted (.block) temp file.

Usage

When using

ATTACH 'encrypted.db' as enc (ENCRYPTION_KEY 'asdf');

temporary files will be encrypted by default. The key for temporary file encryption is randomly generated; this means that temp files that are leftover after e.g. a crash will be unreadable - because the key is also lost in that case.

Disabling temp file encryption

This ideally should be avoided, but e.g. for performance improvement temporary file encryption can also be en/disabled by

SET temp_file_encryption = false;
SET temp_file_encryption = true;

However, again, you should be careful with the use of these PRAGMAs since incorrect usage could lead to corrupted temp files (e.g. a mix of encrypted and plaintext temp files).

Tests

Test are in test/sql/storage/encryption/temp_files/. They partly consist of some (adapted) existing tests that are available for temporary files.

@ccfelius ccfelius changed the title Temporary file encryption Adding temporary file encryption Jun 23, 2025
Copy link
Collaborator

@Mytherin Mytherin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! Looks good - some comments below:

set.AddFunction(
PragmaFunction::PragmaStatement("debug_disable_temp_file_encryption", PragmaDisableTempFilesEncryption));

set.AddFunction(PragmaFunction::PragmaStatement("enable_full_encryption", PragmaEnableFullEncryption));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between full_encryption and temp_file_encryption?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Full encryption is standard db encryption + wal + temp file encryption. I might wanted to add a setting that in one go disables WAL and temp file encryption all together. This is mainly for performance reasons, since users might not care about encrypting WAL and temp files.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems error-prone to have the extra setting. I'm not sure if we are testing all combinations correctly here. What happens if full_encryption=true but encrypt_wal=false and encrypt_temp_file=false? What is supposed to/expected to happen in that case?

I think having only the two separate settings (encrypt_wal + encrypt_temp_file) is easier to reason about. We can always add a dummy setting that enables these (i.e. SET full_encryption=true sets encrypt_wal=true and encrypt_temp_file=true).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems better indeed, thanks!


set.AddFunction(PragmaFunction::PragmaStatement("debug_enable_wal_encryption", PragmaEnableWalEncryption));
set.AddFunction(PragmaFunction::PragmaStatement("debug_disable_wal_encryption", PragmaDisableWalEncryption));
set.AddFunction(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we turn this into settings?

AddTempKeyToCache(db);
}

auto temp_key = GetKeyFromCache(db, "temp_key");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there is a lot of duplicated code between the two EncryptTemporaryBuffer methods - can we unify them?

// zero-out the metadata buffer
memset(metadata, 0, DEFAULT_ENCRYPTED_BUFFER_HEADER_SIZE);

uint8_t tag[MainHeader::AES_TAG_LEN];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pattern seems to be present all over the code - perhaps it would be nicer to have a dedicated class for this? e.g.:

struct EncryptionTag {
    EncryptionTag() {
        memset(tag, 0, MainHeader::AES_TAG_LEN);
    }
    data_ptr_t data();
    idx_t size();

    uint8_t data[MainHeader::AES_TAG_LEN];
};

Same with the nonce.

@@ -43,6 +44,7 @@ unique_ptr<FileBuffer> StandardBufferManager::ConstructManagedBuffer(idx_t size,
if (source) {
auto tmp = std::move(source);
D_ASSERT(tmp->AllocSize() == BufferManager::GetAllocSize(size + block_header_size));
tmp->Restructure(size, block_header_size);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we instead add the block_header_size to the constructor of the FileBuffer (FileBuffer(FileBuffer &source, FileBufferType type, idx_t block_header_size))? Then the file buffer itself can decide to restructure if required and we don't need to remember to do it ourselves outside of the FileBuffer class.

}

static void PragmaEnableTempFilesEncryption(ClientContext &context, const FunctionParameters &parameters) {
DBConfig::GetConfig(context).options.enable_temp_file_encryption = true;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we maybe verify there are no temporary files currently active when setting this - and throw an error if there are any - given that this will lead to not being able to read/interact with the existing temporary files?

@duckdb-draftbot duckdb-draftbot marked this pull request as draft June 24, 2025 12:59
@ccfelius ccfelius marked this pull request as ready for review June 25, 2025 13:38
@duckdb-draftbot duckdb-draftbot marked this pull request as draft June 25, 2025 20:44
@ccfelius ccfelius marked this pull request as ready for review June 25, 2025 20:53
@duckdb-draftbot duckdb-draftbot marked this pull request as draft June 26, 2025 12:24
@Mytherin Mytherin marked this pull request as ready for review July 1, 2025 08:33
Mytherin added a commit that referenced this pull request Jul 10, 2025
Follow-up from #18013

This PR adds support for encrypting temporary files (i.e. files spilled
to disk in out-of-memory situations). This can be enabled using the
`temp_file_encryption` setting:

```sql
SET temp_file_encryption=true;
```

When enabled, any files spilled to disk are also encrypted. The key for
those files is kept in-memory and not stored anywhere - if the database
loses connection, the contents of the temporary files cannot be read
again.
@Mytherin Mytherin merged commit d287d45 into duckdb:main Jul 10, 2025
52 of 54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants