Skip to content

feat: column-level encryption codec over fjall store (declarative encrypted-fields map) #132

@forkwright

Description

@forkwright

Context

Area

akroasis fjall codec layer — declarative, config-driven middleware that consults a static BTreeSet<(table, column)> map and applies ChaCha20-Poly1305 to specified fields before write / after read. Closes the gap where filesystem access to the fjall store reveals unencrypted signal metadata.

Severity

low — incremental hardening. Today akroasis has at-rest passphrase encryption on the fjall store but no per-field encryption; an attacker with filesystem-level access can read mesh signal metadata directly. Filing as a small enhancement, not a structural redesign.

Evidence

External design prior: thunderbird/thunderbolt src/db/powersync/middleware/EncryptionMiddleware.ts + src/db/encryption/config.ts:encryptedColumnsMap.

encryptedColumnsMap: {
  "signals":   ["payload", "metadata"],
  "vault":     ["secret"],
  "messages":  ["body"]
}

Middleware intercepts each write, looks up the table-column pair, applies AES-GCM (Thunderbolt) or ChaCha20-Poly1305 (akroasis equivalent — already in the akroasis crypto stack) before passing through to the storage layer. Reads do the inverse.

The key insight: which columns are encrypted is a single map separate from the middleware. Adding an encrypted column is a one-line change to the map; no code modification.

Conflict

  • Adjacent to AK1 (akroasis#17 hybrid PQ key wrapping) — that issue handles distributing keys across devices; this issue handles per-field encryption with the resulting keys.
  • No conflict with existing akroasis issues.

Why it matters

  1. Filesystem-level threat surface. A laptop seizure scenario with akroasis running locally exposes signal metadata if the fjall store is at-rest only. Per-field encryption mitigates.
  2. Pattern is generic. A BTreeSet<(TableId, ColumnId)> + a ColumnCodec trait covers this in <100 LOC of Rust.
  3. akroasis already has ChaCha20-Poly1305. No new crypto crate; just a codec layer over existing storage.

Done criteria

  1. akroasis (likely the fjall-store wrapper crate): ColumnCodec trait with encrypt(field: &[u8], context: &EncryptionContext) -> Vec<u8> and inverse.
  2. Static ENCRYPTED_FIELDS: BTreeSet<(TableId, FieldId)> lives in a single canonical place; not scattered across modules.
  3. Storage-layer write path consults the map; no opt-in per-call API.
  4. Read path does the inverse before returning to the consumer.
  5. Migration: existing un-encrypted data is re-encrypted on first write after the codec lands; or a one-shot migration tool wraps existing rows.
  6. Test: a synthetic fjall write of a mapped field produces ciphertext on disk, plaintext through the codec read path.

Source

thunderbird/thunderbolt src/db/powersync/middleware/EncryptionMiddleware.ts (read 2026-04-25). Sister to akroasis#17 (AK1 hybrid key wrapping).

Filed 2026-04-25 from thunderbolt deep-dive.

Provenance

Originally filed on the kanon forge as issue #18 on 2026-04-25T22:22:43.405368579-05:00[America/Chicago]. Recovered from 2026-05-09 pre-brick restic backup. Forge URL no longer reachable post firmware brick.

Severity

P1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions