Skip to content

feat: block-level encryption at rest#71

Merged
polaz merged 26 commits into
mainfrom
feat/#20-feat-block-level-encryption-at-rest
Mar 22, 2026
Merged

feat: block-level encryption at rest#71
polaz merged 26 commits into
mainfrom
feat/#20-feat-block-level-encryption-at-rest

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Mar 22, 2026

Summary

  • Add pluggable EncryptionProvider trait for block-level encryption at rest
  • Ship AES-256-GCM implementation behind encryption feature flag (aes-gcm crate)
  • Encrypt all block types (data, index, filter, meta, range tombstone) after compression, before checksumming
  • Thread encryption through Config → Writer → sub-writers → recovery → read path

Upstream Reference

fjall-rs#224

Design

Pipeline: raw data → compress → encrypt → checksum → disk (reverse on read)

Checksums protect encrypted bytes on disk, so corruption is detected cheaply before any decryption attempt. Per-block overhead: 28 bytes (12-byte random nonce + 16-byte GCM auth tag).

API:

use lsm_tree::{Config, Aes256GcmProvider};

let encryption = Arc::new(Aes256GcmProvider::new(&key));
let tree = Config::new(path, seqno, visible_seqno)
    .with_encryption(Some(encryption))
    .open()?;

The EncryptionProvider trait is always available (no feature gate); only the built-in Aes256GcmProvider requires encryption feature. Custom providers (hardware KMS, envelope encryption) can implement the trait directly.

Test Plan

  • 9 unit tests for EncryptionProvider / Aes256GcmProvider (roundtrip, wrong key, tamper, truncation)
  • 3 integration tests: encrypted write→flush→read roundtrip, roundtrip with LZ4 compression, on-disk confidentiality verification (plaintext absent from encrypted SST)
  • 427 existing unit tests pass (0 regressions)
  • 727 total tests across all test binaries pass
  • Clippy clean (0 new warnings)
  • Builds with and without encryption feature

Closes #20

Summary by CodeRabbit

  • New Features

    • Optional block-level encryption-at-rest (feature-gated) with a pluggable provider and config API; AES-256-GCM provider provided. Writers and table I/O now propagate encryption so on-disk blocks can be encrypted.
  • Error Handling

    • New encrypt/decrypt error variants surface encryption/decryption failures.
  • Tests

    • Integration and unit tests for encryption roundtrips, ciphertext vs plaintext on-disk checks, and tamper-detection.

Copilot AI review requested due to automatic review settings March 22, 2026 09:54
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional block-level encryption-at-rest: a pluggable EncryptionProvider and AES‑GCM impl; threads encryption through block read/write, table recovery, writers, indices, scanner, and tree/ingest/compaction paths; block I/O now compress→encrypt→checksum on write and checksum→decrypt→decompress on read.

Changes

