Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

The Encrypted<T> tier is absent from the StorageEngine seam — and that seam is the one ADR-005 calls expensive to change

Источник: dev/fera-research/docs/oc3/001-encrypted-t-dropped-from-storage-seam.md (read-only; сгенерировано scripts/sync-docs.mjs)

The Encrypted<T> tier is absent from the StorageEngine seam — and that seam is the one ADR-005 calls expensive to change

Section titled “The Encrypted<T> tier is absent from the StorageEngine seam — and that seam is the one ADR-005 calls expensive to change”
  • kind: problem
  • area: storage
  • severity: high

The seam as written (plaintext Message in, plaintext Message out):

  • ~/dev/fera/fera/core/crates/storage/src/engine.rs:27-32 — fn get_messages(&self, chat: ChatId, before: Option<MessageId>, limit: u32) -> Result<Vec<Message>, Self::Error>;
  • ~/dev/fera/fera/core/crates/storage/src/engine.rs:39 — fn put_messages(&mut self, chat: ChatId, msgs: &[Message]) -> Result<(), Self::Error>;
  • ~/dev/fera/fera/core/crates/storage/src/engine.rs:62 — fn get_outbox(&self) -> Result<Vec<Message>, Self::Error>;
  • ~/dev/fera/fera/core/crates/storage/src/memory.rs:62,76,113 — the only impl matches, in-memory, no encryption
  • ~/dev/fera/fera/core/crates/domain/src/chat.rs:123 — fn put_messages(&mut self, chat: ChatId, msgs: &[Message]) -> Result<(), Self::Error>; (the Store trait duplicates the plaintext signature)

The only mention of Encrypted<T> anywhere in the whole repository:

  • ~/dev/fera/fera/core/crates/storage/src/lib.rs:6 — //! \Encrypted`/`Plaintext` phantom boundary (ADR-015/016/031)`

That is a doc comment. grep -rn "Encrypted\|Plaintext\|fn decrypt\|ChaCha\|Argon2\|XChaCha" --include='*.rs' ~/dev/fera/fera returns that one line and nothing else.

What the spec requires:

  • project-design/context/TECHSPEC.md:136-138 — fn get_messages(&self, chat_id, from: MessageId, limit: u32) -> Vec<Encrypted<Message>>
  • project-design/context/ADR-016.md → ADR.md:616-635 — the two-tier design: Encrypted<T> { data: Vec<u8>, _marker: PhantomData<T> }, Plaintext<T>, fn decrypt<T>(e: Encrypted<T>, key: &Key) -> Plaintext<T>; content tier XChaCha20-Poly1305 with Argon2id; metadata tier ChaCha20/AES-GCM
  • project-design/context/ADR.md:626-629 — “The Encrypted<T>/Plaintext<T> phantom-type boundary (ADR-015) enforces at compile time that content can’t be persisted without going through the appropriate tier’s encryption path.”
  • project-design/context/ADR.md:602-606 (ADR-015) — phantom types at that boundary are explicitly mandated
  • project-design/context/QA.md:73-76 — “Encrypted never observed in plaintext outside the decrypt boundary, enforced by phantom types + on-disk byte spot-check test”
  • project-design/context/ADR.md:631-635 + ADR.md:639-690 (ADR-031) — per-field tier assignment; last-message-preview is content tier

And the repo itself calls this seam load-bearing:

  • ~/dev/fera/fera/core/crates/storage/src/engine.rs:3-6 — “Batch-shaped per ADR-005 — the unit of exchange is a page, never a single item. This is the seam the future nylon-ring module boundary (TECHSPEC §10 step 4) will cut across, so its shape is load-bearing: retrofitting batching is expensive.”

The StorageEngine trait is the intended module boundary for storage. Its author documented, in the file itself, that this seam’s shape is expensive to change. The trait was then given a shape that contradicts TECHSPEC §3 and silently drops the entire ADR-016 encryption tier from the boundary.

