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
Evidence
Section titled “Evidence”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>;(theStoretrait 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-GCMproject-design/context/ADR.md:626-629— “TheEncrypted<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 mandatedproject-design/context/QA.md:73-76— “Encryptednever 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 duplicatedStoretrait indomain/chat.rs:114-123all change.domainandui-elmare 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
StorageEngineprovides the appearance, not the guarantee. It also contradictsADR.md:626-629explicitly.
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.
Предлагаемое решение
Section titled “Предлагаемое решение”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:
- Add
Encrypted<T>/Plaintext<T>+decrypttofera-protocol(they are wire-format boundary types;TECHSPEC.md:155-158places them in the storage/protocol boundary area).ADOPTthe ADR-031 field-by-field tier assignment from the start so the firstEncryptedis not also a guess about which tier. - Change the three signatures in
engine.rs:27-39,62toVec<Encrypted<Message>>/&[Encrypted<Message>]. - Keep the
MemoryEnginereturning aNoopCipherso the contract suite stays runnable — but add an explicitencryption_tier_roundtripcase toengine.rs:97-212so the suite actually exercises the property, and so QA.md:73-76 has something to grow into. - Fix the duplicate plaintext
Storetrait atdomain/chat.rs:114-123in the same commit (see004-two-competing-store-seams.md) rather than making the same signature change twice.
Cross-references
Section titled “Cross-references”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.