Cohort / File(s) Summary
Build / crate root
Cargo.toml, src/lib.rs
New encryption Cargo feature and optional aes-gcm dependency; add pub mod encryption and re-export EncryptionProvider (and feature-gated Aes256GcmProvider).
Encryption core & errors
src/encryption.rs, src/error.rs
New EncryptionProvider trait, AES‑GCM provider (feature-gated) with tests; added Error::Encrypt and Error::Decrypt.
Configuration
src/config/mod.rs
Config gains pub(crate) encryption: Option<Arc<dyn EncryptionProvider>>, with_encryption(...) builder, default None.
Block I/O core
src/table/block/mod.rs, src/table/util.rs, src/table/meta.rs, src/vlog/blob_file/meta.rs
Block APIs extended to accept encryption: Option<&dyn EncryptionProvider>; write path compress→encrypt→checksum and read path verify→decrypt→decompress; size checks account for encryption overhead.
Table internals & recovery
src/table/mod.rs, src/table/inner.rs, src/table/tests.rs
Table::recover and helpers accept/propagate encryption; Inner stores provider; tests updated to pass None where appropriate.
Index, iterators & scanner
src/table/block_index/two_level.rs, src/table/block_index/volatile.rs, src/table/iter.rs, src/table/scanner.rs
Index structures, iterators, and scanner carry optional encryption and forward it to block loads.
Writers & multiwriter
src/table/writer/mod.rs, src/table/multi_writer.rs, src/table/writer/*
Writers gain encryption fields and use_encryption(...) builder methods; MultiWriter propagates encryption on rotation; block writes pass encryption.
Filters & index writers
src/table/writer/filter/*, src/table/writer/index/*
Filter and index writer traits/impls extended with use_encryption(...); per-writer encryption state added and used during block writes.
Tree / ingestion / compaction / blob-tree
src/tree/mod.rs, src/tree/ingest.rs, src/blob_tree/mod.rs, src/blob_tree/ingest.rs, src/compaction/flavour.rs
Tree/blob-tree/ingest/compaction configure MultiWriter with config.encryption and pass encryption into Table::recover for flushed files.
Tests / integration
tests/encryption_roundtrip.rs, src/table/tests.rs
New integration tests (feature-gated) exercising AES‑GCM roundtrips, on-disk differences, and tamper detection; unit tests updated for new signatures.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Tree as Tree / Writer
    participant BlockIO as Block I/O
    participant Encryptor as EncryptionProvider
    participant Disk as Disk

    Note over App,Disk: Write path (compress → encrypt → checksum)
    App->>Tree: write(key, value)
    Tree->>BlockIO: Block::write_into(data, compression, encryption)
    BlockIO->>BlockIO: compress(data)
    BlockIO->>Encryptor: encrypt(compressed)
    Encryptor-->>BlockIO: nonce + ciphertext + tag
    BlockIO->>BlockIO: checksum(ciphertext)
    BlockIO->>Disk: write(header + ciphertext + checksum)

    Note over App,Disk: Read path (checksum → decrypt → decompress)
    App->>Tree: read(key)
    Tree->>BlockIO: Block::from_file(handle, compression, encryption)
    BlockIO->>Disk: read(header + ciphertext + checksum)
    BlockIO->>BlockIO: verify_checksum(ciphertext)
    BlockIO->>Encryptor: decrypt(nonce + ciphertext + tag)
    Encryptor-->>BlockIO: plaintext
    BlockIO->>BlockIO: decompress(plaintext)
    BlockIO-->>Tree: block data
    Tree-->>App: value
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐇 I tuck bytes beneath the mulch and humm,

nonces hop, and tags go drum,
I press the carrot, seal each crumb,
Burrows sleep safe on disk below,
A rabbit guards the data glow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: block-level encryption at rest' directly and concisely summarizes the main change, which is the addition of block-level encryption at rest functionality to the codebase.
Linked Issues check ✅ Passed The PR comprehensively implements all coding objectives from linked issue #20: provides a pluggable EncryptionProvider trait, includes AES-256-GCM implementation, encrypts all block types through the storage pipeline, properly sequences encryption after compression, and threads it through Config/Writer/recovery/read paths.
Out of Scope Changes check ✅ Passed All changes are in-scope: encryption infrastructure, trait definitions, trait implementations across block writers/readers, config threading, and integration tests. No unrelated refactoring, unplanned features, or extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#20-feat-block-level-encryption-at-rest

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds block-level encryption-at-rest support to the LSM-table block pipeline (encrypt after compression, before checksumming) via a pluggable EncryptionProvider, plus a feature-gated AES-256-GCM provider, and threads encryption through writers/readers and config.

Changes:

  • Introduce EncryptionProvider trait and Aes256GcmProvider (behind encryption feature) and expose them via the crate API.
  • Extend SST block read/write paths to support encrypt/decrypt while keeping checksums over on-disk (encrypted) bytes.
  • Thread encryption through Config, table writers (data/index/filter/meta/range tombstone), and the table read path; add integration tests for encrypted roundtrips and on-disk confidentiality.

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
Cargo.toml Adds encryption feature and optional aes-gcm dependency.
src/lib.rs Exposes encryption module, re-exports EncryptionProvider and Aes256GcmProvider.
src/error.rs Adds Encrypt / Decrypt error variants for encryption failures.
src/encryption.rs New encryption module: provider trait + AES-256-GCM implementation and unit tests.
src/config/mod.rs Adds Config.encryption and builder with_encryption(...).
src/table/block/mod.rs Implements encrypt/decrypt in block write/read pipeline and updates call sites in tests.
src/table/util.rs Threads encryption through load_block(...) into Block::from_file(...).
src/table/inner.rs Stores encryption provider in table inner state.
src/table/mod.rs Passes encryption into table iter creation and various block reads during recovery/open.
src/table/meta.rs Threads encryption into meta-block loading.
src/table/iter.rs Propagates encryption into the table iterator and block loads.
src/table/scanner.rs Updates scanner block reads to new Block::from_reader signature (currently no encryption).
src/table/multi_writer.rs Adds encryption propagation across rotated table writers.
src/table/writer/mod.rs Adds encryption to Writer and forwards to index/filter writers + block writes.
src/table/writer/index/mod.rs Extends index-writer trait to accept encryption provider.
src/table/writer/index/full.rs Implements encryption-aware full index writer.
src/table/writer/index/partitioned.rs Implements encryption-aware partitioned index writer.
src/table/writer/filter/mod.rs Extends filter-writer trait to accept encryption provider.
src/table/writer/filter/full.rs Implements encryption-aware full filter writer.
src/table/writer/filter/partitioned.rs Implements encryption-aware partitioned filter writer.
src/table/block_index/volatile.rs Threads encryption into volatile block-index iter reads.
src/table/block_index/two_level.rs Threads encryption into two-level block-index iter reads.
src/table/tests.rs Updates table tests to pass None encryption to updated APIs.
src/tree/mod.rs Threads config encryption into writer construction and recovery.
src/tree/ingest.rs Threads encryption through ingestion writers and table recovery.
src/compaction/flavour.rs Ensures compaction-prepared writers and recovered tables carry encryption.
src/blob_tree/mod.rs Threads encryption through blob-tree’s underlying table writer and recovery.
src/blob_tree/ingest.rs Threads encryption into blob-tree ingest recovery.
src/vlog/blob_file/meta.rs Explicitly sets blob-file meta blocks as unencrypted and updates Block API calls.
tests/encryption_roundtrip.rs Adds integration tests for encrypted write→flush→read and on-disk confidentiality.

Comment thread src/encryption.rs
Comment thread src/table/block/mod.rs Outdated
Comment thread src/table/block/mod.rs Outdated
Comment thread src/table/scanner.rs Outdated
Comment thread src/config/mod.rs Outdated
Comment thread src/encryption.rs
@polaz polaz force-pushed the feat/#20-feat-block-level-encryption-at-rest branch 2 times, most recently from 04a2b03 to c77931f Compare March 22, 2026 10:32
@polaz polaz requested a review from Copilot March 22, 2026 10:53
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Mar 22, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

@polaz Sure! I'll re-review the PR now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/lsm-tree PR: 12
File: src/table/block/mod.rs:109-119
Timestamp: 2026-03-15T16:10:30.764Z
Learning: In `structured-world/lsm-tree`, pre-allocation bounds checks on `uncompressed_length`, `data_length`, `handle.size()`, and `real_val_len` (256 MiB cap, returning `DecompressedSizeTooLarge`) are intentionally deferred to PR `#7` (branch `feat/#258-security-validate-uncompressedlength-before-decomp`, closes issues `#258` and `#257`). Do not flag missing size-cap guards in `Block::from_reader`, `Block::from_file`, or `blob_file::Reader::get` as unaddressed — they are handled in that separate security PR.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in a separate issue — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in issue `#31` — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: CR
Repo: structured-world/lsm-tree PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-20T22:03:44.692Z
Learning: Applies to **/*.rs : When adding validation for on-disk data, add a test that tampers the relevant field and asserts the error. Use the same serialization path as production (e.g., `lz4_flex::compress` not `compress_prepend_size`).

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:06.289Z
Learning: In `structured-world/lsm-tree`, the switch from `Mutex<IntervalTree>` to `RwLock<IntervalTree>` (or a freeze-on-seal immutable structure) for memtable range-tombstone storage in `src/memtable/mod.rs` is intentionally deferred from PR `#21` to a separate issue (`#30`). Do not flag the existing `Mutex<IntervalTree>` usage in `src/memtable/mod.rs` as a concurrency concern in the context of PR `#21`; it is tracked in issue `#30`.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T13:24:43.905Z
Learning: In the lsm-tree repository, the `unsafe { Slice::builder_unzeroed }` pattern used in lz4 decompression is pre-existing upstream code (not introduced by any feature PR). It is tracked for a separate fix in issue `#2`. The zstd decompression path uses the safe `Vec`-based approach instead.

