Phase 2g: GroupKeyDistribute \u2192 encrypted post
Removes the last persona-signed direct push on the wire. Group/circle
seeds no longer travel via the 0xA0 `GroupKeyDistribute` uni-stream from
admin to member. Instead the admin publishes an encrypted post containing
the seed + metadata; each member is a recipient; the post propagates via
the normal CDN. Members decrypt with their posting secret to recover the
seed.
Eliminates the wire-level coordination signal between an admin endpoint
and each member endpoint when a group is created, a member is added, or
a key is rotated.
Core pieces:
- New `VisibilityIntent::GroupKeyDistribute` variant.
- New `types::GroupKeyDistributionContent` — JSON payload inside the
encrypted post: group_id, circle_name, epoch, group_public_key, admin,
canonical_root_post_id, group_seed.
- New `group_key_distribution` module:
- `build_distribution_post(admin, admin_secret, record, group_seed, members)`
returns `(PostId, Post, PostVisibility::Encrypted)` — wraps the CEK
per member using standard `crypto::encrypt_post`.
- `try_apply_distribution_post(s, post, visibility, our_personas)`
iterates every posting identity's secret trying to decrypt; on
success stores `group_key` + `group_seed` and returns true.
- `process_pending(s, our_personas)` scans stored
GroupKeyDistribute-intent posts and applies any we can decrypt.
Node API:
- `add_to_circle`: builds a distribution post wrapping the current seed
to just the new member, stores with intent=GroupKeyDistribute, and
propagates via `update_neighbor_manifests_as` (no direct push).
- `create_group_key_inner`: at group creation, after wrapping keys for
every non-self member, builds one distribution post addressed to all
of them and propagates through the CDN.
- `rotate_group_key`: same pattern at epoch rotation.
- New `Node::process_group_key_distributions` — scans and applies.
`sync_all` now calls it automatically so seeds take effect right after
a pull cycle.
Removals (wire-breaking; v0.6.2 already forked):
- MessageType 0xA0 (`GroupKeyDistribute`), its payload struct, the
handler in connection.rs, and `Network::push_group_key` all deleted.
ConnectionManager's `secret_seed` (network secret) is no longer used for
group-key unwrapping — that shifted to posting secrets in the apply
pass, matching the v0.6.1+ identity split where group keys are wrapped
to posting NodeIds.
Tests: new `member_decrypts_and_applies` covers a recipient decrypting +
storing the seed and a non-recipient failing to apply. Workspace
compiles clean; 118 / 118 core tests pass on a stable run (pre-existing
flaky `relay_cooldown` test with a 1ms timing window is unrelated).
This commit is contained in:
parent
2cb211eb11
commit
f88618bb6f
8 changed files with 385 additions and 132 deletions
|
|
@ -15,7 +15,7 @@ use crate::protocol::{
|
|||
AnchorReferralRequestPayload, AnchorReferralResponsePayload, AnchorRegisterPayload,
|
||||
BlobHeaderDiffPayload,
|
||||
BlobHeaderRequestPayload, BlobHeaderResponsePayload, BlobRequestPayload, BlobResponsePayload,
|
||||
CircleProfileUpdatePayload, GroupKeyDistributePayload, GroupKeyRequestPayload,
|
||||
CircleProfileUpdatePayload, GroupKeyRequestPayload,
|
||||
GroupKeyResponsePayload, InitialExchangePayload, MeshPreferPayload,
|
||||
MessageType, NodeListUpdatePayload, PostDownstreamRegisterPayload,
|
||||
ProfileUpdatePayload, PullSyncRequestPayload, PullSyncResponsePayload,
|
||||
|
|
@ -5117,51 +5117,6 @@ impl ConnectionManager {
|
|||
"Received social disconnect notice"
|
||||
);
|
||||
}
|
||||
MessageType::GroupKeyDistribute => {
|
||||
let payload: GroupKeyDistributePayload = read_payload(recv, MAX_PAYLOAD).await?;
|
||||
let cm = conn_mgr.lock().await;
|
||||
|
||||
// Verify the sender is the admin
|
||||
if payload.admin != remote_node_id {
|
||||
warn!(peer = hex::encode(remote_node_id), "GroupKeyDistribute from non-admin, ignoring");
|
||||
} else {
|
||||
let storage = cm.storage.get().await;
|
||||
let record = crate::types::GroupKeyRecord {
|
||||
group_id: payload.group_id,
|
||||
circle_name: payload.circle_name.clone(),
|
||||
epoch: payload.epoch,
|
||||
group_public_key: payload.group_public_key,
|
||||
admin: payload.admin,
|
||||
created_at: now_ms(),
|
||||
canonical_root_post_id: payload.canonical_root_post_id,
|
||||
};
|
||||
let _ = storage.create_group_key(&record, None);
|
||||
|
||||
// Find our wrapped key and unwrap the group seed
|
||||
for mk in &payload.member_keys {
|
||||
let _ = storage.store_group_member_key(&payload.group_id, mk);
|
||||
if mk.member == cm.our_node_id {
|
||||
match crypto::unwrap_group_key(
|
||||
&cm.secret_seed,
|
||||
&payload.admin,
|
||||
&mk.wrapped_group_key,
|
||||
) {
|
||||
Ok(seed) => {
|
||||
let _ = storage.store_group_seed(&payload.group_id, payload.epoch, &seed);
|
||||
info!(
|
||||
circle = %payload.circle_name,
|
||||
epoch = payload.epoch,
|
||||
"Received and unwrapped group key"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(error = %e, "Failed to unwrap group key");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageType::CircleProfileUpdate => {
|
||||
let payload: CircleProfileUpdatePayload = read_payload(recv, MAX_PAYLOAD).await?;
|
||||
let cm = conn_mgr.lock().await;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue