Encrypted<T> does not encode the tier: ADR-016's compile-time guarantee is not true
Источник:
dev/fera-research/docs/oc1/005-encrypted-phantom-type-does-not-encode-tier.md(read-only; сгенерировано scripts/sync-docs.mjs)
Encrypted<T> does not encode the tier: ADR-016’s compile-time guarantee is not true
Section titled “Encrypted<T> does not encode the tier: ADR-016’s compile-time guarantee is not true”- kind: problem
- area: storage / trust
- evidence:
/home/nikki/dev/fera/project-design/context/ADR.md:604-606— ADR-015: “Phantom types for theEncrypted<T>/Plaintext<T>boundary (ADR-016) so the storage layer cannot accidentally persist unencrypted data where a trait signature demandsEncrypted<T>.”/home/nikki/dev/fera/project-design/context/ADR.md:627-629— ADR-016: “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.” CompareADR.md:631-635: “Two distinct encryption code paths must be maintained; which data belongs to which tier must be decided explicitly per field, not inferred”./home/nikki/dev/fera/project-design/context/TECHSPEC.md:155-159— the actual signature:No tier in either type. No bound onstruct Encrypted<T> { data: Vec<u8>, _marker: PhantomData<T> }struct Plaintext<T> { data: T }fn decrypt<T>(e: Encrypted<T>, key: &Key) -> Plaintext<T>;T.Keyhas no tier identity (TECHSPEC.md:160-162names two ciphers and oneKey).- Field tier assignments:
ADR.md:648-664(ADR-031’s audit table). - Real code:
~/dev/fera/fera/core/crates/storage(StorageEngine contract + in-memory reference impl, commit004540f) — noEncrypted/Plaintexttypes exist yet, so this is a spec-level defect, not yet a code defect.
- severity: mid
Суть / Substance
Section titled “Суть / Substance”As specified, Encrypted<T> carries no information about which tier
encrypted the payload, and decrypt is generic over an unbounded T.
Two consequences, both of which invalidate the ADR-016 sentence it is
cited for:
- Tier is not a type property. Nothing in
Encrypted<ChatTitle>says whether it was sealed with the metadata cipher or the content cipher. ADR-016:631-635 concedes tier assignment is “explicitly per field, not inferred” — meaning the tier assignment is a convention living only in ADR-031’s table, enforced by review, not by types. So “enforces at compile time … the appropriate tier’s encryption path” (ADR-016:627-629) overstates what the design achieves. It enforces encrypted-ness, which is the weaker ADR-015 claim, and that one is true. Tis a lie in the other direction.decrypt::<Message>(encrypted_chat_title)type-checks and yieldsPlaintext<Message>; nothing verifies the ciphertext was produced from aMessage. Phantom types bind the value to a declared type, they do not authenticate the correspondence between ciphertext and plaintext type. If the intent was type-level integrity, anAead-style sealed box with an explicitSeal<T, Tier>marker plus a real AEAD tag is required — and nothing in the signature carries a nonce or tag at all (data: Vec<u8>is ambiguous between nonce, tag, ciphertext and header).
Adjacent, same code path: no per-record nonce handling is specified.
A single Vec<u8> per record with XChaCha20-Poly1305 (ADR-016:624-626)
requires a nonce per record and, for the metadata tier with random access
per-field records, a deterministic or counter-derived nonce scheme or
XChaCha’s extended nonce buys nothing. Malleability of metadata is the
concrete risk — see my finding on AAD/field binding.
Варианты решения (tradeoffs)
Section titled “Варианты решения (tradeoffs)”- (A) Make the tier a type parameter:
Encrypted<T, Tier>withstruct Metadata; struct Content;, anddecrypt<T, Tier: Aead>(…) -> Plaintext<T>. Compile-time tier enforcement, two paths still distinct. Costs: generic noise at every storage call site; cross-tier moves (a preview reclassified) become type errors needing an explicit rewrap. - (B) Keep single-tier
Encrypted<T>, put the tier in the sealed header (first byte = tier id + nonce + tag) and enforce at runtime in oneopen()function. Simpler call sites; loses compile-time enforcement entirely; runtime check must be audited like any other validation — mildly at odds with ADR-015’s “parse, don’t validate” ethos. - (C) Two nominal types:
EncryptedMetadata(Vec<u8>)/EncryptedContent(Vec<u8>)with noTat all; the plaintext type moves to the trait method (get_messages() -> Vec<Plaintext<Message>>). The plaintext type then lives in the signature where it is actually checkable, and the ciphertext type carries the tier. Cleanest invalid-state story; biggest deviation from ADR-015’s stated sketch.
Предлагаемое решение
Section titled “Предлагаемое решение”(C), with (A) as the cheap middle path if the T phantom must stay.
- Regardless of choice, fix the ADR-016 sentence — it is the only place claiming compile-time tier enforcement, and it is false. Say what is actually enforced (encrypted-ness at the boundary) and what is not (tier identity, ciphertext↔plaintext correspondence).
- Specify the sealed-container layout explicitly — nonce, tag, tier id,
and AAD — before any storage impl is written against it. Right now
data: Vec<u8>is not a format. - Make the
StorageEnginetrait (TECHSPEC:136-143) returnPlaintext<T>rather thanEncrypted<T>where the engine is the decryptor, so the type mismatch is unrepresentable rather than merely discouraged. - Note for QA: add a gate that no
Vec<u8>field crosses the storage boundary untyped.