Skip to content

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

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 the Encrypted<T> / Plaintext<T> boundary (ADR-016) so the storage layer cannot accidentally persist unencrypted data where a trait signature demands Encrypted<T>.”
    • /home/nikki/dev/fera/project-design/context/ADR.md:627-629 — ADR-016: “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.” Compare ADR.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:
      struct Encrypted<T> { data: Vec<u8>, _marker: PhantomData<T> }
      struct Plaintext<T> { data: T }
      fn decrypt<T>(e: Encrypted<T>, key: &Key) -> Plaintext<T>;
      No tier in either type. No bound on T. Key has no tier identity (TECHSPEC.md:160-162 names two ciphers and one Key).
    • 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, commit 004540f) — no Encrypted/Plaintext types exist yet, so this is a spec-level defect, not yet a code defect.
  • severity: mid

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:

  1. 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.
  2. T is a lie in the other direction. decrypt::<Message>(encrypted_chat_title) type-checks and yields Plaintext<Message>; nothing verifies the ciphertext was produced from a Message. 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, an Aead-style sealed box with an explicit Seal<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> with struct Metadata; struct Content;, and decrypt<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 one open() 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 no T at 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.

(C), with (A) as the cheap middle path if the T phantom must stay.

  1. 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).
  2. 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.
  3. Make the StorageEngine trait (TECHSPEC:136-143) return Plaintext<T> rather than Encrypted<T> where the engine is the decryptor, so the type mismatch is unrepresentable rather than merely discouraged.
  4. Note for QA: add a gate that no Vec<u8> field crosses the storage boundary untyped.