Learnt from: CR
Repo: structured-world/lsm-tree PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-20T22:03:44.692Z
Learning: Applies to src/table/block/**/*.rs : `src/table/block/` — On-disk block format (header + compressed payload)
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/table/block/mod.rs:136

  • Block::write_into caps only the plaintext (data.len()) to MAX_DECOMPRESSION_SIZE, but it never validates the final on-disk payload length after compression + optional encryption. If compression expands (or a provider adds overhead), header.data_length can exceed the read-path limit (MAX_DECOMPRESSION_SIZE + ENCRYPTION_OVERHEAD), producing blocks that will later be rejected as too large. Add a post-compress/post-encrypt check that payload.len() stays within the same bound enforced by from_reader/from_file (or compute the bound from the provider).
        // Encrypt the compressed payload if an encryption provider is configured.
        // The encrypted bytes replace the compressed bytes on disk; checksums
        // cover the encrypted form so corruption is detected before decryption.
        let encrypted_buf: Option<Vec<u8>>;
        let payload: &[u8] = if let Some(enc) = encryption {
            encrypted_buf = Some(enc.encrypt(payload)?);

            #[expect(clippy::expect_used, reason = "encrypted_buf was just assigned")]
            encrypted_buf.as_ref().expect("just assigned")
        } else {
            encrypted_buf = None;
            let _ = &encrypted_buf; // suppress unused warning
            payload
        };

        #[expect(clippy::cast_possible_truncation, reason = "blocks are limited to u32")]
        {
            header.data_length = payload.len() as u32;
            header.checksum = Checksum::from_raw(crate::hash::hash128(payload));
        }

