Skip to content

feat: encryption at rest (AES-256-GCM) - #81

Merged
kacy merged 7 commits into
mainfrom
feat/encryption-at-rest
Feb 10, 2026
Merged

feat: encryption at rest (AES-256-GCM)#81
kacy merged 7 commits into
mainfrom
feat/encryption-at-rest

Conversation

@kacy

@kacy kacy commented Feb 10, 2026

Copy link
Copy Markdown
Owner

summary

adds optional encryption at rest for AOF and snapshot files using AES-256-GCM. controlled by a compile-time encryption cargo feature (off by default) and a runtime --encryption-key-file CLI flag. zero cost when the feature is not compiled in — no crypto dependencies linked, no branches on the hot path.

each AOF record and snapshot entry is encrypted independently with a random 12-byte nonce, enabling streaming I/O and isolated corruption recovery. encrypted files use a v3 format; existing v2 plaintext files remain readable and are transparently migrated on the next BGREWRITEAOF or BGSAVE.

key highlights:

  • feature flag chain: ember-persistence/encryptionemberkv-core/encryptionember-server/encryption
  • EncryptionKey type with redacted Debug output to prevent accidental key logging
  • key file accepts 32 raw bytes or 64 hex characters
  • FormatError::EncryptionRequired when opening v3 files without a key
  • FormatError::DecryptionFailed on wrong key or tampered data (AEAD guarantees)
  • backward-compatible: v2 files work with or without the feature compiled in

what was tested

  • cargo build --workspace — builds without encryption (default)
  • cargo build --workspace --features encryption — builds with encryption
  • cargo test --workspace — 886 tests pass (no encryption)
  • cargo test --workspace --features encryption — 906 tests pass (20 new encryption tests)
  • cargo clippy --workspace -- -D warnings — clean
  • cargo clippy --workspace --features encryption -- -D warnings — clean
  • cargo fmt --all --check — clean

new tests cover:

  • encryption round-trip, wrong key detection, tampered ciphertext detection
  • empty plaintext, unique nonces per call, key from raw/hex files
  • encrypted AOF write/read, v2 backward compat, v3 without key error, truncate preserving version
  • encrypted snapshot write/read, all data types, v2 compat, v3 without key error

design considerations

  • per-record encryption rather than full-file: preserves streaming I/O, corruption isolation, and AOF truncation semantics
  • AEAD (AES-256-GCM) provides both confidentiality and integrity — the 16-byte auth tag replaces CRC32 for tamper detection on encrypted records
  • v3 format uses [nonce: 12B][len: 4B][ciphertext] per record, where ciphertext includes the auth tag
  • separate open() / open_encrypted() methods rather than a single method with an Option<Key> parameter — avoids clippy unit_arg warnings from cfg-gated () arguments and makes the API explicit about encryption intent
  • EncryptionKeyRef type alias in recovery (&EncryptionKey or &()) provides clean cfg-gated function signatures without runtime overhead
  • key loss = data loss, inherent to encryption — clear error messages guide users

kacy added 7 commits February 10, 2026 09:29
adds an optional `encryption` feature flag backed by AES-256-GCM.
when enabled, provides:
- EncryptionKey type with from_file (raw 32B or 64 hex chars)
- encrypt_record / decrypt_record primitives
- FormatError::EncryptionRequired and DecryptionFailed variants
- FORMAT_VERSION_ENCRYPTED (v3) and version-gated header reading

the feature is off by default — no crypto deps are linked and no
encryption types exist in the API when disabled.
AofWriter gains open_encrypted() for v3 (AES-256-GCM) files.
each record is written as [nonce: 12B][len: 4B][ciphertext].
AofReader gains open_encrypted() that handles both v2 (plaintext)
and v3 (encrypted) files — enabling transparent migration.

v3 files opened without a key return FormatError::EncryptionRequired.
wrong keys are detected immediately via AEAD authentication.
SnapshotWriter gains create_encrypted() for v3 files. each entry is
encrypted independently as [nonce: 12B][len: 4B][ciphertext]. footer
CRC covers the encrypted bytes for truncation detection.