The consequence is not “encryption is not implemented yet” — a not-yet-implemented tier can be added behind the signature. The consequence is that the signature as written makes the tier unrepresentable: Vec<Message> means the boundary hands out decrypted, typed domain objects. Message is the plaintext domain type carrying text: Option<MessageText> (storage/src/engine.rs:199-211, the contract suite’s own msg() helper). There is no Encrypted<Message> in the type, so ADR-015’s compile-time guarantee — “content can’t be persisted without going through the appropriate tier’s encryption path” — cannot be expressed, and QA.md:73-76’s phantom-type enforcement has nothing to enforce.

This is the same trap ADR-005 warns about for batching, applied to encryption: batch-first design is “cheap from day one, retrofitting is expensive” (ADR.md:137-139), and so is tier-first design. The one seam that carries both has been signed off with neither.

It also creates a false green signal. The MemoryEngine contract suite (engine.rs:97-212) passes against a plaintext engine. When a real sanakirja engine arrives, the suite will be run against it unchanged and will pass unchanged — because the suite never mentions encryption. A trait that cannot express the security property cannot test it.

Note this is a deviation, not a deferral: nothing in TECHSPEC, RFC, or OQ records the encryption tier as deferred past any milestone. ADR-010’s offline-first outbox (the currently-implemented path) is precisely the data that would be persisted unencrypted.

Варианты решения (с tradeoffs)

Section titled “Варианты решения (с tradeoffs)”

Option A — change the trait now to Vec<Encrypted<Message>> / &[Encrypted<Message>] (matches spec, costs churn now).

  • Cost: MemoryEngine, the contract suite helpers, and the duplicated Store trait in domain/chat.rs:114-123 all change. domain and ui-elm are downstream consumers.
  • Benefit: the seam matches TECHSPEC:136-138 exactly, and QA.md:73-76 becomes implementable before there is real user data on disk.
  • Risk: low. The whole workspace is 3821 LOC; nothing external depends on this signature yet. This is the cheapest moment in the project’s history to do it.

Option B — add encryption as a wrapper trait / decorator later (EncryptedStorageEngine<E: StorageEngine>).

  • Cost: preserves the current signature, so the plaintext Vec<Message> keeps existing at the module boundary — which is precisely where ADR-021 says the data crosses a process boundary and where a future third-party native module would receive it.
  • Benefit: no churn today.
  • Risk: high, and it defeats ADR-015. The whole point of the phantom type is that the compiler prevents the plaintext call. A decorator that can be bypassed by any caller who simply uses the inner StorageEngine provides the appearance, not the guarantee. It also contradicts ADR.md:626-629 explicitly.

Option C — keep plaintext signatures, record encryption as an explicit open ADR amendment / TECHSPEC deviation.

  • Cost: small.
  • Benefit: honest bookkeeping; makes the gap visible to the owner instead of invisible.
  • Risk: leaves a security-relevant spec violation undocumented until someone trips it.

Option A, immediately, before any sanakirja engine or any real persistence lands. The cost of changing this trait is at its absolute maximum-cheap right now: 3821 LOC total, zero external consumers, no migration to write, no on-disk data to re-encrypt. It only becomes expensive once modules/storage-sanakirja exists (TECHSPEC §10 step 4).

Concretely:

  1. Add Encrypted<T> / Plaintext<T> + decrypt to fera-protocol (they are wire-format boundary types; TECHSPEC.md:155-158 places them in the storage/protocol boundary area). ADOPT the ADR-031 field-by-field tier assignment from the start so the first Encrypted is not also a guess about which tier.
  2. Change the three signatures in engine.rs:27-39,62 to Vec<Encrypted<Message>> / &[Encrypted<Message>].
  3. Keep the MemoryEngine returning a NoopCipher so the contract suite stays runnable — but add an explicit encryption_tier_roundtrip case to engine.rs:97-212 so the suite actually exercises the property, and so QA.md:73-76 has something to grow into.
  4. Fix the duplicate plaintext Store trait at domain/chat.rs:114-123 in the same commit (see 004-two-competing-store-seams.md) rather than making the same signature change twice.
  • docs/oc3/004-two-competing-store-seams.md — the duplicated trait that must change in the same commit
  • Bus: this is the only finding of mine that is a security deviation; oc1/009 (module unload safety) and oc5/002 (no panic containment) are also HIGH and independent.