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.
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
🔴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.
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.
Reclassification — a rewrap of the content key under a new label key. Cheap, and unstarted.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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: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_alignmenttrigger enforces the samething 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.
Classificationis an ordinalInt16,Compartmentsis a 64-bit vector, and dominance isclassification >= other && compartments.isSuperset(of: other). Nothing in that requiresreading 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:
label_key_grants_read— you see only your own grantgenerationbump on revocationTwo honest observations:
an org admin; Messenger has peers. The key plumbing underneath is identical, and
LabelKeyGrantalready carriesgenerationfor exactly the rotation-on-revocation case.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
conversation's label is the obvious default and matches
Policy.WriteRule.equal..starPropertywrite-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 isthe current default and the one that keeps every write auditable.
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.
re-encryption of the file. Cheap, because content keys are already separate from label keys.
The real costs, unchanged by the correction
through
HMAC(folder_key, normalise(name))for uniqueness without knowledge, so lookupsurvives; content search does not. Client-side indexes only.
Better than today: ⚠️
flamenet-files-same-label-islandsrecords that label sharingcurrently has no revocation at all.
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.
changed the description
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
clsandcmpare 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.AttachmentFileView.swift:6,FilesScreen.kt:369.ChatView.swift:1323.engine/.../Label.kt(Label,Clearance,dominates, the ten-byte encoding); iOSPackages/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
containsAllfrom the Kotlindominatesturned 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
⚠️ 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.