Skip to content

Add encryption compress-then-encrypt path - #1110

Merged
bootjp merged 3 commits into
design/encryption-stage7-projectionfrom
feature/encryption-stage9-compression
Jul 19, 2026
Merged

Add encryption compress-then-encrypt path#1110
bootjp merged 3 commits into
design/encryption-stage7-projectionfrom
feature/encryption-stage9-compression

Conversation

@bootjp

@bootjp bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • compress storage plaintext with Snappy before AES-GCM when the result is smaller
  • authenticate and validate the compression flag, including fail-closed Raft flag handling
  • disable Pebble block compression for encryption-wired open and restore paths
  • add property, tamper, restore-policy, race, and paired performance coverage

Risk

The storage envelope gains a second valid flag value. Existing flag-zero envelopes remain readable. Unknown flag bits and nonzero Raft flags fail closed. Compression runs only after the storage-envelope cutover gate selects encrypted writes.

Caller audit

  • encryptForKey: all callers (PutAt, ExpireAt, direct and Raft ApplyMutations) propagate errors and share the same compression policy.
  • decryptForKey: GetAt, forward/reverse scans, ExistsAt, and delete-prefix visibility checks propagate integrity/decompression errors.
  • DecodeEnvelope: storage propagates decode errors; Raft now additionally rejects every nonzero flag.
  • Pebble open helpers: initial open, fresh reopen, native restore temp DB, streaming restore temp DB, and post-swap reopen all preserve the encrypted-store compression policy.

Verification

  • go test ./... -count=1 -timeout=20m
  • go test -race ./store -run 'TestEncryption_|TestCompressionRoundTripProperty|TestPebbleCompressionPolicy' -count=1 -timeout=15m\n- go test . -run 'Test.*Encryption|Test.*Sidecar|Test.*Snapshot' -count=1 -timeout=10m\n- go test ./adapter -run TestEncryptionAdmin -count=1 -timeout=10m\n- golangci-lint --allow-parallel-runners --config=.golangci.yaml run ./internal/encryption ./store --timeout=5m\n- paired 1 KiB benchmark, fixed 1000 iterations x 10: cleartext median ~119.7 us/op; encrypted median ~113.9 us/op\n- git diff --check\n- signed commit verified for bootjp <contact@bootjp.me>\n\n## Author\n\nbootjp

Summary by CodeRabbit

  • 新機能

    • 暗号化データをSnappyで圧縮し、圧縮効果がある場合のみ保存するようになりました。
    • 圧縮状態を含むデータの整合性検証を強化しました。
  • バグ修正

    • 未対応の暗号化フラグや改ざんされた圧縮データを安全に拒否するようになりました。
    • 暗号化ストアでは重複したストレージ圧縮を無効化し、保存効率を改善しました。
  • ドキュメント

    • Stage 9Aの実装完了範囲と、今後の対応項目を更新しました。

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6b03b011-6c0b-4f09-bfb9-f39a77a01664

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Stage 9A 暗号化圧縮

