Protocol Specification

sTELgano-std-1

Open Protocol · Version 1.0 · April 2026

Overview

sTELgano-std-1 is an open protocol for contact-layer steganographic messaging. It defines how two parties establish and use an encrypted communication channel hidden inside a phone contact. The protocol covers identifier derivation, key generation, message encryption, transport, access control, and data lifecycle -- designed for independent implementation and self-hosting.

Core Invariants

N=1

At most one message exists per room at any time. Replies atomically delete the previous message in a single database transaction. No history exists anywhere.

Server Blindness

No server-side function accepts plaintext phone numbers or PINs. The server only processes opaque SHA-256 hashes and AES-256-GCM ciphertext.

Client-Side Crypto

All key derivation and encryption/decryption happens in the browser via the Web Crypto API. The server never touches plaintext or key material.

Identifiers

All identifiers are derived client-side using SHA-256. Each serves a distinct purpose in the protocol. The salts are public constants, optionally overridable for self-hosted instances.

room_hash
SHA-256(normalize(phone) + ":" + ROOM_SALT)

Uniquely identifies the room. Derived solely from the phone number. Both parties compute the same hash from the shared steg number.

access_hash
SHA-256(normalize(phone) + ":" + PIN + ":" + ACCESS_SALT)

Proves the caller knows the correct PIN. Each party has a different access_hash because they each choose a different PIN.

sender_hash
SHA-256(normalize(phone) + ":" + access_hash + ":" + room_hash + ":" + SENDER_SALT)

Identifies the sender within a room. Because access_hash is an input, two users with the same phone but different PINs produce different sender identities.

enc_key
PBKDF2(phone, room_id + ENC_SALT, 600000, SHA-256, 256-bit)

The symmetric encryption key. PIN is NOT part of the derivation because both users need the same key but have different PINs.

Key Design Decisions

  • PIN is NOT part of enc_key

    Both users need the same encryption key to read each other's messages, but they each choose a different PIN. If PIN were part of the key, messages would be unreadable by the other party.

  • access_hash IS part of sender_hash

    This ensures that two users with the same phone number but different PINs produce different sender identities. Without this, a single user could impersonate both sides of a conversation.

  • 600,000 PBKDF2 iterations

    Follows the OWASP 2023 recommendation for password-based key derivation. At ~2 seconds per derivation, brute-force attacks on the encryption key become computationally infeasible.

  • Salts are public constants

    The salts are embedded in the client-side JavaScript. They can be overridden by self-hosters, but rotating salts is a breaking change -- all existing rooms become permanently inaccessible.

Message Encryption

AES-256-GCM

Algorithm AES-256-GCM (Galois/Counter Mode)
Key Size 256-bit (derived via PBKDF2)
IV (Nonce) 96-bit random per message
Auth Tag 128-bit MAC for integrity verification
Max Ciphertext 8,192 bytes (base64-encoded)
Key Derivation PBKDF2 with 600,000 iterations

Transport

Phoenix Channels

Real-time communication over WebSocket using Phoenix Channels. The socket is fully anonymous -- no session cookie, no authentication token, no user identity. Connection state is entirely ephemeral.

Topic: anon_room:{room_hash} (64-char lowercase hex)

Channel Events

Join requires three 64-character hex strings: room_hash, access_hash, and sender_hash.

send_message
read_receipt
edit_message
delete_message
typing
expire_room

Access Control

Mechanism Behavior
Room Creation First join with a valid room_hash creates the room automatically
RoomAccess Server stores (room_hash, access_hash) pairs to validate PIN correctness
Lockout 10 failed access attempts trigger a 30-minute lock on that access_hash
Room Expiry Optional TTL per room, enforced by an hourly background worker that also hard-deletes the room's messages and access records
Max Occupancy Two access_hash entries per room (the two conversation parties)

Data Lifecycle

Instant Delete

Messages

Hard-deleted immediately on reply (N=1 invariant). The previous message row is permanently removed from the database in the same transaction that inserts the new message. No deferred purge, no soft-delete window.

Hourly Check

Rooms

Expire at their configured TTL. An hourly background worker deactivates expired rooms and, in a single atomic transaction, hard-deletes every message and every (room_hash, access_hash) record so no long-term linkability of past attempts survives.

Ephemeral

Sessions

Stored in browser sessionStorage only (up to 6 keys, including an optional extension secret when paid tiers are enabled). Cleared automatically on logout, panic route (/x), or room expiry. Never sent to the server. Never persisted to disk.

Self-Hosting

Run Your Own Instance

sTELgano-std-1 is designed for self-hosting. The four cryptographic salts (ROOM_SALT, ACCESS_SALT, SENDER_SALT, ENC_SALT) can be customized via environment variables, creating a completely independent namespace. Rooms on one instance cannot interact with rooms on another.

Rotating salts after deployment is a breaking change. All existing rooms will become permanently inaccessible.

View on GitHub

Version History

Version Date Notes
std-1 April 2026 Initial release. Defines contact-layer steganographic messaging with SHA-256 identifiers, AES-256-GCM encryption, PBKDF2 key derivation, Phoenix Channel transport, and N=1 message invariant.