feat(fof-layer1): wire types + V_me auto-gen on persona creation
Adds VouchGrantBatch type to types.rs and extends ProfilePostContent with optional vouch_grants + bio_epoch fields (back-compat via #[serde(default)]). VouchGrantBatch carries one shared batch_eph_pub + a Vec<Vec<u8>> of 48-byte wrappers; receivers identify their wrapper by AEAD success, not position. Wires V_me auto-generation into both persona-creation paths: - Node::open first-run auto-persona block now seeds vouch_keys_own epoch=1 alongside upsert_posting_identity. - create_posting_identity calls ensure_initial_v_me after the new persona is stored. Helpers live as private free functions at module scope so both the sync (Node::open holds a Storage guard) and async (create_posting holds a StoragePool) sites can share them. Idempotent — re-running on a persona that already has a current key is a no-op. Two existing ProfilePostContent construction sites updated to set the new fields to defaults (None / 0); they'll get real values when the publish path is wired up in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8a53d83306
commit
bc008c5049
3 changed files with 79 additions and 0 deletions
|
|
@ -63,6 +63,35 @@ pub struct Node {
|
|||
budget_last_reset_ms: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
/// FoF Layer 1: generate a fresh 32B `V_me` and insert it as the
|
||||
/// persona's current epoch (epoch=1). Idempotent if the persona already
|
||||
/// has a current key — does nothing in that case.
|
||||
fn generate_and_store_initial_v_me(
|
||||
storage: &crate::storage::Storage,
|
||||
persona_id: &NodeId,
|
||||
now_ms: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
use rand::RngCore;
|
||||
if storage.current_own_vouch_key(persona_id)?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut key);
|
||||
storage.insert_own_vouch_key(persona_id, 1, &key, now_ms)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Async wrapper used by `Node::create_posting_identity`. Acquires the
|
||||
/// storage handle and delegates to the sync helper.
|
||||
async fn ensure_initial_v_me(
|
||||
storage: &StoragePool,
|
||||
persona_id: &NodeId,
|
||||
now_ms: u64,
|
||||
) -> anyhow::Result<()> {
|
||||
let s = storage.get().await;
|
||||
generate_and_store_initial_v_me(&s, persona_id, now_ms)
|
||||
}
|
||||
|
||||
impl Node {
|
||||
/// Create or open a node in the given data directory (Desktop profile)
|
||||
pub async fn open(data_dir: impl AsRef<Path>) -> anyhow::Result<Self> {
|
||||
|
|
@ -133,6 +162,8 @@ impl Node {
|
|||
created_at: now,
|
||||
})?;
|
||||
s.set_default_posting_id(&nid)?;
|
||||
// FoF Layer 1: auto-gen V_me epoch 1 for this fresh persona.
|
||||
generate_and_store_initial_v_me(&s, &nid, now)?;
|
||||
// Mark this as the disposable auto-gen persona from the
|
||||
// fresh-install flow. If the user subsequently imports, we
|
||||
// prune this id iff it's still pristine (no name, no posts,
|
||||
|
|
@ -684,6 +715,12 @@ impl Node {
|
|||
}
|
||||
}
|
||||
|
||||
// FoF Layer 1: every persona owns its own V_me (symmetric 32B key).
|
||||
// Auto-generate epoch 1 at creation. Stored in vouch_keys_own with
|
||||
// is_current=1. Per Layer 4, rotations append new epochs; this row
|
||||
// is never deleted automatically.
|
||||
ensure_initial_v_me(&self.storage, &node_id, now).await?;
|
||||
|
||||
Ok(identity)
|
||||
}
|
||||
|
||||
|
|
@ -772,6 +809,8 @@ impl Node {
|
|||
avatar_cid: None,
|
||||
timestamp_ms: pi.created_at,
|
||||
signature,
|
||||
vouch_grants: None,
|
||||
bio_epoch: 0,
|
||||
};
|
||||
let post = Post {
|
||||
author: pi.node_id,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,8 @@ pub fn build_profile_post(
|
|||
avatar_cid,
|
||||
timestamp_ms,
|
||||
signature,
|
||||
vouch_grants: None,
|
||||
bio_epoch: 0,
|
||||
};
|
||||
Post {
|
||||
author: *author,
|
||||
|
|
|
|||
|
|
@ -274,6 +274,44 @@ pub struct ProfilePostContent {
|
|||
/// 64-byte ed25519 signature. See `crypto::sign_profile` for the byte
|
||||
/// layout signed by the posting identity.
|
||||
pub signature: Vec<u8>,
|
||||
/// FoF Layer 1: HPKE-style anonymous wrapper batch carrying the
|
||||
/// voucher's V_me to each non-revoked recipient. `None` on bio posts
|
||||
/// that don't issue vouches (e.g., the inaugural empty-state profile
|
||||
/// post for a brand-new persona). Bound to this post via the
|
||||
/// `bio_post_id` baked into each wrapper's HKDF info.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vouch_grants: Option<VouchGrantBatch>,
|
||||
/// FoF Layer 1: monotonic bio-post revision counter for this persona.
|
||||
/// Used by receivers to short-circuit the scan via `vouch_bio_scan_cache`.
|
||||
/// Increments on every bio publish; 0 for pre-Layer-1 posts (back-compat).
|
||||
#[serde(default)]
|
||||
pub bio_epoch: u32,
|
||||
}
|
||||
|
||||
/// FoF Layer 1: a batch of per-recipient HPKE-style wrappers carrying
|
||||
/// the voucher's `V_me` to each currently-vouched persona. Sits inside
|
||||
/// a `VisibilityIntent::Profile` post. The post's author is the voucher.
|
||||
///
|
||||
/// Wire shape: one shared 32B ephemeral X25519 pubkey + N 48-byte
|
||||
/// wrappers (32B sealed `V_me` + 16B AEAD tag). Wrappers carry no
|
||||
/// recipient identifier; recipient anonymity is preserved by HPKE key
|
||||
/// privacy. The HKDF info string includes the bio post id but NEVER
|
||||
/// a recipient identifier — including one would break key privacy.
|
||||
///
|
||||
/// Dummy wrappers are random 48-byte sequences indistinguishable from
|
||||
/// real ones; they AEAD-fail on every persona. Bucketed padding is
|
||||
/// applied by the publisher (Layer 3 specifies the bucket boundaries).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VouchGrantBatch {
|
||||
/// Shared ephemeral X25519 pubkey for this batch.
|
||||
pub batch_eph_pub: [u8; 32],
|
||||
/// Epoch of the voucher's `V_me` being distributed in this batch.
|
||||
/// All wrappers in the batch carry the same epoch's key.
|
||||
pub v_x_epoch: u32,
|
||||
/// Real + dummy wrappers, shuffled. Each entry is exactly 48 bytes
|
||||
/// (32B sealed `V_me` + 16B AEAD tag); receivers identify "their"
|
||||
/// wrapper by successful AEAD decryption, not by position.
|
||||
pub wrappers: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Content payload of a `VisibilityIntent::Announcement` post.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue