chat: roster-bound authorship, replay dedup, and rate limits (Phase 2)
CI / check (push) Successful in 2m59s
CI / check (push) Successful in 2m59s
Chat-hardening plan Phase 2 — only current authenticated room members can create chat UI work, impersonation via the wire name is structurally closed, and no member can monopolize the event channel: - core: new ChatRoster (bounded id -> sanitized-name map, shared) replaces the event task's bare HashSet; upserted on PeerJoined/PeerUpdated, removed on graceful PeerLeft AND terminal grace-expiry eviction (both timer paths). Non-roster chat is dropped before attachment handling; the rendered author label is the roster-bound name — the sender-claimed wire name is never read. - gossip: ChatIngressGate after verify_gossip, before any sanitize work or event send: early known-author gate (live + mid-reconnect peers), exact- replay suppression keyed on the deterministic Ed25519 signature (1024-entry cap + freshness-window TTL, zero new deps vs the plan's BLAKE3 option), then per-author (8 burst, 1/s) and room-wide (32 burst, 8/s) token buckets. Replays are detected before tokens are consumed; a room-bucket reject refunds the author token; rejection logging is squelched per author. - The inner Chat.ts is now ignored entirely; RoomEvent carries the signed envelope timestamp. 550 lib tests (+18), reconnect_eviction +1 (grace keeps chat authority, terminal eviction revokes it), clippy --all-targets -D warnings clean. Tests-green-only: the plan's two-machine field-test section remains open. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+4
-4
@@ -2003,10 +2003,10 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
||||
// Keep the message if it has visible text OR an attachment (an
|
||||
// image with no caption is still a real message).
|
||||
if !text.is_empty() || attachment.is_some() {
|
||||
// The author label is a spoof-sensitive short label, not
|
||||
// body text — use the strict name sanitizer (Phase 2 of
|
||||
// the chat-hardening plan replaces the wire name with the
|
||||
// roster-bound name entirely).
|
||||
// The author label arrives ROSTER-BOUND from the core
|
||||
// (chat-hardening Phase 2) — the wire name never reaches
|
||||
// here. The strict name sanitizer is idempotent defense
|
||||
// in depth, like the body pass above.
|
||||
let name = crate::sanitize::sanitize_name(&name);
|
||||
push_chat(
|
||||
&mut state.chat_messages,
|
||||
|
||||
Reference in New Issue
Block a user