feat(fof-layer3): PostVisibility::FoFClosed + body crypto + bucket padding
Adds the Mode 1 (encrypted body) primitives: PostVisibility::FoFClosed - New tag variant. The actual gating data (slot_binder_nonce, pub_post_set, wrap_slots) lives in Post.fof_gating — single source of truth shared between Mode 2 (Public + fof_gating) and Mode 1 (FoFClosed + fof_gating). Invariant: FoFClosed implies Some(gating). fof::encrypt_fof_body / decrypt_fof_body - ChaCha20-Poly1305 under the gating CEK with slot_binder_nonce as AAD (binds body decrypt to the post's gating; an attacker who steals CEK can't reuse it against a different post). - Plaintext format: real_len_u32_le || body_bytes || random_padding. Length prefix lets the reader strip padding after decrypt. - Bucketed body padding: power-of-2 from 1KB up to 256KB, then +256KB linear above. Different bodies in the same bucket produce identically-sized ciphertexts (test asserts this). fof::next_body_size_bucket(real) -> usize - Min 1KB, power-of-2 to 256KB, then +256KB steps. Aligns with the future storage chunk size at 256KB+. Three new tests (145 total): - body_bucket_rule_boundaries: spec-conformance for the bucket sizes. - fof_body_roundtrip: encrypt → decrypt; wrong CEK rejects; wrong AAD (slot_binder_nonce) rejects. - fof_body_padding_hides_real_length: 5B body and 500B body produce same-sized on-wire ciphertexts (1KB bucket). 8 match arms updated to handle FoFClosed across import, network, node, storage. Most paths skip FoFClosed-specific handling (it goes through the FoF wrap_slot path); revoke_post_access bails with a pointer to the FoF revoke helpers; index_post_recipients no-ops (FoF has no per-recipient identifiers on the wire). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
10de3f6108
commit
856f386231
6 changed files with 232 additions and 3 deletions
|
|
@ -62,6 +62,9 @@ fn parse_exported_intent(raw: Option<&str>, vis: &PostVisibility) -> VisibilityI
|
|||
// No intent recorded — infer from the visibility shape.
|
||||
match vis {
|
||||
PostVisibility::Public => VisibilityIntent::Public,
|
||||
// FoF Layer 3: FoFClosed pairs with VisibilityIntent::Public.
|
||||
// The FoF gating handles audience; intent is the structural tag.
|
||||
PostVisibility::FoFClosed => VisibilityIntent::Public,
|
||||
PostVisibility::Encrypted { recipients } => {
|
||||
// Heuristic: DMs typically wrap to 1-2 people (recipient + self);
|
||||
// Friends posts wrap to every public follow (usually many).
|
||||
|
|
@ -679,6 +682,16 @@ pub async fn merge_with_key(
|
|||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
PostVisibility::FoFClosed => {
|
||||
// FoF Layer 3 import: skip for now. The recovered
|
||||
// post would need its fof_gating + CEK to decrypt,
|
||||
// and the receiving persona's keyring may not
|
||||
// include the right V_x. Re-issue via the author's
|
||||
// device is the supported path.
|
||||
debug!(post = ep.id, "FoFClosed post — skipping (import not yet supported)");
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Create new post under our identity
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue