You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Add pluggable page-level AEAD encryption support - #866
add an optional file encryption header carrying provider, profile, key, wrapped data key, and file identity metadata
introduce a provider-neutral AEAD SPI and registry without embedding a concrete cryptographic implementation
encrypt compressed page bodies for non-aligned, aligned, and table-model write paths, and decrypt them on the corresponding read paths
preserve existing unencrypted TsFile behavior and propagate encryption metadata through append, recovery, lazy loading, and sketch tooling
add compatibility, round-trip, tamper-detection, append, and recovery tests
Motivation
TsFile currently has legacy encryption interfaces but no self-describing file-level context for pluggable page-level authenticated encryption. This change establishes the format and I/O integration points while leaving algorithm and key-management implementations to external providers.
Compatibility
Existing unencrypted TsFiles keep their current layout and remain readable.
Encrypted files require a registered provider matching the identifiers stored in the encryption header.
Page metadata remains readable while compressed page bodies are protected with AEAD.
The encrypted-file format and public API are still under discussion and are not intended as a compatibility commitment in this draft.
❌ Patch coverage is 82.52551% with 274 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.93%. Comparing base (d22008b) to head (9ca3576).
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR introduces pluggable page-level AEAD encryption for TsFile by adding a self-describing file encryption header, a provider-neutral encryption SPI/registry, and integrating page body encrypt/decrypt across write and read paths while preserving unencrypted TsFile compatibility.
Changes:
Added FileEncryptionHeader (written after the TsFile version byte) and propagated encryption context through writers/readers, append, and recovery flows.
Introduced provider-neutral AEAD SPI (IEncryptProvider, EncryptionProviderRegistry) and page-associated-data binding (PageCryptoContext) with per-chunk ordinals and per-page indices.
Added/updated tests to validate round-trips, tamper detection, ordinal continuity across append/recovery, and reader behavior.
Encrypting every page body leaves TsFileLastReader.readAlignedLastPoint incompatible with encrypted aligned BLOB/OBJECT columns: that path slices the stored body and passes it directly to IUnCompressor/ValuePageReader without decryption (TsFileLastReader.java:179-216). It will feed ciphertext to the decoder (or decompressor), so last-point reads fail for these supported types. Route that path through the AEAD-aware page deserialization while tracking the selected page index and chunk ordinal.
Provider IDs are trimmed when registering and unregistering, but lookup uses the untrimmed persisted value. A provider whose declared ID contains surrounding whitespace is therefore registered successfully under the trimmed key yet can never be resolved from a parameter/header carrying that declared ID. Apply the same normalization during lookup (or reject non-canonical IDs consistently).
Preserve encryption context for materialized chunks
The same EncryptParameter instance is attached to every Chunk returned by readMemChunk() and to its lazy page readers. Destroying it here means an already materialized in-memory chunk becomes unusable as soon as the sequence reader is closed; subsequent page loading fails with parameter_destroyed. Give returned chunks an independently owned context or otherwise coordinate the context lifetime instead of invalidating it with the file handle.
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this. The overall direction looks good: a self-describing file header, a provider SPI, and per-page AEAD with the AAD bound to fileCryptoId / chunk ordinal / page index. The fail-closed checks on chunk copy/merge, the immutable file context and the ordinal validation are nice, and the lifecycle tests cover a lot of edge paths.
Validation: I ran the 7 test classes listed in the description plus the two new lifecycle test classes (72 tests), and the full java/tsfile suite (1038 tests, 0 failures, 10 skipped), on b58e5c37. The only change since then, fc88fb87, adds a pageWriter null check in reset() and doesn't affect the comments below. I reproduced each issue with a small probe test; the repro steps are in the inline comments.
Summary of the inline comments:
Blocking: page-level raw copy (writePageHeaderAndDataIntoBuff(ByteBuffer, PageHeader)) is not fail-closed for page AEAD and silently writes undecryptable files. This is the IoTDB fast-compaction path.
The AAD binds pages to chunk ordinals, but nothing binds chunks to their series: swapping whole chunks between s1 and s2 goes undetected.
TsFileReader now strongly retains every QueryDataSet, which changes the contract for existing users of unencrypted files.
With the reference-counted Chunk / ChunkReader ownership, cleanup only happens if every caller closes every chunk.
Recovery and unsealed reads: headerless file + AEAD parameter, a torn header surfacing as an unchecked exception, and the getEncryptParam() fallback.
Parameter ownership, SPI hardening (reachability of the legacy encrypt/decrypt, per-file key generation, nonce guidance), and a few nits.
The reason will be displayed to describe this comment to others. Learn more.
The AAD binds a page to (fileCryptoId, chunkOrdinal, pageIndex, sizes), but nothing binds a chunk to its series.
measurementId, dataType, encoding and compressionType (and the device) are not in the associated data. The ordinal lives in the unauthenticated chunk header, so it moves together with the chunk.
Repro: write d1.s1 = 11 and d1.s2 = 22 (PLAIN/UNCOMPRESSED, one point each, so both chunks are 79 bytes). Swap the two chunks' whole byte ranges (header + pages) in the file. Querying d1.s1 returns 22, with no authentication error.
Truncating trailing pages or chunks, or editing the plaintext statistics so pages get skipped, also goes undetected. testSwappingEqualSizedPagesBetweenChunksFailsAuthentication only covers swapping pages between chunks.
Suggestion: put the fields that are fixed when a page is encrypted (dataType, encoding, compressionType, time/value column mask) into the AAD. measurementId and the device need more thought, because names can be remapped at flush time (measurementNameRemapper) and chunk writers don't know the device. Either way, please document that page AEAD does not provide chunk-level integrity (deletion, truncation, reordering).
The reason will be displayed to describe this comment to others. Learn more.
Addressed the whole-chunk swap repro in 2f2a452 with a separate position proof in encrypted Chunk Headers. Immediately before writing the header, the provider authenticates a domain/version, fileCryptoId, the actual absolute chunk-marker offset, and chunkOrdinal. Sequential and positional readers verify the proof before using the ordinal or page body.
Updated in ecb0b6b: the proof is no longer hardcoded to 28 bytes by TsFile. Each immutable provider profile declares its size through getChunkAuthenticationSize(); the encrypted Chunk Header stores the proof length and payload, and readers validate the length against the profile before reading the payload. The current TimechoDB SM4-GCM profile uses 28 bytes (12-byte IV plus 16-byte tag), but other profiles may declare a different bounded size. Tests cover variable-length serialization, a 32-byte proof through query/both read paths/recovery, and invalid or mismatched lengths.
Page AAD and page-buffer encryption timing remain unchanged. Scope clarification: this protects relocation of an existing chunk, not general metadata integrity. Measurement/device mapping, codec fields, statistics and indexes remain unauthenticated; pruning based on altered statistics, deletion/truncation and index remapping are not claimed to be detected by this design.
The reason will be displayed to describe this comment to others. Learn more.
With reference counting, provider cleanup only happens if every caller closes every Chunk, and existing callers don't.
Every Chunk built with a page-AEAD parameter now retain()s it (copying the plaintext key if the parameter carries one) and must be closed. Existing callers of readMemChunk don't close chunks. For example, IoTDB's ChunkCache caches the Chunk returned by readMemChunk and doesn't close it on eviction. With the cache disabled, it re-wraps the loaded chunk in constructChunk and never closes the original. In practice the count rarely reaches zero, so "the provider is closed after the last reference" doesn't hold. Meanwhile every downstream call site has to learn a new close contract.
Suggestion: decouple the provider context's lifetime from object lifetimes. For example, keep a bounded process-wide cache of IEncrypt keyed by (providerId, profileId, keyId, keyVersion, wrappedDataKey) that closes entries on eviction, and let Chunk hold only the immutable file encryption descriptor. Then Chunk doesn't need to be closeable, and unencrypted paths stay untouched.
The reason will be displayed to describe this comment to others. Learn more.
We are keeping explicit ownership for the new page-AEAD feature rather than adding a process-wide crypto cache to TsFile. Chunk retains the shared file-level provider context, and ChunkReader owns/releases the supplied chunk; retained chunks do not independently unwrap a key on each read. The plaintext path does not acquire crypto references. The consuming TimechoDB branch has been adapted: ChunkCache closes entries on eviction, closes temporary loaded chunks after creating retained query views, and handles eviction/retain races; query readers release their owned views. This downstream adaptation is required before enabling page AEAD. We do not claim that arbitrary unmodified downstream callers will release encrypted resources automatically.
The reason will be displayed to describe this comment to others. Learn more.
Closing a ChunkReader now destroys the encryption reference of the caller's Chunk.
Before this PR, close() here was a no-op. Now it closes the supplied chunk.
Repro: Chunk c = reader.readMemChunk(meta), read it through new ChunkReader(c), then close(). Afterwards c.getEncryptParam().isDestroyed() is true. Re-wrapping the chunk's data with its parameter, new Chunk(c.getHeader(), data, c.getEncryptParam()) (the ChunkCache#constructChunk pattern), throws Encryption parameters have been destroyed. That worked before this PR and still works for unencrypted chunks. It's a behavior change worth documenting, or avoiding with the approach suggested on Chunk.
Also, ChunkReader and AbstractAlignedChunkReader now own resources but aren't AutoCloseable, so try-with-resources can't be used.
The reason will be displayed to describe this comment to others. Learn more.
The ownership transfer for page-AEAD chunks is intentional and documented on ChunkReader: closing it releases the supplied chunk's reference. A cache must pass an independently retained Chunk view, not its own cache entry. The TimechoDB integration now follows that rule and closes cache-owned references on eviction. Plaintext chunks do not have this crypto lifetime requirement. In d391241, IChunkReader also extends AutoCloseable, so scalar and aligned readers can be used with try-with-resources.
The reason will be displayed to describe this comment to others. Learn more.
Please address the two inline findings before merging: page authentication failures can be silently converted into incomplete table-query results, and concurrent first use can fail while service-provider registration is still in progress. Both issues were reproduced locally on b58e5c3; the affected code paths are unchanged in fc88fb8.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9ca3576. Both default authenticateChunk and verifyChunkAuthentication failures now include getClass().getName() in the localized exception message. Added EncryptionProviderRegistryTest.testUnsupportedChunkAuthenticationNamesImplementation to verify both fail-closed paths identify the implementation. The targeted run passed 99 TsFile tests and 5 common MessagesTest cases.
The reason will be displayed to describe this comment to others. Learn more.
Updated in 9ca3576 to the enhanced switch syntax with grouped known-marker cases and an explicit default rejection. The selector is a byte rather than an enum, so this does not provide enum-style compile-time exhaustiveness checking; unknown markers still fail closed at runtime. The targeted encryption/header/query/recovery and read-utility tests passed.
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9ca3576: EncryptException is now imported normally and the catch clause no longer uses its fully qualified name. Spotless and the targeted test run passed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Motivation
TsFile currently has legacy encryption interfaces but no self-describing file-level context for pluggable page-level authenticated encryption. This change establishes the format and I/O integration points while leaving algorithm and key-management implementations to external providers.
Compatibility
Validation
./mvnw spotless:check -P with-java -pl java/tsfile -am./mvnw test -P with-java -pl java/tsfile -am -Dtest='PageCryptoContextTest,FileEncryptionHeaderTest,TDEPageAeadTsFileTest,UnClosedTsFileReaderTest,ForceAppendTsFileWriterTest,RestorableTsFileIOWriterTest,TimePageWriterTest' -Dsurefire.failIfNoSpecifiedTests=falseDraft discussion points