Comment thread src/table/block/mod.rs Outdated
Comment thread src/encryption.rs
Comment thread src/vlog/blob_file/meta.rs Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/encryption_roundtrip.rs (1)

89-145: Consider adding a decryption-failure test.

Per coding guidelines, validation tests should include tampering scenarios. A test that corrupts ciphertext and asserts a decryption error would strengthen coverage.

Would you like me to draft a test that:

  1. Writes encrypted data to disk
  2. Tampers with the SST bytes (e.g., flips bits in the ciphertext)
  3. Asserts the read fails with a checksum or decryption error

This would verify the integrity guarantees of the encryption pipeline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/encryption_roundtrip.rs` around lines 89 - 145, Add a new test (e.g.,
encrypted_data_tamper_fails) that reuses the same setup as
encrypted_data_differs_on_disk: create dir_encrypted, Aes256GcmProvider via
test_key(), open a tree with
make_config(...).with_encryption(Some(encryption)).insert the same value and
flush_active_memtable(0); then locate the SST with
find_table_file(dir_encrypted.path()), drop/close the tree to ensure files are
written, read the SST bytes, corrupt them (flip a few bits or change a
contiguous range), write the corrupted bytes back to disk, reopen the tree with
make_config(...).with_encryption(Some(encryption)).open() and call
tree_enc.get(b"secret", u64::MAX) expecting a failure (Err) due to
decryption/checksum error — assert that get returns an Err (or matches the
decryption/checksum error variant your crate exposes) rather than Ok(Some(_)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/encryption_roundtrip.rs`:
- Around line 89-145: Add a new test (e.g., encrypted_data_tamper_fails) that
reuses the same setup as encrypted_data_differs_on_disk: create dir_encrypted,
Aes256GcmProvider via test_key(), open a tree with
make_config(...).with_encryption(Some(encryption)).insert the same value and
flush_active_memtable(0); then locate the SST with
find_table_file(dir_encrypted.path()), drop/close the tree to ensure files are
written, read the SST bytes, corrupt them (flip a few bits or change a
contiguous range), write the corrupted bytes back to disk, reopen the tree with
make_config(...).with_encryption(Some(encryption)).open() and call
tree_enc.get(b"secret", u64::MAX) expecting a failure (Err) due to
decryption/checksum error — assert that get returns an Err (or matches the
decryption/checksum error variant your crate exposes) rather than Ok(Some(_)).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2214e520-0313-41fb-baf1-80722c4a0d23

📥 Commits

Reviewing files that changed from the base of the PR and between c190077 and 90fba8e.

📒 Files selected for processing (30)
  • Cargo.toml
  • src/blob_tree/ingest.rs
  • src/blob_tree/mod.rs
  • src/compaction/flavour.rs
  • src/config/mod.rs
  • src/encryption.rs
  • src/error.rs
  • src/lib.rs
  • src/table/block/mod.rs
  • src/table/block_index/two_level.rs
  • src/table/block_index/volatile.rs
  • src/table/inner.rs
  • src/table/iter.rs
  • src/table/meta.rs
  • src/table/mod.rs
  • src/table/multi_writer.rs
  • src/table/scanner.rs
  • src/table/tests.rs
  • src/table/util.rs
  • src/table/writer/filter/full.rs
  • src/table/writer/filter/mod.rs
  • src/table/writer/filter/partitioned.rs
  • src/table/writer/index/full.rs
  • src/table/writer/index/mod.rs
  • src/table/writer/index/partitioned.rs
  • src/table/writer/mod.rs
  • src/tree/ingest.rs
  • src/tree/mod.rs
  • src/vlog/blob_file/meta.rs
  • tests/encryption_roundtrip.rs

@polaz polaz force-pushed the feat/#20-feat-block-level-encryption-at-rest branch from 90fba8e to 3120e83 Compare March 22, 2026 11:35
@polaz polaz requested a review from Copilot March 22, 2026 11:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.

Comment thread src/encryption.rs
Comment thread src/table/block/mod.rs
Comment thread src/table/block/mod.rs
Comment thread src/table/block/mod.rs
@polaz polaz force-pushed the feat/#20-feat-block-level-encryption-at-rest branch from ddac1e5 to 62391e4 Compare March 22, 2026 12:18
@polaz polaz requested a review from Copilot March 22, 2026 12:19
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Mar 22, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

