Skip to content

feat(storage): at-rest encryption for node content via key providers - #59

Merged
Patel230 merged 3 commits into
mainfrom
feat/storage-encryption
Aug 15, 2026
Merged

feat(storage): at-rest encryption for node content via key providers#59
Patel230 merged 3 commits into
mainfrom
feat/storage-encryption

Conversation

@Patel230

Copy link
Copy Markdown
Contributor

Phase 2 item 4: migration v5's encrypted/encryption_key_version columns are now wired to a real key provider.

Design (app-layer, not SQLCipher — the pure-Go modernc.org/sqlite driver has no encryption hooks; a separate driver is out of scope):

  • KeyProvider interface: versioned master key material. Ship a NewEnvKeyProvider(var) that sources the secret from the process environment — nothing touches disk.
  • NodeCipher: AES-256-GCM; per-version keys derived via HKDF-SHA256 (deterministic, never stored). Ciphertext format yaad.aes256gcm.v{n}.<base64url(nonce||seal)> with random nonces (no determinism leak) and the key version bound as AAD — relabeled ciphertexts fail authentication.
  • Store.EnableEncryption(provider) after NewStore; idempotent, eagerly validated (missing env var fails at setup, not first write). Threads through every node content read/write path (createNodeQ, updateNodeQ, updateNodeContentQ, scans, batch, neighbors, by-hash, versions) plus txStore via the shared *Q helpers. Bookkeeping columns set in lockstep.
  • node_versions history encrypts; RollbackToVersion/DiffVersions decrypt before use (no double-encryption).
  • Legacy boundary: plaintext rows written pre-enable stay readable (prefix check) and upgrade to ciphertext on their next update — progressive migration, no bulk rewrite needed.
  • SearchNodes fallback: FTS5 indexes the stored (ciphertext) column, so keyword search would silently miss; with a cipher active, search degrades to an in-memory decrypted token scan (OR semantics, match-count ranking). Fine at yaad graph sizes; documented.
  • Default behaviour unchanged — no provider = plaintext, bookkeeping false/0.

Tests: round trip + nonce randomization, tamper/wrong-key/relabel rejection, env provider, legacy-plaintext boundary + progressive re-encrypt, tx path, FTS fallback. Full go test ./... + -race green; lint/fmt clean on linux/amd64, linux/arm64, windows/amd64. go directive bumped to 1.26.6 (stdlib CVEs per govulncheck).

migration v5's columns are now wired to a real key provider:

- KeyProvider interface: versioned master key material; EnvKeyProvider
  reads it from a process environment variable (no secret on disk)
- NodeCipher: AES-256-GCM with per-version keys derived via HKDF-SHA256;
  ciphertexts carry 'yaad.aes256gcm.v{n}.' prefix, random nonce, and the
  key version as authenticated data (relabeling/tampering fails reads)
- Store.EnableEncryption(opts in after NewStore) threads the cipher
  through every node read/write path, txStore included, and bookkeeps
  nodes.encrypted / encryption_key_version
- node_versions history encrypts alongside live content
- legacy plaintext rows stay readable after enabling encryption and are
  progressively re-encrypted on update
- SearchNodes falls back to an in-memory token scan over decrypted
  content when the cipher is active (FTS5 indexes ciphertext)
- default behaviour unchanged: no provider = plaintext, columns false/0
- go directive -> 1.26.6 (stdlib CVEs per govulncheck)

tests: round trip, tamper + wrong-key rejection, env provider, legacy
plaintext boundary, tx path, FTS fallback; full suite + race green
@Patel230
Patel230 merged commit ab5e649 into main Aug 15, 2026
12 checks passed
@Patel230
Patel230 deleted the feat/storage-encryption branch August 15, 2026 15:40
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