Peer-to-peer file transfer, reusing Flamenet Files and enclave #49

Open
opened 2026-09-02 01:51:26 +00:00 by paulhitt · 2 comments
paulhitt commented 2026-09-02 01:51:26 +00:00 (Migrated from gitlab.com)

Ask: peer-to-peer file transfer in Messenger, reusing the Flamenet Files apps and the
enclave server as much as possible, and reusing the concepts wherever they fit. Labels and
security levels are to be kept
— that is a requirement, not an option.

🔴 Correction to the first draft of this issue

The original description claimed enclave is server-enforced confidentiality and therefore
incompatible with Messenger's end-to-end model. That was wrong, and it mattered, because
it made the "combine the products" option look far more expensive than it is.

Enclave is already zero-knowledge, and already keyed by label. From
Sources/EnclaveCrypto/KeyHierarchy.swift:

identity key   per user, derived from passphrase; never leaves client
     | wraps
label key      one per label in use, wrapped to every cleared principal
     | wraps
node key       one per file/folder, random
     | wraps
content key    one per revision

with the invariant spelled out in the module's own words: a node's key is wrapped to its
LABEL key, never to its parent's key
— because the tree permits a RESTRICTED file inside a
CONFIDENTIAL folder, and parent-derived keys would hand every folder-key holder the ability
to read above their clearance. The node_keys_label_alignment trigger enforces the same
thing in Postgres, so the misalignment is unrepresentable rather than merely discouraged.

So there are already two independent locks: RLS decides what the server will serve,
key wrapping decides what anyone can open. A snapshot, a backup, or a compromised server
yields ciphertext.

What this means for keeping labels in Messenger

The lattice runs entirely on metadata, never on content. Classification is an ordinal
Int16, Compartments is a 64-bit vector, and dominance is
classification >= other && compartments.isSuperset(of: other). Nothing in that requires
reading a file. Keeping labels and security levels costs nothing against E2E — the two
were designed to coexist.

The mapping to Messenger, which is the actual work:

enclave Messenger
label key wrapped to each cleared principal group key wrapped to each roster member
clearance granted by the control plane membership granted by the roster
label_key_grants_read — you see only your own grant sealed rosters (§8.9b)
generation bump on revocation roster change → rekey
compartments no equivalent today — this is the new capability

Two honest observations:

  • Clearance and membership are the same mechanism with different deciders. Enclave has
    an org admin; Messenger has peers. The key plumbing underneath is identical, and
    LabelKeyGrant already carries generation for exactly the rotation-on-revocation case.
  • Compartments are the interesting part. Messenger has no concept of need-to-know
    within a conversation. A compartment would give you a file visible to a subset of a
    group without a second group — which is a genuinely new feature, not a port.

Read-down needs no derivation scheme: grants are enumerated per label, and the module notes
labels in use are "dozens, not 2^64", so a cleared principal simply holds a grant for every
label they dominate.

What is actually left to decide

  • Who assigns the label when a file is dropped into a conversation? Inheriting the
    conversation's label is the obvious default and matches Policy.WriteRule.equal.
  • .starProperty write-up needs a public key per label for encrypt-to-without-read-back,
    since a lower-cleared writer cannot derive up. Not needed if we stay on .equal, which is
    the current default and the one that keeps every write auditable.
  • Transport. WebRTC data channel for direct transfer — calls already carry ICE, a
    deployed coturn, and signalling over the ratchet, so this is the same machinery with a
    different payload. Sealed relay storage as fallback, which is the common case behind NAT.
  • Reclassification is a rewrap of the content key under a new label key, not a
    re-encryption of the file. Cheap, because content keys are already separate from label keys.

The real costs, unchanged by the correction

  • No server-side search, preview, thumbnailing, or scanning. Filenames already run
    through HMAC(folder_key, normalise(name)) for uniqueness without knowledge, so lookup
    survives; content search does not. Client-side indexes only.
  • Revocation is rekeying, bounded to files at the affected label rather than all files.
    Better than today: ⚠️ flamenet-files-same-label-islands records that label sharing
    currently has no revocation at all.
  • Are these messages or files? Disappearing, unsendable and in-history, versus
    persistent, named and browsable. The two products answer differently today and the answer
    decides the data model.

⚠️ Related: relay#27 (relay-side sealed fan-out) asks the same question — what the server may
learn in exchange for doing work — and the answers should agree.