Layer / File(s) Summary
エンベロープとRAFTフラグ検証
internal/encryption/*
未知のエンベロープフラグを拒否するErrEnvelopeFlagを追加し、RAFT経路では圧縮フラグ付きエンベロープを拒否する。
暗号化ペイロードの圧縮と復号
store/encryption_glue.go, store/encryption_compression_test.go, go.mod
Snappyを圧縮後に暗号化し、圧縮フラグをAADで認証する。復号後は必要な場合のみ復元し、改ざん・不正ペイロード・ラウンドトリップを検証する。
Pebble圧縮ポリシーの配線
store/lsm_store.go, store/*test.go
暗号化ストアの作成、再オープン、スナップショット復元、DB交換時にPebbleブロック圧縮を無効化する。
Stage 9Aの記録とベンチマーク
docs/design/*, store/lsm_store_sync_mode_benchmark_test.go
Stage 9Aを完了、9B+を未完了として設計文書に記録し、平文・暗号化のRaft適用ベンチマークを追加する。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant EncryptedStore
  participant Pebble
  participant Snappy
  Client->>EncryptedStore: 値を書き込む
  EncryptedStore->>Snappy: 平文を圧縮する
  Snappy-->>EncryptedStore: 圧縮済みペイロードとフラグ
  EncryptedStore->>Pebble: フラグをAADに含めて暗号化値を保存する
  Client->>EncryptedStore: 値を読み取る
  EncryptedStore->>Pebble: 暗号化値を取得する
  EncryptedStore->>Snappy: 認証後のペイロードを復元する
  Snappy-->>Client: 元の平文を返す
Loading

Possibly related PRs

  • bootjp/elastickv#719: 本PRが拡張する暗号化エンベロープ実装を追加した。
  • bootjp/elastickv#809: 同じencryptForKey書き込み経路のエンベロープ処理を変更した。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 暗号化前に Snappy 圧縮する compress-then-encrypt 経路の追加を簡潔に表しており、変更内容と一致しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Stage 9A verification completed on the current signed head.

  • full Go suite passed, including adapter multi-node tests
  • storage race tests and encryption cutover/sidecar integration passed
  • lint passed with 0 issues
  • fixed-iteration paired benchmark stayed within the 10% gate
  • storage/Raft decode caller audit completed; errors remain fail-closed

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

@bootjp Thanks for the Stage 9A verification details. I’ll review #1110, with particular attention to the compression/encryption envelope invariants, fail-closed flag handling, and the Pebble compression-policy coverage.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements Stage 9A data-at-rest compression (compress-then-encrypt) using Snappy before AES-256-GCM encryption. Pebble block compression is disabled for encryption-wired stores to avoid redundant CPU usage on high-entropy ciphertext. Feedback on these changes suggests checking the decoded length of Snappy payloads against maxSnapshotValueSize before decompression to prevent potential out-of-memory (OOM) panics, and adding a fast-path threshold check to bypass compression for very small plaintexts to optimize write-path performance.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread store/encryption_glue.go Outdated
Comment on lines +423 to +430
if env.Flag&encryption.FlagCompressed != 0 {
plain, err = snappy.Decode(nil, plain)
if err != nil {
return nil, errors.Wrap(
errors.WithSecondaryError(ErrEncryptedReadCompression, err),
"store: decompress encrypted value")
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When decompressing the decrypted payload, snappy.Decode reads the uncompressed length prefix from the untrusted/corrupted payload and allocates a slice of that size. To prevent potential out-of-memory (OOM) panics from corrupted or malformed payloads, check snappy.DecodedLen against maxSnapshotValueSize before calling snappy.Decode.

	if env.Flag&encryption.FlagCompressed != 0 {
		dLen, err := snappy.DecodedLen(plain)
		if err != nil {
			return nil, errors.Wrap(
				errors.WithSecondaryError(ErrEncryptedReadCompression, err),
				"store: decompress encrypted value")
		}
		if dLen > maxSnapshotValueSize {
			return nil, errors.Wrap(
				errors.WithSecondaryError(ErrEncryptedReadCompression, errors.New("decoded length exceeds maximum value size")),
				"store: decompress encrypted value")
		}
		plain, err = snappy.Decode(nil, plain)
		if err != nil {
			return nil, errors.Wrap(
				errors.WithSecondaryError(ErrEncryptedReadCompression, err),
				"store: decompress encrypted value")
		}
	}
References
  1. When deserializing data with a size header, pre-allocating a buffer is acceptable if a strictly enforced maximum size limit is checked before allocation and the entire data must be materialized in memory. Avoid adding complexity with chunked reads if it doesn't meaningfully change the peak memory bound.

Comment thread store/encryption_glue.go
Comment on lines +446 to +452
func compressForEncryption(plaintext []byte) ([]byte, byte) {
compressed := snappy.Encode(nil, plaintext)
if len(compressed) >= len(plaintext) {
return plaintext, 0
}
return compressed, encryption.FlagCompressed
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

For very small plaintexts, Snappy compression is highly unlikely to yield a strictly smaller size due to the format's framing and varint overhead. Adding a fast-path threshold check avoids unnecessary snappy.Encode allocations and CPU cycles on the write path for small values.

Suggested change
func compressForEncryption(plaintext []byte) ([]byte, byte) {
compressed := snappy.Encode(nil, plaintext)
if len(compressed) >= len(plaintext) {
return plaintext, 0
}
return compressed, encryption.FlagCompressed
}
func compressForEncryption(plaintext []byte) ([]byte, byte) {
if len(plaintext) < 32 {
return plaintext, 0
}
compressed := snappy.Encode(nil, plaintext)
if len(compressed) >= len(plaintext) {
return plaintext, 0
}
return compressed, encryption.FlagCompressed
}

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: c2fda70b92

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
internal/encryption/envelope.go (1)

93-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

新しい境界エラーを github.com/cockroachdb/errors に統一してください。

3地点とも github.com/pkg/errors.Wrapf を追加しており、リポジトリの境界エラー規則と一致しません。

  • internal/encryption/envelope.go#L93-L96: Encode のフラグエラーを指定ライブラリでラップする。
  • internal/encryption/envelope.go#L123-L126: Decode のフラグエラーも同じ実装へ統一する。
  • internal/encryption/raft_envelope.go#L108-L111: Raft のフラグ拒否も同じエラーライブラリへ統一する。

As per coding guidelines, 「Wrap errors with github.com/cockroachdb/errors at boundaries in Go code」に従ってください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/encryption/envelope.go` around lines 93 - 96, Replace the
boundary-error wrapping in the flag validation paths with
github.com/cockroachdb/errors, covering the Encode validation in
internal/encryption/envelope.go:93-96, the Decode validation in
internal/encryption/envelope.go:123-126, and the Raft flag rejection in
internal/encryption/raft_envelope.go:108-111; preserve the existing error values
and messages.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@store/lsm_store_sync_mode_benchmark_test.go`:
- Line 113: Remove the //nolint directive from the startTS calculation in the
benchmark and add an explicit negative-boundary check for the loop index,
following the pattern used by BenchmarkApplyMutationsRaft_SyncMode to satisfy
gosec without suppressing the warning.

---

Nitpick comments:
In `@internal/encryption/envelope.go`:
- Around line 93-96: Replace the boundary-error wrapping in the flag validation
paths with github.com/cockroachdb/errors, covering the Encode validation in
internal/encryption/envelope.go:93-96, the Decode validation in
internal/encryption/envelope.go:123-126, and the Raft flag rejection in
internal/encryption/raft_envelope.go:108-111; preserve the existing error values
and messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a9de206-f939-4bfa-b57a-43031028e852

📥 Commits

Reviewing files that changed from the base of the PR and between cbc93a7 and c2fda70.

📒 Files selected for processing (14)
  • docs/design/2026_04_29_partial_data_at_rest_encryption.md
  • docs/design/2026_07_18_implemented_9a_encryption_compression.md
  • go.mod
  • internal/encryption/envelope.go
  • internal/encryption/envelope_test.go
  • internal/encryption/errors.go
  • internal/encryption/raft_envelope.go
  • internal/encryption/raft_envelope_test.go
  • store/encryption_compression_test.go
  • store/encryption_glue.go
  • store/lsm_store.go
  • store/lsm_store_env_test.go
  • store/lsm_store_sync_mode_benchmark_test.go
  • store/lsm_store_test.go

b.ResetTimer()
for i := 0; i < b.N; i++ {
key := []byte(fmt.Sprintf("enc-bench-%010d", i))
startTS := uint64(i) * 2 //nolint:gosec // benchmark loop index is non-negative.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

//nolintディレクティブを削除し、コードをリファクタリングしてください。

コーディングガイドラインに従い、Goのコードでは//nolintを使用せず、リファクタリングで対処する必要があります。同一ファイル内の BenchmarkApplyMutationsRaft_SyncMode で行われているように、負の境界チェックを明示的に行うことで gosec の警告を回避できます。

As per coding guidelines: **/*.go: Avoid using //nolint directives; refactor instead in Go code.