SnapshotReader gains open_encrypted() that handles both v2 (plaintext)
and v3 (encrypted) files transparently. wrong keys and tampered data
are detected via AEAD authentication.
recover_shard_encrypted() accepts an optional encryption key and
forwards it to snapshot reader and AOF reader. handles both v2
(plaintext) and v3 (encrypted) files transparently.
adds `encryption` feature flag to emberkv-core that forwards to
ember-persistence. ShardPersistenceConfig gains an optional
encryption_key field (cfg-gated). recovery, AOF writer, and
snapshot writer all use encrypted variants when a key is present.
the flag is only available when compiled with --features encryption.
reads a 32-byte key (raw or hex) from the given file path and passes
it through to shard persistence config. validates that persistence
is enabled when encryption is configured. logs when encryption is
active.
refactor cfg-gated code to avoid unit_arg/unused_unit clippy lints:
- aof: extract open_persistence_file helper, separate open/open_encrypted
- snapshot: extract open_tmp helper, separate create/create_encrypted
- recovery: use EncryptionKeyRef type alias for cleaner cfg gating
- encryption: use io::Error::other instead of deprecated constructor
@kacy
kacy merged commit 5549bb6 into main Feb 10, 2026
7 checks passed
@kacy
kacy deleted the feat/encryption-at-rest branch February 10, 2026 14:59
kacy added a commit that referenced this pull request Feb 11, 2026
* feat: add encryption module and v3 format support to ember-persistence

adds an optional `encryption` feature flag backed by AES-256-GCM.
when enabled, provides:
- EncryptionKey type with from_file (raw 32B or 64 hex chars)
- encrypt_record / decrypt_record primitives
- FormatError::EncryptionRequired and DecryptionFailed variants
- FORMAT_VERSION_ENCRYPTED (v3) and version-gated header reading

the feature is off by default — no crypto deps are linked and no
encryption types exist in the API when disabled.

* feat: add encryption support to AOF writer and reader

AofWriter gains open_encrypted() for v3 (AES-256-GCM) files.
each record is written as [nonce: 12B][len: 4B][ciphertext].
AofReader gains open_encrypted() that handles both v2 (plaintext)
and v3 (encrypted) files — enabling transparent migration.

v3 files opened without a key return FormatError::EncryptionRequired.
wrong keys are detected immediately via AEAD authentication.

* feat: add encryption support to snapshot writer and reader

SnapshotWriter gains create_encrypted() for v3 files. each entry is
encrypted independently as [nonce: 12B][len: 4B][ciphertext]. footer
CRC covers the encrypted bytes for truncation detection.

SnapshotReader gains open_encrypted() that handles both v2 (plaintext)
and v3 (encrypted) files transparently. wrong keys and tampered data
are detected via AEAD authentication.

* feat: pass encryption key through recovery

recover_shard_encrypted() accepts an optional encryption key and
forwards it to snapshot reader and AOF reader. handles both v2
(plaintext) and v3 (encrypted) files transparently.

* feat: forward encryption feature through emberkv-core

adds `encryption` feature flag to emberkv-core that forwards to
ember-persistence. ShardPersistenceConfig gains an optional
encryption_key field (cfg-gated). recovery, AOF writer, and
snapshot writer all use encrypted variants when a key is present.

* feat: add --encryption-key-file CLI flag to ember-server

the flag is only available when compiled with --features encryption.
reads a 32-byte key (raw or hex) from the given file path and passes
it through to shard persistence config. validates that persistence
is enabled when encryption is configured. logs when encryption is
active.

* fix: resolve clippy and formatting warnings for encryption feature

refactor cfg-gated code to avoid unit_arg/unused_unit clippy lints:
- aof: extract open_persistence_file helper, separate open/open_encrypted
- snapshot: extract open_tmp helper, separate create/create_encrypted
- recovery: use EncryptionKeyRef type alias for cleaner cfg gating
- encryption: use io::Error::other instead of deprecated constructor
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