**Ask:** peer-to-peer file transfer in Messenger, reusing the Flamenet Files apps and the enclave server as much as possible, and reusing the concepts wherever they fit. **Labels and security levels are to be kept** — that is a requirement, not an option. ## 🔴 Correction to the first draft of this issue The original description claimed enclave is server-enforced confidentiality and therefore incompatible with Messenger's end-to-end model. **That was wrong**, and it mattered, because it made the "combine the products" option look far more expensive than it is. Enclave is *already* zero-knowledge, and already keyed by label. From `Sources/EnclaveCrypto/KeyHierarchy.swift`: ``` identity key per user, derived from passphrase; never leaves client | wraps label key one per label in use, wrapped to every cleared principal | wraps node key one per file/folder, random | wraps content key one per revision ``` with the invariant spelled out in the module's own words: **a node's key is wrapped to its LABEL key, never to its parent's key** — because the tree permits a RESTRICTED file inside a CONFIDENTIAL folder, and parent-derived keys would hand every folder-key holder the ability to read above their clearance. The `node_keys_label_alignment` trigger enforces the same thing in Postgres, so the misalignment is unrepresentable rather than merely discouraged. So there are already **two independent locks**: RLS decides what the server will serve, key wrapping decides what anyone can open. A snapshot, a backup, or a compromised server yields ciphertext. ## What this means for keeping labels in Messenger The lattice runs entirely on **metadata**, never on content. `Classification` is an ordinal `Int16`, `Compartments` is a 64-bit vector, and dominance is `classification >= other && compartments.isSuperset(of: other)`. Nothing in that requires reading a file. **Keeping labels and security levels costs nothing against E2E** — the two were designed to coexist. The mapping to Messenger, which is the actual work: | enclave | Messenger | |---|---| | label key wrapped to each cleared principal | group key wrapped to each roster member | | clearance granted by the control plane | membership granted by the roster | | `label_key_grants_read` — you see only your own grant | sealed rosters (§8.9b) | | `generation` bump on revocation | roster change → rekey | | compartments | **no equivalent today — this is the new capability** | Two honest observations: - **Clearance and membership are the same mechanism with different deciders.** Enclave has an org admin; Messenger has peers. The key plumbing underneath is identical, and `LabelKeyGrant` already carries `generation` for exactly the rotation-on-revocation case. - **Compartments are the interesting part.** Messenger has no concept of need-to-know *within* a conversation. A compartment would give you a file visible to a subset of a group without a second group — which is a genuinely new feature, not a port. Read-down needs no derivation scheme: grants are enumerated per label, and the module notes labels in use are "dozens, not 2^64", so a cleared principal simply holds a grant for every label they dominate. ## What is actually left to decide - **Who assigns the label** when a file is dropped into a conversation? Inheriting the conversation's label is the obvious default and matches `Policy.WriteRule.equal`. - **`.starProperty` write-up** needs a public key per label for encrypt-to-without-read-back, since a lower-cleared writer cannot derive up. Not needed if we stay on `.equal`, which is the current default and the one that keeps every write auditable. - **Transport.** WebRTC data channel for direct transfer — calls already carry ICE, a deployed coturn, and signalling over the ratchet, so this is the same machinery with a different payload. Sealed relay storage as fallback, which is the common case behind NAT. - **Reclassification** is a rewrap of the content key under a new label key, not a re-encryption of the file. Cheap, because content keys are already separate from label keys. ## The real costs, unchanged by the correction - **No server-side search, preview, thumbnailing, or scanning.** Filenames already run through `HMAC(folder_key, normalise(name))` for uniqueness without knowledge, so lookup survives; content search does not. Client-side indexes only. - **Revocation is rekeying**, bounded to files at the affected label rather than all files. Better than today: ⚠️ `flamenet-files-same-label-islands` records that label sharing currently has *no* revocation at all. - **Are these messages or files?** Disappearing, unsendable and in-history, versus persistent, named and browsable. The two products answer differently today and the answer decides the data model. ⚠️ Related: relay#27 (relay-side sealed fan-out) asks the same question — what the server may learn in exchange for doing work — and the answers should agree.
paulhitt commented 2026-09-02 01:55:55 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
paulhitt commented 2026-09-05 10:40:32 +00:00 (Migrated from gitlab.com)

Status, 2026-09-05: more of this is built than the description implies

I went looking for where to start and found most of the "mapping" section already shipped. Worth writing down, because the issue reads like a green field and it is not.

Already working, on both platforms

  • Labels travel on the wire. cls and cmp are on the attachment reference — E2EAttachment.swift:67-68 / Content.kt:849-850. A file sent into a conversation carries its classification and compartments inside the sealed envelope.
  • Both receivers show the classification, and both say in their own comments that it is a marking rather than a permission — AttachmentFileView.swift:6, FilesScreen.kt:369.
  • A saved file keeps the sender's classification, not Unclassified — ChatView.swift:1323.
  • The lattice exists on both sides. Android engine/.../Label.kt (Label, Clearance, dominates, the ten-byte encoding); iOS Packages/FlamenetFiles/Sources/FilesEngine/Label.swift.