♻️ 提案する修正
-				startTS := uint64(i) * 2 //nolint:gosec // benchmark loop index is non-negative.
+				if i < 0 {
+					b.Fatalf("unexpected negative iteration counter: %d", i)
+				}
+				startTS := uint64(i) * 2
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
startTS := uint64(i) * 2 //nolint:gosec // benchmark loop index is non-negative.
if i < 0 {
b.Fatalf("unexpected negative iteration counter: %d", i)
}
startTS := uint64(i) * 2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@store/lsm_store_sync_mode_benchmark_test.go` at line 113, Remove the //nolint
directive from the startTS calculation in the benchmark and add an explicit
negative-boundary check for the loop index, following the pattern used by
BenchmarkApplyMutationsRaft_SyncMode to satisfy gosec without suppressing the
warning.

Source: Coding guidelines

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head compression findings in signed head 0b254061bc39c5cb7a6ef2cf13775b5d2dc25ab3.

  • checks Snappy decoded length against the existing 256 MiB value bound before allocating
  • rejects malformed/oversized authenticated payloads with ErrEncryptedReadCompression
  • skips Snappy below the 32-byte threshold
  • added threshold and authenticated oversized-payload regression tests
  • re-audited all decryptForKey callers; point reads, scans, existence checks, and prefix-delete visibility all propagate the fail-closed error
  • focused unit tests passed; focused race passed; package lint reports 0 issues; pre-commit lint reports 0 issues

@codex review
@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

@bootjp Thanks for the detailed update. I’ll review the updated signed head, focusing on the bounded Snappy decode path, authenticated malformed/oversized-payload handling, threshold behavior, and fail-closed propagation through all decryptForKey callers.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@bootjp

bootjp commented Jul 18, 2026

Copy link
Copy Markdown
Owner Author

Addressed the current-head benchmark finding in signed head 679d83fdd4c57056cda03c482dd5e32a1fa1679d. The new benchmark now uses the same explicit non-negative loop-index guard as the existing sync-mode benchmark and no longer suppresses gosec.

Evidence: benchmark executes both cleartext/encrypted subcases; golangci-lint ./store reports 0 issues; pre-commit lint reports 0 issues.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

Reviewed commit: 679d83fdd4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp
bootjp merged commit b3acb83 into design/encryption-stage7-projection Jul 19, 2026
6 checks passed
@bootjp
bootjp deleted the feature/encryption-stage9-compression branch July 19, 2026 06:04
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.

1 participant