@polaz Sure! I'll re-review the PR now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/lsm-tree PR: 12
File: src/table/block/mod.rs:109-119
Timestamp: 2026-03-15T16:10:30.764Z
Learning: In `structured-world/lsm-tree`, pre-allocation bounds checks on `uncompressed_length`, `data_length`, `handle.size()`, and `real_val_len` (256 MiB cap, returning `DecompressedSizeTooLarge`) are intentionally deferred to PR `#7` (branch `feat/#258-security-validate-uncompressedlength-before-decomp`, closes issues `#258` and `#257`). Do not flag missing size-cap guards in `Block::from_reader`, `Block::from_file`, or `blob_file::Reader::get` as unaddressed — they are handled in that separate security PR.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T14:52:37.071Z
Learning: In `structured-world/lsm-tree`, the lz4 decompression logic (safe `Vec`-based buffer + `lz4_flex::decompress_into` + byte-count validation) is intentionally kept inline at both call sites (`Block::from_reader` and `Block::from_file` in `src/table/block/mod.rs`, and `Reader::get` in `src/vlog/blob_file/reader.rs`) rather than extracted into a `decompress_lz4_checked` helper. Reason: only 2–3 call sites exist, they return different container types, and inline is considered clearer. Revisit only if a third call site appears.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.724Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path (lines ~749–759) uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to a separate follow-up issue. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until that issue is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T13:24:43.905Z
Learning: In the lsm-tree repository, the `unsafe { Slice::builder_unzeroed }` pattern used in lz4 decompression is pre-existing upstream code (not introduced by any feature PR). It is tracked for a separate fix in issue `#2`. The zstd decompression path uses the safe `Vec`-based approach instead.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.724Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to issue `#72`. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until issue `#72` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 23
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T00:34:54.145Z
Learning: In `structured-world/lsm-tree`, the lint suppression policy distinguishes between legacy and new code:
- Existing/upstream `#[allow(clippy::...)]` in **unchanged** code: treat as legacy — do **not** flag it in reviews.
- New code introduced in a PR **must** use `#[expect(clippy::...)]` with a justification reason.
This avoids churning upstream code while enforcing the `#[expect]` standard for all new contributions.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in a separate issue — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in issue `#31` — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:06.289Z
Learning: In `structured-world/lsm-tree`, the switch from `Mutex<IntervalTree>` to `RwLock<IntervalTree>` (or a freeze-on-seal immutable structure) for memtable range-tombstone storage in `src/memtable/mod.rs` is intentionally deferred from PR `#21` to a separate issue (`#30`). Do not flag the existing `Mutex<IntervalTree>` usage in `src/memtable/mod.rs` as a concurrency concern in the context of PR `#21`; it is tracked in issue `#30`.

Learnt from: CR
Repo: structured-world/lsm-tree PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-20T22:03:44.692Z
Learning: Applies to src/table/block/**/*.rs : `src/table/block/` — On-disk block format (header + compressed payload)
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

src/table/block/mod.rs:133

  • Block::write_into only validates the plaintext size (data.len()), but after compression/encryption the on-disk payload can exceed the MAX_DECOMPRESSION_SIZE (+ overhead) cap that from_reader/from_file enforce. In the worst case this can produce SST blocks that the read path will reject (or that exceed the intended allocation cap). Consider validating payload.len() against the same limit after compression/encryption (including provider overhead) and returning DecompressedSizeTooLarge (or a dedicated error) on overflow.
        // Encrypt the compressed payload if an encryption provider is configured.
        // The encrypted bytes replace the compressed bytes on disk; checksums
        // cover the encrypted form so corruption is detected before decryption.
        let encrypted_buf: Option<Vec<u8>>;
        let payload: &[u8] = if let Some(enc) = encryption {
            encrypted_buf = Some(enc.encrypt(payload)?);

            #[expect(clippy::expect_used, reason = "encrypted_buf was just assigned")]
            encrypted_buf.as_ref().expect("just assigned")
        } else {
            encrypted_buf = None;
            let _ = &encrypted_buf; // suppress unused warning
            payload
        };

        #[expect(clippy::cast_possible_truncation, reason = "blocks are limited to u32")]
        {
            header.data_length = payload.len() as u32;
            header.checksum = Checksum::from_raw(crate::hash::hash128(payload));
        }

Comment thread src/table/block/mod.rs Outdated
Comment thread src/encryption.rs
Comment thread src/encryption.rs
@polaz polaz requested a review from Copilot March 22, 2026 12:54
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Mar 22, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

