Skip to content

feat: Enable private rooms with automatic secret distribution #140

@sanity

Description

@sanity

Summary

River has a nearly-complete private room implementation that is currently disabled. The encryption infrastructure (AES-256-GCM symmetric + ECIES key distribution), data model (PrivacyMode, SealedBytes, RoomSecretsV1), contract validation, message encryption/decryption, and secret rotation are all fully implemented and tested. The UI checkbox is hardcoded to false with "Private rooms temporarily disabled".

Why it was disabled (commit 58ab2e7): "non-functional without owner online" — the room owner must be online to distribute the room secret to new/returning members. In a decentralized system where peers aren't always online, this makes private rooms unreliable.

Proposed Design

Core idea: any member can distribute secrets

Instead of requiring the owner to distribute room secrets, any member who has the secret can encrypt it for a new member. The chat delegate uses the existing V2 delegate-contract interaction host functions (get_contract_state, update_contract_state, subscribe_contract) to watch for new members and automatically distribute secrets.

Invitation flow

Invitations are posted as a special message type in the shared public room where both users are already present. The invitation contains the private room's contract key (which is not sensitive — knowing the key without the room secret is useless).

Why public invitations are a feature, not a limitation:

  • Spam/auto-invite behavior is visible to room members and the owner, who can ban abusers
  • Creates social accountability — you can see who's inviting whom
  • Only metadata leaked is "Alice invited Bob to a private room" — not the room's content, name, or other members
  • Dramatically simpler than a per-user inbox contract approach (which would require related contract validation)

UX

Two actions when clicking a username (in message header or member list):

  1. "Private chat" — Creates a new 2-person private room, posts invitation in the shared room
  2. "Invite to room" — Posts invitation for an existing private room the user owns/is a member of

Invitation renders as a clickable card in the shared room. Bob clicks to accept.

Delegate behavior

The chat delegate would:

  1. Subscribe to each private room contract the user is a member of
  2. On new member detected (via contract state change notification): if the new member lacks an encrypted secret entry and we have the room secret, encrypt it for them and update the contract
  3. Persist room list in delegate context so it re-subscribes after restart

Concurrent secret distribution

Multiple members may independently encrypt the secret for the same new member. This is fine — ECIES produces different ciphertexts (randomized ephemeral keys) but they all decrypt to the same room secret. The contract merge deduplicates encrypted_secrets by (member_id, secret_version), keeping whichever entry arrives first.

Implementation Steps

  1. New Invitation message type in the room contract — contains private room contract key, posted in shared rooms
  2. UI rendering of invitation messages as clickable cards
  3. Delegate contract subscription — subscribe to private room contracts, react to member changes
  4. Automatic secret distribution in the delegate — encrypt room secret for new members using V2 host functions
  5. Delegate context persistence — store list of private room contract keys for re-subscribe on restart
  6. Re-enable UI toggle — remove hardcoded false in create_room_modal.rs, restore the reactive signal
  7. Contract merge logic — deduplicate encrypted_secrets entries for concurrent distribution
  8. CLI support — add --private flag to riverctl room create

What Already Exists

Component Status
Encryption (AES-256-GCM + ECIES) ✅ Complete
Data model (PrivacyMode, SealedBytes, RoomSecretsV1) ✅ Complete
Contract validation (enforces encryption in private rooms) ✅ Complete
Message encryption/decryption in UI ✅ Complete
Secret rotation (versioning + UI button) ✅ Complete
Tests (660+ lines in private_room_test.rs + crypto tests) ✅ Complete
V2 delegate-contract host functions (freenet-core) ✅ Complete
UI creation toggle ⚠️ Disabled (hardcoded false)
CLI --private flag ❌ Missing
Delegate contract interaction (in River) ❌ Not yet used
Invitation message type ❌ Not yet implemented

Questions for Discussion

  • Should invitation cards show the room name (decryptable only by invitee) or just "Alice invited you to a private room"?
  • Should there be a confirmation step before accepting an invitation, or should the delegate auto-accept and just notify the user?
  • Should room owners be able to configure whether any member can invite, or only the owner?
  • Is the public invitation metadata leak acceptable for all use cases, or should we also support a private invitation path (per-user inbox contracts) for higher-security scenarios?

[AI-assisted - Claude]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions