# Encrypted Rooms (Password-Protected Channels) | EchoHub Documentation

# Encrypted Rooms (Password-Protected Channels)

An encrypted room is a channel whose entire content — every message and every file — is end-to-end encrypted with a key derived from a shared passphrase. Only people who know the passphrase can read the room. Not even the server owner can read the content, yet the server can still gate who joins, and it can count and measure what's stored (message count, file sizes, timestamps) without ever seeing the plaintext.

This is a stronger guarantee than the transport and at-rest encryption described elsewhere, where the server decrypts every message to process it. Here the server is treated as untrusted for content: it holds only ciphertext and wrapped keys.

> The passphrase is the only key. There is no recovery. If everyone who knows a room's passphrase forgets it, that room's history is permanently unreadable — by design.

## What the server can and cannot see

The server can see | The server cannot see
--- | ---
That the channel is encrypted | Message text
Message count and timestamps | File contents
Who sent each message (sender identity) | Image previews (ASCII art)
Each attachment's file name and byte size | The passphrase, the room key, or the key-encryption key
The estimated total size (via `/meta`) | Anything that would let it decrypt the above

File names are stored in plaintext so the file list stays usable — treat a file name itself as non-secret. Everything inside the file is encrypted.

## Key hierarchy

Three keys are derived from one passphrase. The passphrase, the key-encryption key, and the room content key never leave the client.

    flowchart TD
        P[Passphrase] -->|PBKDF2-SHA256, 210k iterations, per-room salt| OKM[64-byte output]
        OKM --> AK["Auth key (first 32 bytes)"]
        OKM --> KEK["Key-encryption key / KEK (last 32 bytes)"]
        AK -->|sent as lowercase hex| SRV1["Server: BCrypt-hash as the join gate"]
        RCK["Room Content Key (random 256-bit)"] -->|encrypts all content| CONTENT[Messages + files + previews]
        RCK -->|AES-256-GCM wrap under KEK| WRAP["Wrapped room key"]
        WRAP -->|stored| SRV2["Server: stores wrapped key + salt only"]

- Auth key — the join credential. Derived from the passphrase, sent to the server as hex, and stored only as a BCrypt hash. Proving knowledge of it is what lets you join; it reveals nothing about the content key.
- Key-encryption key (KEK) — never sent. Used locally to wrap (encrypt) and unwrap the room content key.
- Room Content Key (RCK) — a random 256-bit key generated once, at room creation. It encrypts every message and file. The server stores it only in wrapped form, so it can hand the wrapped key to a joiner but can never unwrap it itself.

All content encryption is AES-256-GCM with a random 12-byte nonce and a 16-byte authentication tag per item, so identical inputs never produce identical ciphertext, and any tampering is detected.

Room-encrypted text carries a self-describing prefix so clients and the server can tell it apart from plaintext:

    $RC1$base64(nonce || tag || ciphertext)

## Creating a room

The client does all the cryptography locally, then hands the server only what it needs to gate joins and store (but not read) the content.

    sequenceDiagram
        participant Client
        participant Server

        Client->>Client: generate salt + random Room Content Key (RCK)
        Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
        Client->>Client: wrap RCK under KEK
        Client->>Server: create channel { authKey(hex), salt, wrappedRoomKey }
        Server->>Server: BCrypt-hash auth key, store salt + wrapped key
        Note over Server: Server never receives passphrase, KEK, or RCK

## Joining a room

    sequenceDiagram
        participant Client
        participant Server

        Client->>Server: GET /crypto → { isEncrypted, salt }
        Client->>Client: DeriveKeys(passphrase, salt) → auth key + KEK
        Client->>Server: join { authKey(hex) }
        Server->>Server: BCrypt-verify against stored hash
        alt correct passphrase
            Server->>Client: history (ciphertext) + wrapped room key
            Client->>Client: unwrap RCK with KEK, then decrypt everything locally
        else wrong passphrase
            Server->>Client: rejected (join gate fails)
        end

A wrong passphrase fails the BCrypt gate, so the server never even hands out the wrapped key. Even if it did, an attacker without the KEK cannot unwrap it.

## What gets encrypted

When you send a message or attach files to an encrypted room, the client encrypts each part with the room content key before uploading:

- Message text → `$RC1$…` ciphertext.
- Files (any kind) → the whole blob is AES-256-GCM encrypted client-side; the server stores an opaque ciphertext blob.
- Image ASCII previews → rendered on the client, then room-encrypted. The server never sees the rendered art.

The server records each attachment's kind, file name, and byte size (of the ciphertext blob) as metadata, and broadcasts the ciphertext to other members, who decrypt locally.

## Changing the passphrase

`/passwd <old> <new>` rotates the passphrase. Because only the wrapping of the room content key changes — not the RCK itself — all existing history stays readable:

1. The client proves knowledge of the old passphrase (old auth key).
2. It unwraps the RCK with the old KEK, then re-wraps it under the new KEK (new salt).
3. It uploads the new auth key + salt + re-wrapped key. The content is never re-encrypted.

## Inspecting a room

Use `/meta` in any channel to see what the server knows about it, including encrypted rooms:

    Room info for #private-room:
      Room ID       3f2a…-…-…
      Created       7/16/2026 2:31 PM
      Messages      128
      Unique users  4
      Est. size     42.5 MB
      Protection    end-to-end encrypted

`Est. size` is the sum of stored attachment blob sizes plus message text length — an estimate of the room's footprint, computed entirely from metadata the server holds without reading any content.

## Limitations & security notes

- No recovery. A lost passphrase means unrecoverable history. Keep it safe; there is no reset.
- File names are plaintext. They stay readable so the file list works — don't put secrets in a file name.
- IRC is disabled for encrypted rooms. The IRC gateway forwards plaintext and cannot participate in the room's key scheme, so encrypted channels are not bridged to IRC.
- Metadata is visible. Message counts, timestamps, sender identities, file names, and sizes are intentionally readable so the server can moderate at the metadata level and report `/meta`.
- Endpoint trust. End-to-end encryption protects content from the server and the network, not from a compromised client device that already holds the passphrase.

## Related

- Message Encryption — transport (`$ENC$v1$`) and optional at-rest database encryption, where the server does decrypt content for processing. Encrypted rooms are a separate, stronger layer that sits on top.