@polaz Sure! I'll re-review the PR now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/lsm-tree PR: 12
File: src/table/block/mod.rs:109-119
Timestamp: 2026-03-15T16:10:30.764Z
Learning: In `structured-world/lsm-tree`, pre-allocation bounds checks on `uncompressed_length`, `data_length`, `handle.size()`, and `real_val_len` (256 MiB cap, returning `DecompressedSizeTooLarge`) are intentionally deferred to PR `#7` (branch `feat/#258-security-validate-uncompressedlength-before-decomp`, closes issues `#258` and `#257`). Do not flag missing size-cap guards in `Block::from_reader`, `Block::from_file`, or `blob_file::Reader::get` as unaddressed — they are handled in that separate security PR.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.724Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path (lines ~749–759) uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to a separate follow-up issue. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until that issue is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T14:52:37.071Z
Learning: In `structured-world/lsm-tree`, the lz4 decompression logic (safe `Vec`-based buffer + `lz4_flex::decompress_into` + byte-count validation) is intentionally kept inline at both call sites (`Block::from_reader` and `Block::from_file` in `src/table/block/mod.rs`, and `Reader::get` in `src/vlog/blob_file/reader.rs`) rather than extracted into a `decompress_lz4_checked` helper. Reason: only 2–3 call sites exist, they return different container types, and inline is considered clearer. Revisit only if a third call site appears.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.724Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to issue `#72`. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until issue `#72` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T13:24:43.905Z
Learning: In the lsm-tree repository, the `unsafe { Slice::builder_unzeroed }` pattern used in lz4 decompression is pre-existing upstream code (not introduced by any feature PR). It is tracked for a separate fix in issue `#2`. The zstd decompression path uses the safe `Vec`-based approach instead.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 23
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T00:34:54.145Z
Learning: In `structured-world/lsm-tree`, the lint suppression policy distinguishes between legacy and new code:
- Existing/upstream `#[allow(clippy::...)]` in **unchanged** code: treat as legacy — do **not** flag it in reviews.
- New code introduced in a PR **must** use `#[expect(clippy::...)]` with a justification reason.
This avoids churning upstream code while enforcing the `#[expect]` standard for all new contributions.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:06.289Z
Learning: In `structured-world/lsm-tree`, the switch from `Mutex<IntervalTree>` to `RwLock<IntervalTree>` (or a freeze-on-seal immutable structure) for memtable range-tombstone storage in `src/memtable/mod.rs` is intentionally deferred from PR `#21` to a separate issue (`#30`). Do not flag the existing `Mutex<IntervalTree>` usage in `src/memtable/mod.rs` as a concurrency concern in the context of PR `#21`; it is tracked in issue `#30`.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 21
File: src/table/multi_writer.rs:129-206
Timestamp: 2026-03-20T11:14:02.461Z
Learning: In `structured-world/lsm-tree`, the `write_rts_to_writer` function in `src/table/multi_writer.rs` has a known limitation when `clip_range_tombstones=true` (compaction mode): RT fragments that don't intersect any output writer's KV span are silently dropped, which can lose delete-only gaps (e.g. an RT covering `[m,p)` while all output tables cover `[a,l]` and `[q,z]`). This limitation is intentionally deferred to issue `#32` and is documented as a Known Limitation in PR `#21`. Do not flag this as a blocking concern in PR `#21` or any PR until `#32` is addressed.

