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):
- "Private chat" — Creates a new 2-person private room, posts invitation in the shared room
- "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:
- Subscribe to each private room contract the user is a member of
- 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
- 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
- New
Invitation message type in the room contract — contains private room contract key, posted in shared rooms
- UI rendering of invitation messages as clickable cards
- Delegate contract subscription — subscribe to private room contracts, react to member changes
- Automatic secret distribution in the delegate — encrypt room secret for new members using V2 host functions
- Delegate context persistence — store list of private room contract keys for re-subscribe on restart
- Re-enable UI toggle — remove hardcoded
false in create_room_modal.rs, restore the reactive signal
- Contract merge logic — deduplicate
encrypted_secrets entries for concurrent distribution
- 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]
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 tofalsewith "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:
UX
Two actions when clicking a username (in message header or member list):
Invitation renders as a clickable card in the shared room. Bob clicks to accept.
Delegate behavior
The chat delegate would:
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_secretsby(member_id, secret_version), keeping whichever entry arrives first.Implementation Steps
Invitationmessage type in the room contract — contains private room contract key, posted in shared roomsfalseincreate_room_modal.rs, restore the reactive signalencrypted_secretsentries for concurrent distribution--privateflag toriverctl room createWhat Already Exists
false)--privateflagQuestions for Discussion
[AI-assisted - Claude]