New today: the two lattices are proven to agree

🔴 They had never been compared. Each engine pinned its own vectors, which is necessary and not sufficient — two engines can each pass their own and still disagree, and here the disagreement is silent by construction: the ten bytes are authenticated data for every construction that uses a label, so a mismatch produces a decryption failure, or plaintext that quietly belongs to the wrong sensitivity.

19 cross-engine checks now cover it (flamenet-e2e!28, android!67): five encodings byte for byte including a negative classification with every compartment bit set, bit 63 called out separately, and eight dominance cases weighted toward the failure that reads as working — an implementation that compares the level and forgets the areas behaves correctly on every case built from levels alone.

⚠️ Proven by breaking it: dropping containsAll from the Kotlin dominates turned exactly four checks red, all compartment cases, each naming which engine said what.

77 passed, 0 failed. So "labels and security levels are to be kept" now means something across two clients rather than within one.

What actually remains

  1. 🔴 The peer-to-peer transport — the literal title, and the biggest piece. A WebRTC data channel for direct transfer. Calls already carry ICE, a deployed coturn and signalling over the ratchet, so it is the same machinery with a different payload — but it is days of work and it cannot be verified on a simulator, which puts it behind ios#7.
  2. Compartments as a capability. The lattice supports them and the wire carries them; nothing yet uses a compartment to give a subset of a group access without a second group. This is the genuinely new feature the description identifies, and everything under it is now proven consistent.
  3. Reclassification — a rewrap of the content key under a new label key. Cheap, and unstarted.
  4. The ★-property (ios#65 / android#15) — still the owner's call, still must land on both platforms together or a file saves on one phone and is refused on the other.

⚠️ Nothing here needs the enclave server. The correction in the description holds: the lattice runs on metadata, never on content, so keeping labels costs nothing against E2E.

## Status, 2026-09-05: more of this is built than the description implies I went looking for where to start and found most of the "mapping" section already shipped. Worth writing down, because the issue reads like a green field and it is not. ### Already working, on both platforms - **Labels travel on the wire.** `cls` and `cmp` are on the attachment reference — `E2EAttachment.swift:67-68` / `Content.kt:849-850`. A file sent into a conversation carries its classification and compartments inside the sealed envelope. - **Both receivers show the classification**, and both say in their own comments that it is a *marking* rather than a permission — `AttachmentFileView.swift:6`, `FilesScreen.kt:369`. - **A saved file keeps the sender's classification**, not Unclassified — `ChatView.swift:1323`. - **The lattice exists on both sides.** Android `engine/.../Label.kt` (`Label`, `Clearance`, `dominates`, the ten-byte encoding); iOS `Packages/FlamenetFiles/Sources/FilesEngine/Label.swift`. ### New today: the two lattices are proven to agree 🔴 **They had never been compared.** Each engine pinned its own vectors, which is necessary and not sufficient — two engines can each pass their own and still disagree, and here the disagreement is silent by construction: the ten bytes are authenticated data for every construction that uses a label, so a mismatch produces a decryption failure, or plaintext that quietly belongs to the wrong sensitivity. 19 cross-engine checks now cover it (flamenet-e2e!28, android!67): five encodings byte for byte including a negative classification with every compartment bit set, bit 63 called out separately, and eight dominance cases weighted toward *the failure that reads as working* — an implementation that compares the level and forgets the areas behaves correctly on every case built from levels alone. ⚠️ Proven by breaking it: dropping `containsAll` from the Kotlin `dominates` turned exactly four checks red, all compartment cases, each naming which engine said what. **77 passed, 0 failed.** So "labels and security levels are to be kept" now means something across two clients rather than within one. ### What actually remains 1. 🔴 **The peer-to-peer transport — the literal title, and the biggest piece.** A WebRTC data channel for direct transfer. Calls already carry ICE, a deployed coturn and signalling over the ratchet, so it is the same machinery with a different payload — but it is days of work and it cannot be verified on a simulator, which puts it behind ios#7. 2. **Compartments as a capability.** The lattice supports them and the wire carries them; nothing yet *uses* a compartment to give a subset of a group access without a second group. This is the genuinely new feature the description identifies, and everything under it is now proven consistent. 3. **Reclassification** — a rewrap of the content key under a new label key. Cheap, and unstarted. 4. **The ★-property** (ios#65 / android#15) — still the owner's call, still must land on both platforms together or a file saves on one phone and is refused on the other. ⚠️ Nothing here needs the enclave server. The correction in the description holds: the lattice runs on metadata, never on content, so keeping labels costs nothing against E2E.
Sign in to join this conversation.