Learnt from: CR
Repo: structured-world/lsm-tree PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-20T22:03:44.692Z
Learning: Applies to **/*.rs : Before flagging any potential issue, trace the call chain visible in the PR diff. Check the caller to see if it handles the case, verify type-level guarantees, and read adjacent comments about deliberate design decisions.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in issue `#31` — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.835Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in a separate issue — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

Comment thread src/encryption.rs
@polaz polaz force-pushed the feat/#20-feat-block-level-encryption-at-rest branch from 434e55a to 88a85a4 Compare March 22, 2026 13:04
@polaz polaz requested a review from Copilot March 22, 2026 13:05
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Mar 22, 2026

@coderabbitai re-review

polaz added 11 commits March 22, 2026 19:24
Enforce MAX_DECOMPRESSION_SIZE on the final encrypted payload in
Block::write_into so writers cannot produce blocks that readers
would reject as too large.
…t Config field visibility

- Write-path payload check now uses MAX_DECOMPRESSION_SIZE + max_overhead()
  to match the read-path limit, preventing false rejections of encrypted
  blocks near the plaintext size limit
- Change Config.encryption from pub to pub(crate) to avoid semver-breaking
  struct literal construction; callers use with_encryption() builder
…e_into

Check payload length against MAX_DECOMPRESSION_SIZE + overhead using u64
arithmetic first, returning DecompressedSizeTooLarge. Then narrow to u32
for the header field. This avoids using Error::Encrypt for what is a
block format size violation, not an encryption failure.
- Blob meta comment now explicitly states that blob value frames are
  also unencrypted (not just metadata), and KV separation leaves large
  values in plaintext
- Config::with_encryption docs now note: encryption state not in SST
  metadata, blob files not covered by block-level encryption
- Add comment explaining max_overhead() validation strategy in write path
A provider with a very large max_overhead() could push max_payload
past u32::MAX, making the subsequent as-u32 cast truncate silently.
Clamp to u32::MAX to guarantee cast safety regardless of provider.
Align from_reader with from_file: compute enc_overhead as u64 and
compare header.data_length via u64::from(). Removes u32 cast,
debug_assert, and potential overflow risk from u32 addition.
… u32

Compile-time enforcement of the u32 contract instead of doc-only
invariant. Removes as-u64 casts in callers — u64::from(u32) is
infallible.
…turn

Add expect for usize→u32 cast in OVERHEAD constant (always 28).
Use u64::from() instead of as-cast for u32→u64 widening.
@polaz polaz force-pushed the feat/#20-feat-block-level-encryption-at-rest branch from 3779892 to 68d9e79 Compare March 22, 2026 17:28
@polaz polaz requested a review from Copilot March 22, 2026 17:30
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Mar 22, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

@polaz Sure! I'll re-review the PR now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/lsm-tree PR: 12
File: src/table/block/mod.rs:109-119
Timestamp: 2026-03-15T16:10:30.764Z
Learning: In `structured-world/lsm-tree`, pre-allocation bounds checks on `uncompressed_length`, `data_length`, `handle.size()`, and `real_val_len` (256 MiB cap, returning `DecompressedSizeTooLarge`) are intentionally deferred to PR `#7` (branch `feat/#258-security-validate-uncompressedlength-before-decomp`, closes issues `#258` and `#257`). Do not flag missing size-cap guards in `Block::from_reader`, `Block::from_file`, or `blob_file::Reader::get` as unaddressed — they are handled in that separate security PR.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T13:24:43.905Z
Learning: In the lsm-tree repository, the `unsafe { Slice::builder_unzeroed }` pattern used in lz4 decompression is pre-existing upstream code (not introduced by any feature PR). It is tracked for a separate fix in issue `#2`. The zstd decompression path uses the safe `Vec`-based approach instead.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-15T14:52:37.071Z
Learning: In `structured-world/lsm-tree`, the lz4 decompression logic (safe `Vec`-based buffer + `lz4_flex::decompress_into` + byte-count validation) is intentionally kept inline at both call sites (`Block::from_reader` and `Block::from_file` in `src/table/block/mod.rs`, and `Reader::get` in `src/vlog/blob_file/reader.rs`) rather than extracted into a `decompress_lz4_checked` helper. Reason: only 2–3 call sites exist, they return different container types, and inline is considered clearer. Revisit only if a third call site appears.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.749Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path (lines ~749–759) uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to a separate follow-up issue. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until that issue is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 21
File: src/table/multi_writer.rs:129-206
Timestamp: 2026-03-20T11:14:05.620Z
Learning: In `structured-world/lsm-tree`, the `write_rts_to_writer` function in `src/table/multi_writer.rs` has a known limitation when `clip_range_tombstones=true` (compaction mode): RT fragments that don't intersect any output writer's KV span are silently dropped, which can lose delete-only gaps (e.g. an RT covering `[m,p)` while all output tables cover `[a,l]` and `[q,z]`). This limitation is intentionally deferred to issue `#32` and is documented as a Known Limitation in PR `#21`. Do not flag this as a blocking concern in PR `#21` or any PR until `#32` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-22T09:55:39.749Z
Learning: In `structured-world/lsm-tree` (`src/compaction/leveled/mod.rs`), the multi-level compaction path uses a single aggregate key range across all L0+L1 inputs to query L2 overlapping tables. Using per-range L2 overlap queries (one per contiguous L1 range segment) instead would avoid pulling in L2 tables that sit in gaps between disjoint L1 ranges on sparse keyspaces. This improvement is intentionally deferred from PR `#66` to issue `#72`. Do not flag the coarse aggregate L2 range query as a blocking concern in PR `#66` or future PRs until issue `#72` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 21
File: src/table/writer/mod.rs:437-457
Timestamp: 2026-03-20T12:29:47.183Z
Learning: In `structured-world/lsm-tree` (`src/table/writer/mod.rs`), the synthetic weak-tombstone sentinel written for RT-only SSTs (when `item_count == 0` but `range_tombstones` is non-empty) intentionally increments `meta.tombstone_count`, `meta.weak_tombstone_count`, `meta.item_count`, and `meta.key_count`. All on-disk metadata counters must match actual block contents for recovery validation consistency (established in commit 7452427a for item_count/key_count; same reasoning applies to tombstone counters). The resulting +1 inflation on GC heuristics is harmless — it overestimates reclaimable space and triggers slightly earlier checks. Do not flag this as a metadata accuracy bug in PR `#21` or future PRs.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:06.298Z
Learning: In `structured-world/lsm-tree`, the switch from `Mutex<IntervalTree>` to `RwLock<IntervalTree>` (or a freeze-on-seal immutable structure) for memtable range-tombstone storage in `src/memtable/mod.rs` is intentionally deferred from PR `#21` to a separate issue (`#30`). Do not flag the existing `Mutex<IntervalTree>` usage in `src/memtable/mod.rs` as a concurrency concern in the context of PR `#21`; it is tracked in issue `#30`.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.855Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in a separate issue — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 0
File: :0-0
Timestamp: 2026-03-20T08:17:39.855Z
Learning: In `structured-world/lsm-tree`, the `Memtable` range-tombstone storage uses `Mutex<IntervalTree>` (introduced in PR `#21`). Switching to `RwLock<IntervalTree>` or a freeze-on-seal immutable structure is a deliberate future improvement tracked in issue `#31` — do not flag the `Mutex` usage as a blocking concern in PR `#21` or future PRs that reference this design.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 28
File: src/tree/mod.rs:913-956
Timestamp: 2026-03-22T03:09:14.362Z
Learning: In `structured-world/lsm-tree` (`src/tree/mod.rs`), the `resolve_merge_get` function's disk-scan loop in step 3 is a flat scan across all levels/runs and does not apply the L0-specific newest-first ordering used by `get_internal_entry_from_tables`. This means it can stop on a base/tombstone boundary from an older L0 run while a newer overlapping L0 run still has visible `MergeOperand` entries. This is a known limitation documented with `// TODO(`#46`)` in the code and is intentionally deferred to issue `#46`. Do not flag the flat L0 scan in `resolve_merge_get` as a merge-semantics bug in PR `#28` or future PRs until issue `#46` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 67
File: src/memtable/mod.rs:72-77
Timestamp: 2026-03-22T12:26:47.795Z
Learning: In `structured-world/lsm-tree` (`src/memtable/interval_tree.rs` and `src/memtable/mod.rs`), the memtable `IntervalTree` used for range tombstone containment queries (`is_key_suppressed_by_range_tombstone`, `query_suppression`, `insert_range_tombstone`) still uses lexicographic `Ord` on `UserKey` rather than the pluggable `SharedComparator` introduced in PR `#67`. Threading the comparator into the AVL interval tree is a non-trivial refactor intentionally deferred from PR `#67` and tracked in issue `#81`. Do not flag the absence of comparator-aware RT suppression in the memtable interval tree as a blocking concern in PR `#67` or future PRs until issue `#81` is addressed.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 23
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T00:34:54.145Z
Learning: In `structured-world/lsm-tree`, the lint suppression policy distinguishes between legacy and new code:
- Existing/upstream `#[allow(clippy::...)]` in **unchanged** code: treat as legacy — do **not** flag it in reviews.
- New code introduced in a PR **must** use `#[expect(clippy::...)]` with a justification reason.
This avoids churning upstream code while enforcing the `#[expect]` standard for all new contributions.

Learnt from: polaz
Repo: structured-world/lsm-tree PR: 71
File: src/encryption.rs:43-59
Timestamp: 2026-03-22T16:43:32.038Z
Learning: In `structured-world/lsm-tree` (`src/encryption.rs`), the `EncryptionProvider` trait's `encrypt` and `decrypt` methods intentionally accept only raw bytes (no AAD/block-context parameter) in PR `#71`. Adding AAD support (e.g., `encrypt(&self, plaintext: &[u8], aad: &[u8])`) to authenticate block identity/header fields is tracked as a follow-up improvement with a TODO at `src/encryption.rs` lines 142–147. Do not flag the absence of AAD parameters on `EncryptionProvider::encrypt`/`decrypt` as a blocking concern in PR `#71` or future PRs until that follow-up is addressed.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.

Comment thread src/encryption.rs Outdated
…mpliance

Convert encryption unit tests to return crate::Result<()> and use ?
instead of .expect() to satisfy clippy::expect_used with -D warnings.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.

@polaz polaz merged commit 56e3f1c into main Mar 22, 2026
19 checks passed
@polaz polaz deleted the feat/#20-feat-block-level-encryption-at-rest branch March 22, 2026 18:02
@sw-release-bot sw-release-bot Bot mentioned this pull request Mar 22, 2026
polaz added a commit that referenced this pull request Mar 22, 2026
Pass None for the new EncryptionProvider argument added in #71.
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.

feat: block-level encryption at rest

2 participants