Technical protocol document for the GoChat encrypted web messenger.
Dual-profile architecture, SMP-web spike analysis, implementation roadmap, and task registry.
Project: GoChat - Browser-Native Encrypted Messenger
Parent project: SimpleGo
Ecosystem: SimpleGo (hardware) / GoRelay (relay server) / GoChat (browser client)
Date: 2026-04-04
Branch analyzed: ep/smp-web-spike on simplex-chat/simplexmq
Status: Season 12 complete (Widget Product), Season 13 (Security Hardening) next
These rules apply to this document, all season protocols, and all GoChat development. They are non-negotiable.
-
Nothing invented. What is missing gets asked. The Prinzessin shows everything needed. Claude does not fabricate information, assume technical details, or fill gaps with speculation. If a detail is unknown, it is marked as unknown and the question is raised.
-
Conventional Commits only. Every commit follows the format
type(scope): description. Valid types: feat, fix, docs, test, refactor, ci, chore. Commits are as granular as possible, as few as necessary. -
No em dashes. Only regular hyphens (-) or rewritten sentences. Em dashes are an AI giveaway.
-
No version number changes without explicit permission from the project owner.
-
English for all code, commits, comments, and documentation. Conversations happen in German, artifacts are in English.
-
TypeScript strict mode, @noble-only crypto, Web Worker isolation for all browser-side cryptographic operations.
GoChat is a browser-native encrypted messenger built on SimpleXMQ. It brings the SimpleX Messaging Protocol directly into the browser, allowing website visitors to communicate through end-to-end encrypted channels without installing any application or creating any account.
GoChat supports two communication profiles selectable per connection:
- SMP Profile - everyday encrypted communication via the SimpleX Messaging Protocol over WebSocket, compatible with all SimpleX clients and servers
- GRP Profile - high-security communication via the GoRelay Protocol over WebSocket, exclusively through GoRelay infrastructure, with Noise transport, mandatory post-quantum cryptography, two-hop relay routing, and cover traffic
No browser-native SMP client exists anywhere - not from SimpleX Chat, not from the community, not from any third party. No open-source support chat tool offers E2E encryption on the customer-facing widget side. GoChat would be first on both counts.
GoChat is not a replacement for SimpleX - it is a complementary extension. No mobile app is planned or needed. SimpleX covers mobile and desktop. GoChat fills the browser gap and extends with GRP for high-security environments.
Based on the ep/smp-web-spike branch by @epoberezkin (SimpleX founder), which introduces browser-native SMP protocol support. Evgeny confirmed in the SimpleX community chat that smp-web is actively being worked on ("its WIP :)").
SimpleX uses unidirectional message queues on relay servers. A single chat connection requires two queues (one in each direction). The relay server never sees both queues as belonging to the same conversation.
User A SMP Relay User B
| | |
|--- Queue 1 (A sends) --->|--- Queue 1 (B receives) ->|
| | |
|<-- Queue 2 (B sends) ---|<-- Queue 2 (A receives) --|
| | |
What the server sees: isolated encrypted blobs in anonymous queues. No user IDs, no metadata linking Queue 1 to Queue 2, no way to correlate sender and receiver.
-
Contact address creation: The support team creates a permanent SimpleX contact address in their app. This generates a link containing the server address and a public key.
-
User connects: When a website visitor clicks "Support Chat", their browser parses the contact address link, creates a new queue pair on the SMP server, performs X3DH key agreement, and sends a connection request through the contact address queue.
-
Support accepts: The SimpleX app on the support team's device automatically accepts the connection (configurable) and establishes the bidirectional channel.
-
Messaging: Both parties can now exchange E2E encrypted messages through their dedicated queue pair.
Each new visitor who connects via the contact address gets their own independent queue pair. The support team sees each visitor as a separate contact in their SimpleX app. There is no theoretical limit on concurrent connections - the SMP server handles queue isolation natively.
Visitor A ---[Q1/Q2]---> SMP Server ---[Q1/Q2]---> Support Team (Contact: "Anon User 1")
Visitor B ---[Q3/Q4]---> SMP Server ---[Q3/Q4]---> Support Team (Contact: "Anon User 2")
Visitor C ---[Q5/Q6]---> SMP Server ---[Q5/Q6]---> Support Team (Contact: "Anon User 3")
The support team can handle multiple conversations simultaneously in the SimpleX desktop or mobile app.
GoChat supports two communication profiles. Both profiles use the same chat interface - the difference is in the transport layer, encryption strength, and relay infrastructure. The transport layer is abstracted behind a ChatTransport interface from day one, ensuring both profiles can be developed independently and swapped at runtime.
The standard profile for daily use. Speaks the SimpleX Messaging Protocol over WebSocket, fully compatible with all SimpleX clients and any SMP relay server - including SimpleX's own public infrastructure, self-hosted servers, or GoRelay.
Use cases: product support, online shops (with SimpleGo terminal), communities, families, small businesses, freelancers, education, personal websites.
On the receiving end: SimpleX Chat app (phone/desktop) or a SimpleGo hardware terminal. No special software required.
Website visitor (browser) Any SMP Server Receiving end
| | |
|--- WSS + SMP --------------->| |
| E2E encrypted |--- SMP relay --------->|
| | | SimpleX App (phone/desktop)
|<-- WSS + SMP ----------------|<-- SMP relay ----------| or SimpleGo terminal
| E2E encrypted | |
An additional security layer for environments where standard encryption is not sufficient. Uses the GoRelay Protocol (GRP) over WebSocket, exclusively through GoRelay infrastructure.
Use cases: journalism and source protection, whistleblower channels, government and public authorities, healthcare, critical infrastructure, legal and financial services, defense, NGOs and human rights.
On the receiving end: SimpleGo hardware (ESP32-S3) with no smartphone OS, no baseband processor, and hardware-backed key storage.
Website visitor (browser) GoRelay Server Receiving end
| | |
|--- WSS + GRP --------------->| |
| Noise + Post-quantum |--- GRP relay --------->|
| Two-hop routing | Cover traffic | SimpleGo hardware
| Cover traffic | | (ESP32-S3)
|<-- WSS + GRP ----------------|<-- GRP relay ----------|
| | |
| Property | SMP (TLS 1.3) | GRP (Noise IK) |
|---|---|---|
| Handshake size | 1-4 KB | 96-144 bytes |
| Cipher negotiation | Yes (dozens of options) | No (fixed suite, cannot downgrade) |
| Certificate authority | Required (X.509 chain) | Not needed (key IS identity) |
| Identity hiding | Server visible via SNI | Both parties encrypted |
| Deniability | No (signatures in handshake) | Yes (DH only, no signatures) |
| Post-quantum | Not mandatory | Mandatory (X25519 + ML-KEM-768) |
| Routing | Direct (single hop) | Mandatory two-hop relay |
| Cover traffic | No | Yes (Poisson-distributed) |
The elimination of cipher negotiation is the most significant security improvement. TLS has spent two decades fighting downgrade attacks (POODLE, FREAK, Logjam, ROBOT) caused by its negotiation mechanism. Noise cannot have downgrade attacks because there is nothing to downgrade. One fixed cipher suite per protocol version: Noise_IK_25519_ChaChaPoly_BLAKE2s.
Both profiles implement the same abstract transport interface. This is a day-one architectural requirement - all code that sends or receives messages must go through this interface, never directly through a WebSocket.
interface ChatTransport {
connect(server: ServerAddress): Promise<void>
send(block: Uint8Array): Promise<void>
onMessage(handler: (block: Uint8Array) => void): void
close(): void
}
// Season 2-9: SMPWebSocketTransport implements ChatTransport
// Season 12+: GRPWebSocketTransport implements ChatTransportThe ChatTransport abstraction ensures:
- SMP and GRP code never leak into each other
- Profile switching at runtime requires only swapping the transport instance
- Testing can use a mock transport without network dependencies
- Future transport types (HTTP/2 long-polling fallback, WASM-based) slot in without changing application code
GoRelay's dual-protocol architecture allows cross-protocol message delivery. SMP on port 5223, GRP on port 7443, shared QueueStore. A message arriving via SMP can be delivered to a GRP subscriber and vice versa. This means a visitor using the SMP profile can reach a SimpleGo hardware device behind GRP - GoRelay handles the translation transparently.
| Feature | SMP Profile | GRP Profile |
|---|---|---|
| Protocol | SimpleX Messaging Protocol | GoRelay Protocol |
| Transport | WebSocket + TLS | WebSocket + Noise |
| Key exchange | X25519 + Double Ratchet | X25519 + ML-KEM-768 (post-quantum) |
| Relay servers | Any SMP server | GoRelay only |
| Routing | Direct | Mandatory two-hop |
| Cover traffic | No | Yes (Poisson-distributed) |
| Cipher negotiation | TLS cipher suites | None (fixed, no downgrade possible) |
| Identity hiding | Server visible via SNI | Both parties encrypted |
| Server seizure resistance | Standard encryption | Shamir split across servers (future) |
| Traffic disguise | None | Steganographic transport (future) |
| Queue auth | Ed25519 signature | Zero-knowledge proof (future) |
| Compatible with | All SimpleX clients | SimpleGo hardware |
| Use case | Everyday communication | High-security environments |
GoRelay's roadmap includes a Triple Shield layer adding three defense mechanisms on top of the existing encryption stack:
- 6a: Zero-Knowledge Queue Authentication - Schnorr DLOG via Fiat-Shamir. Clients prove queue ownership without revealing their public key. The server learns exactly one bit: "this client has the right key" or not.
- 6b: Shamir's Secret Sharing (2-of-3) - Each encrypted message is split into shares across multiple servers. Any single share contains mathematically zero information. Information-theoretic security that holds even against unlimited computing power.
- 6c: Steganographic Transport - GRP traffic wrapped in protocols mimicking legitimate web traffic. Pluggable Transports framework (HTTPS, WebSocket, meek, obfs4).
When all active, defeating a GoChat-to-SimpleGo communication requires simultaneously breaking eight independent defense layers. Failure at any single point leaves the attacker with nothing.
| Component | Status | Location | Notes |
|---|---|---|---|
| SMP server with all commands | DONE | src/Simplex/Messaging/ |
NEW, SUB, SEND, MSG, ACK, KEY, DEL, LGET, LNK |
| WebSocket on port 443 | DONE | PR #1738, 2026-03-20 | Browser WS and native TLS on same port via SNI routing |
| ALPN fix for browser WebSocket | DONE | PR #1738 | Server ALPN list includes "h2"/"http/1.1", browser gets v6-18 |
| Contact address / short links | DONE | Protocol level | Permanent contact address for support team |
| XFTP file transfer | DONE | Separate protocol | Could be used for file attachments later |
| TLS + identity verification | DONE | Server handshake | Ed25519 certificate chain verification |
These modules are already battle-tested in the live file transfer tool at simplex.chat/file/.
| Component | Status | File | Notes |
|---|---|---|---|
| Binary encoding/decoding | DONE | xftp-web/src/protocol/encoding.ts |
Full SMP wire format: Decoder class, encodeBytes, decodeLarge, Word16/32/64, Bool, Maybe, List, NonEmpty |
| HTTP/2 transport | DONE | xftp-web/src/client.ts |
Browser fetch() transport with timeout, retry, reconnect logic |
| Server handshake | DONE | xftp-web/src/protocol/handshake.ts |
Client hello, server handshake decode, version negotiation |
| Server identity verification | DONE | xftp-web/src/crypto/identity.ts |
Ed25519 certificate chain + web challenge verification |
| X25519 key exchange | DONE | xftp-web/src/crypto/keys.ts |
generateX25519KeyPair, DH key agreement, public key encoding |
| NaCl secretbox encryption | DONE | xftp-web/src/crypto/secretbox.ts |
XSalsa20-Poly1305 authenticated encryption |
| SHA-256 / SHA-512 digests | DONE | xftp-web/src/crypto/digest.ts |
Using @noble/hashes |
| Content padding | DONE | xftp-web/src/crypto/padding.ts |
Block padding/unpadding for fixed-size transmissions |
| Server address parsing | DONE | xftp-web/src/protocol/address.ts |
Parse server URLs with key hash verification |
| Connection agent (pool) | DONE | xftp-web/src/client.ts |
Connection pooling, sequential command queue, auto-reconnect |
| Transmission framing | DONE | xftp-web/src/protocol/transmission.ts |
Block encoding/decoding, session-based auth, correlation IDs |
| Component | Status | File | Notes |
|---|---|---|---|
| Package setup | DONE | smp-web/package.json |
@simplex-chat/smp-web v0.1.0, depends on xftp-web |
| TypeScript config | DONE | smp-web/tsconfig.json |
ES2022 target, strict mode |
| Re-exported encoding primitives | DONE | smp-web/src/index.ts |
Decoder, encodeBytes, decodeBytes, etc. |
| SMP transmission encode/decode | DONE | smp-web/src/protocol.ts |
encodeTransmission(), decodeTransmission() with auth field |
| LGET command encoding | DONE | smp-web/src/protocol.ts |
Link GET - retrieve connection data from short link |
| LNK response decoding | DONE | smp-web/src/protocol.ts |
Parse senderId, encFixedData, encUserData from link response |
| SMP response dispatch | DONE | smp-web/src/protocol.ts |
Switch on LNK / OK / ERR tags |
| ASCII helper | DONE | smp-web/src/protocol.ts |
String to Uint8Array conversion for command tags |
| Hash | Date | Description |
|---|---|---|
4b89a7f |
2026-03-22 | encoding/decoding of LGET/LNK - Core SMP protocol in TypeScript |
3eeffff |
2026-03-22 | smp web: initial setup - Package scaffolding |
01fe841 |
2026-03-20 | smp: allow websocket connections on the same port (#1738) - WebSocket transport on SMP server |
1a12ee0 |
2026-03-20 | xftp-web: version bump to 0.3.0 (#1742) - Updated shared dependency |
dc2921e |
2026-03-18 | xftp-server: embed file download widget (#1733) - Web embedding pattern |
328d3b9 |
2026-03-11 | xftp-web: use XFTP server domain in share link (#1732) - Security improvement |
Priority: Completed in Seasons 2-3.
All transport code implements the ChatTransport interface defined in section 3.4.
Tasks:
- WS-1: Create
smp-web/src/transport.ts- WebSocket transport class - WS-2: Create
smp-web/src/client.ts- SMP client with handshake - WS-3: Connection pooling and reconnect
- WS-4: SharedWorker for tab persistence
Priority: Completed in Season 5.
Tasks:
- BC-1: Browser client API (
browser-client.ts) - BC-2: Browser bundle (esbuild)
- BC-3: Browser client integration tests
- CHAT-1: Chat panel UI
- INFRA-1: SMP server deployment
- INFRA-2: Nginx WSS reverse proxy
- INFRA-3: Contact address setup
- PROTO-1 through PROTO-15: Real server protocol fixes
Priority: Completed in Season 6.
Tasks:
- INV-1: AgentInvitation builder (
invitation.ts) - INV-2: connReq URI builder
- INV-3: NaCl encryption fix
- INV-4: 12 protocol fixes
Priority: Completed in Season 7.
Season 7 discovered and resolved the root cause chain preventing the SimpleX CLI from accepting GoChat invitations. The CLI sends SKEY (sender key registration, v9+ Fast Duplex) before SEND. SKEY failed with AUTH because the queue had no sndSecure, which requires v9+ negotiation. The server only offered v6 over WebSocket because browser connections lack ALPN "smp/1", causing the server to fall back to legacy v6-only mode.
The fix required building the SMP server from Evgeny's unmerged PR #1738, which extends the server's ALPN list to include "h2" and "http/1.1". This allows browser WebSocket connections to get the full v6-18 protocol range. The infrastructure was overhauled: Nginx was eliminated, Docker maps port 443 directly to host port 8444, and a 4096-bit RSA Let's Encrypt certificate was generated for the HTTPS handler.
Tasks:
- S7-1: Identify SKEY as AUTH root cause (CLI debug logging)
- S7-2: Identify ALPN as version-range root cause (SimpleGo team + Haskell source)
- S7-3: Test sndSecure on v6 (3 attempts, all CMD SYNTAX - v6 parser limitation)
- S7-4: Upgrade server to v6.5.0-beta.6 (still v6-v6 over WebSocket)
- S7-5: Find and analyze PR #1738 source code
- S7-6: Build SMP server from PR #1738 branch (Haskell Docker build)
- S7-7: Deploy with 4096-bit RSA Let's Encrypt cert
- S7-8: Eliminate Nginx, Docker direct port mapping (8444->443)
- S7-9: Verify v6-18 over WebSocket (confirmed in browser console)
- S7-10: Cap maxSMPClientVersion to 6 (v7 auth not yet implemented)
Priority: Completed in Season 8.
Season 8 implemented SMP v9 command authorization (CbAuthenticator), rebuilt the server infrastructure from scratch (Debian 13, no Plesk), processed incoming MSG pushes with server-to-recipient decryption, and decrypted the CLI's AgentConfirmation through the NaCl Layer 1 envelope. The critical breakthrough was discovering that Haskell's cryptoBox includes an HSalsa20 key derivation step internally, meaning JavaScript must use nacl.box instead of nacl.secretbox.
Tasks:
- S8-1: Parse server auth X25519 key from ServerHello CertChainPubKey
- S8-2: Implement CbAuthenticator (nacl.box over SHA-512 hash of authorized data)
- S8-3: Raise maxSMPClientVersion to 9, ClientHello with session X25519 key
- S8-4: Per-queue auth keys: Ed25519 -> X25519 for CbAuthenticator
- S8-5: v9 NEW command format with Maybe BasicAuth '0' + sndSecure 'T' (97 bytes)
- S8-6: Fix SMP Maybe encoding: ASCII '0' (0x30), not binary 0x00
- S8-7: Fix CbAuthenticator: nacl.box (DH+HSalsa20+XSalsa20), not nacl.secretbox
- S8-8: Server infrastructure rebuild (Debian 13, Nginx + Certbot, Docker)
- S8-9: Remove debug logging, clean up protocol.ts and handshake.ts
- S8-10: MSG processing with server-to-recipient decryption (nacl.box.open)
- S8-11: Parse RcvMsgBody (SystemTime=12B, not 8B)
- S8-12: ACK with CbAuthenticator
- S8-13: Layer 1 NaCl decryption of smpEncConfirmation
- S8-14: Parse smpConfirmation (sender key + AgentConfirmation body)
Priority: Completed in Season 9.
Season 9 implemented the full end-to-end encryption pipeline: parsing AgentConfirmation with PQ KEM (SNTRUP761) support, X3DH key agreement (receiver side, 3x X448 DH + HKDF-SHA512), Double Ratchet encrypt and decrypt (AES-256-GCM with 16-byte IVs), reply queue parsing from AgentConnInfoReply, and the duplex handshake (sending AgentConfirmation back to CLI). HELLO received and CONNECTION ESTABLISHED. Chat messages decrypted.
Tasks:
- S9-1: Parse AgentConfirmation with E2ERatchetParams + PQ KEM (PR #67, #70, #71)
- S9-2: X3DH key agreement - receiver side (PR #68)
- S9-3: Double Ratchet decrypt for encConnInfo (PR #69)
- S9-4: Fix MsgHeader KEM skip + HELLO handler (PR #72, #73, #74)
- S9-5: Parse reply queue from AgentConnInfoReply tag 'D' (PR #75)
- S9-6: Ratchet encrypt + send handshake to reply queue (PR #76)
- S9-7: Fix handshake format: tag 'C', PHConfirmation 'K', e2eEncryption_=Nothing (PR #77)
- S9-8: Fix smpClientVersion in ClientMsgEnvelope PubHeader (direct)
Code Map (Season 9 additions):
| File | Purpose |
|---|---|
| agent-confirmation.ts | Parse AgentConfirmation with E2ERatchetParams + PQ KEM |
| x3dh-agreement.ts | X3DH receiver-side (3x X448 DH + HKDF-SHA512) |
| ratchet-decrypt.ts | Double Ratchet init + encrypt (rcEncrypt) + decrypt (rcDecrypt) |
| reply-queue.ts | Parse SMPQueueInfo from AgentConnInfoReply tag 'D' |
Priority: Completed in Season 10.
Season 10 delivered bidirectional E2E encrypted chat between browser and SimpleX Desktop App, a multi-step UX flow with visitor name support, offline messaging, and delete confirmation. 11 PRs merged (#79-#89). Critical discoveries: gochat-client.js must never inject DOM elements (showNameInput destroyed external UI), setStatus must only fire from state machine after HELLO (not from connectWithName), and incoming JSON events must be parsed before display.
Tasks:
- S10-1: Fix WebSocket subscription after HELLO (PR #79)
- S10-2: Send HELLO and chat messages (agentVersion=1) (PR #80)
- S10-3: PING/PONG keep-alive 30s interval (PR #81)
- S10-4: Fix sendHello crash + state transition (PR #82)
- S10-5: Wire sendChatMessage through encrypted pipeline (PR #83)
- S10-6: Encode sndMsgId as Word64 BE (PR #84)
- S10-7: Message buffer + Desktop App docs (PR #85)
- S10-8: Visitor name + .env config + admin config (PR #86, #87)
- S10-9: Visitor name input in widget (PR #88)
- S10-10: Remove DOM injection + fix status + event handling (PR #89)
Priority: Completed in Season 11.
Season 11 delivered bidirectional delivery receipts (double checkmarks), connection lifecycle management (END detection, timeout, x.direct.del send), and .env integration for the SimpleGo website. The receipt msgHash bug required three attempts to resolve: the full agentMessage buffer must be hashed, not a subset. The onSubscriptionEnd handler existed in the transport layer but was never wired - one line of code activated END detection.
Tasks:
- S11-1: Parse incoming delivery receipts - A_RCVD inner_tag 'V' (PR #91)
- S11-2: Send delivery receipts for received chat messages (PR #91)
- S11-3: Fix receipt msgHash scope - full agentMessage buffer (PR #92, #93)
- S11-4: Wire onSubscriptionEnd for queue END detection (PR #94)
- S11-5: Add connection timeout for unresponsive agents (PR #94)
- S11-6: Send x.direct.del notification before disconnect (PR #95)
- S11-7: chat.js receipt UI - onDeliveryReceipt callback, pendingChecks, upgradeCheck()
- S11-8: .env integration - dotenv in 11ty, template variables in base.njk
Priority: Completed in Season 3.
- CMD-1: Queue creation commands (NEW/IDS)
- CMD-2: Sender commands (SEND/SKEY)
- CMD-3: Recipient commands (SUB/ACK/KEY/DEL/MSG)
- CMD-4: Connection link commands (LGET/LNK/LSND)
- CMD-5: Utility commands (PING/PONG/ERR)
Priority: Completed in Season 4.
- CONN-1: Contact address parsing
- CONN-2: Connection state machine
- CONN-3: Queue pair creation
- CONN-4: Connection request with full crypto stack
Priority: Completed in Season 9.
- E2E-1: Ratchet receive side (decrypt incoming messages)
- E2E-2: Symmetric ratchet step (advance chain on each message)
- E2E-3: DH ratchet step (re-key on send/receive transitions)
- E2E-4: Out-of-order message handling (skipped message keys)
- E2E-5: Header decryption with current + next header key
- E2E-6: Key storage (IndexedDB + AES-256-GCM encryption at rest)
- SEC-3: Web Worker isolation for all crypto operations
Priority: Medium - partially done in Season 5, remaining tasks in Season 9.
- CHAT-1: Chat panel UI (Season 5)
- UI-1: Nose-bar integration
- UI-2 through UI-8: Remaining UI tasks
Priority: High - Season 12.
- SEC-1: Content Security Policy implementation
- SEC-2: Subresource Integrity for all external scripts
- SEC-3: Web Worker isolation for crypto operations
- SEC-4: Security documentation
- SEC-5: TLS certificate strategy (partially resolved in S5/S7)
- OPS-1: SMP server deployment (S5, upgraded S7)
- OPS-2: Contact address setup (S5)
- OPS-3: Monitoring
- GRP-1: Noise Protocol transport
- GRP-2: Mandatory post-quantum key exchange
- GRP-3: Two-hop relay routing
- GRP-4: Triple Shield integration
Goal: Resolve the AUTH error when the CLI accepts GoChat's invitation by enabling the full SMP protocol range (v6-18) over WebSocket.
Status: COMPLETE (Season 7)
- Identified SKEY as the immediate cause of AUTH (CLI debug logging)
- Identified ALPN as the root cause of v6-only over WebSocket
- Tested sndSecure on v6 (3 PRs, all CMD SYNTAX - v6 parser limitation)
- Found PR #1738 "smp: allow websocket connections on the same port"
- Built SMP server from PR #1738 Haskell source
- Deployed with 4096-bit RSA Let's Encrypt certificate
- Eliminated Nginx, Docker direct port mapping (8444->443)
- Verified: browser now gets ServerHello v6-18, negotiates v6
- Capped maxSMPClientVersion to 6 (v7 auth not yet implemented)
- Result: NEW/IDS/SEND/OK all working on new server infrastructure
Goal: Implement v9 CbAuthenticator, rebuild server, process MSG, decrypt Layer 1.
Status: COMPLETE (Season 8)
- Implemented CbAuthenticator (nacl.box over SHA-512, 80-byte authenticator)
- Raised maxSMPClientVersion to 9, ClientHello with X25519 session key
- Per-queue auth keys changed from Ed25519 to X25519
- v9 NEW command with Maybe BasicAuth '0' + sndSecure 'T' (97 bytes)
- Discovered HSalsa20 requirement: nacl.box not nacl.secretbox
- Server rebuilt from scratch (Debian 13, Nginx + Certbot, Docker)
- MSG processing with server-to-recipient decryption
- RcvMsgBody parsing (SystemTime = 12 bytes, not 8)
- ACK with CbAuthenticator
- Layer 1 NaCl decryption of smpEncConfirmation
- smpConfirmation parsing (sender key + AgentConfirmation body)
- Result: CLI accepts connection, AgentConfirmation received and decrypted
- 494 tests across 19 files
Goal: Parse AgentConfirmation, perform X3DH with real keys, initialize Double Ratchet, exchange HELLO messages, achieve CON state.
- Parse AgentConfirmation (e2e params, X448 keys, connInfo)
- X3DH key agreement with real peer X448 keys
- Initialize Double Ratchet from X3DH output
- Decrypt HELLO from peer (first Ratchet message)
- Send HELLO to peer
- Achieve CON ("CONNECTED") state
- Bidirectional encrypted messaging via Double Ratchet
- Document SMP-VERSIONS.md and SMP-HANDSHAKE.md
Goal: Production-quality bidirectional E2E chat with Desktop App.
Status: COMPLETE (Season 10)
- Bidirectional E2E encrypted messaging with SimpleX Desktop App
- Multi-step UX flow (Start -> Name -> Waiting -> Chat)
- Visitor name support (custom or random guest)
- Offline messaging with single-message limit
- Delete confirmation + Hollywood destruction sequence
- Event handling (x.direct.del, x.msg.new)
- 544+ tests, 11 PRs (#79-#89)
Goal: Delivery confirmations, connection end handling, build-time configuration.
Status: COMPLETE (Season 11)
- Bidirectional delivery receipts (A_RCVD, tag 'V', double checkmarks)
- Receipt msgHash fix (full agentMessage buffer, 3 attempts)
- chat.js receipt UI (onDeliveryReceipt, pendingChecks, upgradeCheck)
- Connection END detection (onSubscriptionEnd wired)
- Connection timeout for unresponsive agents (2 minutes)
- x.direct.del send before disconnect
- .env integration (dotenv + 11ty + base.njk template variables)
- 551+ tests, 5 PRs (#91-#95)
Goal: Production-ready encrypted support chat with security hardening.
- Browser security hardening: CSP, SRI, Web Worker isolation (SEC-1 to SEC-5)
- Dependency vendoring (@noble libraries into repo)
- Error handling and user-facing error states
- Performance optimization and bundle size
- Security review and documentation (SEC-4)
Goal: High-security communication via GoRelay.
Each SMP transmission is a fixed-size block (16,384 bytes) with '#' padding (0x23):
[auth: ByteString] [corrId: ByteString] [entityId: ByteString] [command: raw bytes]
GoChat/
smp-web/
src/
index.ts # Re-exports all public API
types.ts # ChatTransport interface, SMP types
transport.ts # SMPWebSocketTransport (16KB block framing)
handshake.ts # SMP ServerHello/ClientHello encode/decode
client.ts # SMPClient (handshake, session, typed commands, corrId dispatch)
agent.ts # SMPClientAgent (connection pool, reconnection)
commands.ts # SMP command encoders (14 commands)
protocol.ts # SMP transmission encode/decode, response decoder
address.ts # SimpleX contact address URI parser (S4)
state.ts # Connection lifecycle state machine (S4)
crypto-utils.ts # Key gen: Ed25519, X25519, X448 + SPKI encoding (S4)
connection.ts # ConnectionManager (queue creation + send request) (S4)
x3dh.ts # Modified 4-DH X3DH key agreement (S4)
ratchet.ts # Double Ratchet init + first encrypt AES-256-GCM (S4)
agent-envelope.ts # Agent confirmation encoding (S4)
connection-request.ts # Connection request builder + zstd (S4)
browser-client.ts # High-level browser API (S5)
invitation.ts # AgentInvitation builder (S6)
__tests__/ # 551+ tests across 23 files
+-- src/assets/css/chat.css # Chat panel styles (S5, lives in SimpleGo www)
+-- src/assets/js/chat.js # Chat panel logic (S5, lives in SimpleGo www)
+-- _includes/base.njk # Chat HTML integration (S5, lives in SimpleGo www)
xftp-web/ # Shared infrastructure (upstream)
src/
client.ts # HTTP/2 transport, handshake, retry logic
protocol/
encoding.ts # Binary encoding (Decoder, encode/decodeBytes, etc.)
transmission.ts # Block framing, session auth
handshake.ts # Client/server handshake, version negotiation
crypto/
keys.ts # X25519 key generation + DH
secretbox.ts # NaCl XSalsa20-Poly1305
identity.ts # Ed25519 server identity verification
digest.ts # SHA-256 / SHA-512
docs/
PROTOCOL.md # This file
RESEARCH.md # Browser crypto, security, design research
SECURITY-HARDENING-ROADMAP.md # Six-phase browser security hardening plan
seasons/
SEASON-PLAN.md # Season overview and workflow
SEASON-01 through 06 # Season closing protocols
SEASON-07-server-upgrade.md # Season 7 closing protocol
SEASON-11-receipts-lifecycle.md # Season 11 closing protocol
| Package | Version | Purpose |
|---|---|---|
@noble/hashes |
^1.5.0 | SHA-256, SHA-512 (used by xftp-web crypto) |
@noble/curves |
latest | Ed25519, X25519 (6 audits, used by Proton Mail, MetaMask) |
@noble/ciphers |
latest | XSalsa20-Poly1305, AES-256-GCM |
zstd-codec |
latest | Zstd compression (level 3) for connInfo payload |
@simplex-chat/xftp-web |
file:../xftp-web | Encoding, crypto, transport primitives |
typescript |
^5.4.0 | Build tooling |
ws |
^8.0.0 | WebSocket (dev/test only, browser uses native WebSocket) |
| Command | Direction | Format | Purpose |
|---|---|---|---|
NEW |
Client -> Server | NEW <rcvPubKey> <dhPubKey> <subscribeMode> [sndSecure] |
Create new queue |
IDS |
Server -> Client | IDS <rcvId> <sndId> <srvDhPubKey> |
Queue IDs response |
SUB |
Client -> Server | SUB (entityId = queueId) |
Subscribe to queue |
KEY |
Client -> Server | KEY <sndKey> |
Set sender key on queue |
SKEY |
Client -> Server | SKEY <sndKey> |
Sender registers own key (v9+) |
SEND |
Client -> Server | SEND <msgFlags> <msgBody> |
Send message |
MSG |
Server -> Client | MSG <msgId> <ts> <msgFlags> <msgBody> |
Receive message (async push) |
ACK |
Client -> Server | ACK <msgId> |
Acknowledge receipt |
DEL |
Client -> Server | DEL |
Delete queue |
PING |
Client -> Server | PING |
Keepalive |
PONG |
Server -> Client | PONG |
Keepalive response |
OK |
Server -> Client | OK |
Success |
ERR |
Server -> Client | ERR <errorType> |
Error |
END |
Server -> Client | END |
Queue subscription ended (S11) |
Documented during 15 protocol fixes against real SMP v6.4.5.1 server, with critical assistance from the SimpleGo protocol analysis team (49 sessions, 81 bugs, 270 lessons).
ServerHello (WebSocket mode - no certs):
[2B contentLen][versionMin 2B][versionMax 2B][sessIdLen 1B][sessionId 32B]['#' padding]
ServerHello (PR #1738 WebSocket mode - with certs):
[2B contentLen][versionMin 2B][versionMax 2B][sessIdLen 1B][sessionId 48B][NonEmpty certs][signedKey]['#' padding]
Note: sessionId is 48 bytes on PR #1738 server (vs 32 bytes on legacy WebSocket port). Certificate chain and signed DH key are present because the server handles TLS directly.
ClientHello (v6):
[2B contentLen][version 2B][keyHashLen 1B][keyHash 32B]['#' padding]
ClientHello (v7+):
[2B contentLen][version 2B][keyHashLen 1B][keyHash 32B][authPubKey][proxyServer][clientService]['#' padding]
v7+ adds authPubKey (X25519 SPKI) for DH-based command authorization. This replaces Ed25519 signature-based auth.
v6 command transmission:
[sigLen=0x40][64B Ed25519 signature]
[sessIdLen=0x20][32B sessionId]
[corrIdLen=0x18][24B corrId]
[entityIdLen][entityId]
[command bytes]
v6 signed data (CRITICAL):
signedData = [0x20][sessionId 32B] + [corrIdLen][corrId] + [entityIdLen][entityId] + [command]
The 0x20 length prefix before sessionId MUST be included. Without it: ERR AUTH.
v6 NEW command:
"NEW " [0x2C][44B Ed25519 SPKI authKey][0x2C][44B X25519 SPKI dhKey]"S"
No spaces between fields. No basicAuth. No sndSecure on v6. Keys use 1-byte shortString prefix.
v9+ NEW command with sndSecure:
"NEW " [0x2C][44B authKey][0x2C][44B dhKey] "S T"
Space between subscribeMode "S" and sndSecure "T". Total 97 bytes. Requires v9+ negotiation. Creates queue with sk= ready for SKEY registration.
AgentInvitation wire format (Season 6):
ClientMessage:
[0] '_' (0x5F) PHEmpty (1 byte, no auth key)
[1-2] 00 07 agentVersion (uint16 BE, v7)
[3] 'I' (0x49) AgentInvitation tag
[4-5] XX XX connReq URI length (uint16 BE, Large encoding)
[6-N] UTF-8 bytes connReq URI string
[N+1+] UTF-8 bytes connInfo JSON (Tail, no length prefix)
Delivery Receipt wire format (Season 11):
A_RCVD (inner_tag 'V'):
['M'][APrivHeader]['V'][count 1B Word8][AMessageReceipt...]
AMessageReceipt:
[8B agentMsgId Int64 BE]
[1B hashLen][32B msgHash SHA256]
[2B rcptInfo Word16]
CRITICAL: count is Word8 (NOT Word16), rcptInfo is Word16 (NOT Word32)
CRITICAL: msgHash = sha256(full agentMessage buffer), not just JSON body
CRITICAL: agentVersion=1 for outgoing receipts (NOT 7)
The ALPN problem and fix:
The SMP server uses TLS ALPN to decide which protocol version range to advertise:
smpVersionRange = maybe legacyServerSMPRelayVRange (const smpVRange) $ getSessionALPN c| Connection type | ALPN result | Version range |
|---|---|---|
| Native TLS client (sends "smp/1") | Just "smp/1" |
v6-v18 (full) |
| Browser WebSocket (legacy server) | Nothing |
v6-v6 (legacy) |
| Browser WebSocket (PR #1738 server) | Just "h2" |
v6-v18 (full) |
PR #1738 extends the server's ALPN list to ["smp/1", "h2", "http/1.1"]. When a browser connects and proposes "h2", the server matches it. Since getSessionALPN returns Just "h2" (not Nothing), the full v6-v18 range is advertised.
v7+ command authorization (Season 8 prerequisite):
v6 uses Ed25519 signatures for command authorization. v7+ uses X25519 DH-based authorization (crypto_box). The transition is defined in PR #982 "smp: command authorization":
- ClientHello includes
authPubKey(X25519 SPKI) - Commands are authorized via crypto_box instead of Ed25519 signatures
- The server DH key from ServerHello is used as the peer key
- Per-command ephemeral keys may be used
To use sndSecure, the client must negotiate v9+ which requires implementing v7+ auth first.
GRP/1 cipher suite (non-negotiable):
Noise Pattern: IK (primary), XX (fallback)
Key Exchange: X25519 + ML-KEM-768 hybrid (FIPS 203)
KDF: HKDF-SHA-256
AEAD: ChaCha20-Poly1305
Hash: BLAKE2s
Signatures: Ed25519 (upgrade path to ML-DSA-65 in GRP/2)
Block Size: 16,384 bytes with '#' padding (matches SMP)
Rekeying: Every 2-5 minutes or 1000 messages
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| SimpleX changes smp-web API before release | High | Medium | Pin to specific commit, contribute upstream |
| v7+ command auth complexity | Medium | Medium | Reference PR #982, Evgeny's code on ep/smp-web |
| Double Ratchet implementation complexity | Medium | High | Start with NaCl box MVP, upgrade later |
| WebSocket blocked by corporate firewalls | Medium | Low | HTTP/2 long-polling fallback (future) |
| PR #1738 not merged upstream | Low | Medium | We build from source, can track changes |
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| XSS defeating all encryption | Critical | Medium | Strict CSP, SRI, Web Worker isolation |
| Server-delivered code manipulation | Critical | Low | Reproducible builds, SRI hashes |
| npm supply chain attack | High | Medium | @noble-only crypto, lockfile pinning |
| Cross-Site WebSocket Hijacking | High | Low | Token-based auth (not cookies) |
| Let's Encrypt cert renewal | Medium | Low | certbot auto-renewal, manual cert copy to Docker |
-
v7+ auth implementation: How exactly does X25519 DH-based command authorization work? Need to study PR #982 and Evgeny's ep/smp-web code.
-
Upstream contribution: Should we contribute our WebSocket transport client back to the
smp-webpackage? -
Server choice: Self-hosted SMP server vs using SimpleX's public servers?
-
Message persistence: How long should chat history persist in the browser?
-
Let's Encrypt renewal: Automate cert copy to Docker mount after certbot renewal.
GoChat is one component of the SimpleGo ecosystem for encrypted communication across platforms.
| Project | What it does | Language | Status |
|---|---|---|---|
| SimpleGo | First native C implementation of SimpleX protocol on ESP32-S3. | C (21,863 lines) | Alpha, 7 contacts verified |
| GoRelay | Dual-protocol relay server. SMP + GRP. | Go (~5,000 lines) | Alpha, SimpleX test passing |
| GoChat | Browser-native encrypted messenger. (This project) | TypeScript | Season 11 complete |
| ID | Layer | Description | Season |
|---|---|---|---|
| WS-1 | Transport | WebSocket transport class (ChatTransport) | S2 DONE |
| WS-2 | Transport | SMP client with handshake | S2 DONE |
| WS-3 | Transport | Connection pooling and reconnect | S2 DONE |
| WS-4 | Transport | SharedWorker for tab persistence | S9 |
| CMD-1 | Commands | Queue creation (NEW/IDS) | S3 DONE |
| CMD-2 | Commands | Sender commands (SEND/SKEY) | S3 DONE |
| CMD-3 | Commands | Recipient commands (SUB/ACK/KEY/DEL/MSG) | S3 DONE |
| CMD-4 | Commands | Connection link commands (LGET/LNK/LSND) | S3 DONE |
| CMD-5 | Commands | Utility commands (PING/PONG/ERR) | S3 DONE |
| CONN-1 | Connection | Contact address parsing (all formats) | S4 DONE |
| CONN-2 | Connection | Connection state machine (7 states) | S4 DONE |
| CONN-3 | Connection | Queue pair creation + ConnectionManager | S4 DONE |
| CONN-4 | Connection | Connection request (X3DH + Ratchet + 6 crypto layers) | S4 DONE |
| E2E-1 | Encryption | Ratchet receive side (decrypt incoming) | S8 |
| E2E-2 | Encryption | Symmetric ratchet step (advance chain) | S8 |
| E2E-3 | Encryption | DH ratchet step (re-key on transitions) | S8 |
| E2E-4 | Encryption | Out-of-order message handling | S8 |
| E2E-5 | Encryption | Header decryption (current + next key) | S8 |
| E2E-6 | Encryption | Key storage (IndexedDB + AES-256-GCM) | S8 |
| BC-1 | Browser Client | Browser client API (browser-client.ts) | S5 DONE |
| BC-2 | Browser Client | Browser bundle (esbuild IIFE) | S5 DONE |
| BC-3 | Browser Client | Integration tests (24 tests, 7 scenarios) | S5 DONE |
| CHAT-1 | Chat UI | Chat panel (CSS, JS, HTML, left-docked, responsive) | S5 DONE |
| INFRA-1 | Infrastructure | SMP server Docker deployment (smp.simplego.dev) | S5 DONE |
| INFRA-2 | Infrastructure | Nginx WSS reverse proxy (port 8444) | S5 DONE (eliminated S7) |
| INFRA-3 | Infrastructure | Contact address setup | S5 DONE |
| PROTO-1:15 | Protocol | 15 real-server SMP v6 protocol fixes | S5 DONE |
| INV-1 | Invitation | AgentInvitation builder (invitation.ts) | S6 DONE |
| INV-2 | Invitation | connReq URI builder (smp + e2e params) | S6 DONE |
| INV-3 | Invitation | NaCl encryption fix (nacl.box vs raw cipher) | S6 DONE |
| INV-4 | Invitation | 12 protocol fixes (A_CRYPTO + A_MESSAGE) | S6 DONE |
| SEC-6 | Security | Security hardening roadmap document | S6 DONE |
| S7-1:10 | Infrastructure | Server upgrade, ALPN fix, PR #1738 build, Nginx elimination | S7 DONE |
| S8-1 | Auth | Parse server auth X25519 key from ServerHello | S8 DONE |
| S8-2 | Auth | CbAuthenticator (nacl.box over SHA-512) | S8 DONE |
| S8-3 | Auth | Negotiate v9, ClientHello with X25519 session key | S8 DONE |
| S8-4 | Auth | Per-queue auth keys Ed25519 -> X25519 | S8 DONE |
| S8-5 | Protocol | v9 NEW with Maybe BasicAuth '0' + sndSecure 'T' | S8 DONE |
| S8-6 | Protocol | Fix SMP Maybe encoding (ASCII '0', not binary 0x00) | S8 DONE |
| S8-7 | Crypto | Fix CbAuth: nacl.box not nacl.secretbox (HSalsa20) | S8 DONE |
| S8-8 | Infrastructure | Server rebuild (Debian 13, Nginx + Certbot, Docker) | S8 DONE |
| S8-9 | Protocol | Remove debug logging, clean up protocol.ts | S8 DONE |
| S8-10 | Protocol | MSG processing with server-to-recipient decryption | S8 DONE |
| S8-11 | Protocol | Parse RcvMsgBody (SystemTime=12B) | S8 DONE |
| S8-12 | Protocol | ACK with CbAuthenticator | S8 DONE |
| S8-13 | Crypto | Layer 1 NaCl decryption of smpEncConfirmation | S8 DONE |
| S8-14 | Crypto | Parse smpConfirmation (sender key + AgentConfirmation) | S8 DONE |
| S9-1:8 | Encryption | AgentConfirmation, X3DH, Ratchet, HELLO, CON | S9 DONE |
| S10-1:10 | Chat + UX | Bidirectional chat, visitor name, multi-step UX, destruction | S10 DONE |
| S11-1 | Protocol | Parse incoming delivery receipts (A_RCVD tag 'V') | S11 DONE |
| S11-2 | Protocol | Send delivery receipts for received chat messages | S11 DONE |
| S11-3 | Protocol | Fix receipt msgHash scope (full agentMessage buffer) | S11 DONE |
| S11-4 | Protocol | Wire onSubscriptionEnd for queue END detection | S11 DONE |
| S11-5 | Client | Add connection timeout for unresponsive agents | S11 DONE |
| S11-6 | Protocol | Send x.direct.del notification before disconnect | S11 DONE |
| S11-7 | UI | chat.js receipt UI (onDeliveryReceipt, pendingChecks) | S11 DONE |
| S11-8 | Config | .env integration (dotenv + 11ty + base.njk) | S11 DONE |
| SEC-1 | Security | Content Security Policy | S12 |
| SEC-2 | Security | Subresource Integrity | S12 |
| SEC-3 | Security | Web Worker crypto isolation | S12 |
| SEC-4 | Security | Trust boundary documentation | S12 |
| SEC-5 | Security | TLS certificate strategy | S5/S7 DONE |
| LIB-1:5 | Library | simplex-js npm library | S13 |
| GRP-1 | GRP Transport | Noise Protocol transport | S14 |
| GRP-2 | GRP Transport | ML-KEM-768 post-quantum | S14 |
| GRP-3 | GRP Transport | Two-hop relay routing | S15+ |
| GRP-4 | GRP Transport | Triple Shield (ZKP, Shamir, Stego) | S16+ |
| Date | Change |
|---|---|
| 2026-03-25 | Initial protocol document created. Analyzed ep/smp-web-spike branch, documented existing infrastructure, defined implementation roadmap. |
| 2026-03-25 | Major update: added dual-profile architecture (SMP + GRP), ChatTransport interface requirement, new task categories (GRP-1 to GRP-4, SEC-1 to SEC-5, UI-6 to UI-8, WS-4), browser-specific risk assessment, ecosystem context, ground rules, task registry. |
| 2026-03-25 | Season 2 complete. WS-1, WS-2, WS-3 implemented. |
| 2026-03-25 | Season 4 complete. CONN-1 to CONN-4 implemented. 226 new tests (413 total). |
| 2026-03-26 | Season 5 complete. Chat UI, browser client API, SMP server infrastructure, 15 protocol fixes. 485 tests. |
| 2026-03-28 | Season 6 complete. Connection request to SimpleX App. 12 protocol fixes. 493 tests. |
| 2026-03-28 | Season 7 complete. Server upgrade to PR #1738 build. ALPN fix enables v6-18 over WebSocket. Nginx eliminated, Docker direct port mapping. 4096-bit RSA cert. sndSecure confirmed as v9+ only (v6 parser limitation). v7+ command auth identified as Season 8 prerequisite. Added Section 5.1d, Phase 3c/3d, Section 7.6, S7 and S8 task IDs. Season numbers shifted: S8 = v7+ auth + bidirectional messaging, S9 = polish, S10 = security, S11 = library, S12+ = GRP. |
| 2026-03-30 | Season 8 complete. Implemented v9 CbAuthenticator (nacl.box over SHA-512), server rebuilt on Debian 13 (Nginx + Certbot + Docker), MSG processing with server-to-recipient decryption (nacl.box.open), Layer 1 NaCl decryption of smpEncConfirmation, RcvMsgBody parsing (SystemTime=12B), ACK with CbAuth. Key discoveries: HSalsa20 in nacl.box (not nacl.secretbox), Maybe encoding = ASCII '0'/'1' (not binary), four DH keypairs per connection, asymmetric smpEncConfirmation format. 494 tests. 14 S8 task IDs (all DONE). E2E tasks renumbered for S9 (X3DH + Double Ratchet + CON). Added Section 5.1e, Phase 3d/3e. |
| 2026-03-31 | Season 9 complete. AgentConfirmation parsing with SNTRUP761 PQ KEM support. X3DH key agreement (receiver side). Double Ratchet encrypt/decrypt (AES-256-GCM with 16-byte IV). Reply queue parsing from AgentConnInfoReply. Full duplex handshake (send AgentConfirmation to CLI). HELLO received - CONNECTION ESTABLISHED. Chat messages received and decrypted. 11 PRs (#67-#77), 537 tests. Added Section 5.1f with 8 S9 task IDs (all DONE). |
| 2026-04-01 | Season 10 complete. Bidirectional E2E chat with Desktop App. 11 PRs (#79-#89), 544+ tests. Visitor name, multi-step UX, offline messaging, delete confirmation. Critical: widget must never inject DOM, status only from state machine, parse JSON events. Added Section 5.1g with 10 S10 task IDs (all DONE). |
| 2026-04-02 | Season 11 complete. Delivery receipts (bidirectional, A_RCVD tag 'V', double checkmarks). Receipt msgHash fix (3 attempts, full agentMessage buffer). Connection lifecycle (END detection, timeout, x.direct.del send). .env integration (dotenv + 11ty). chat.js receipt UI. 5 PRs (#91-#95), 551+ tests. Key discoveries: msgHash = sha256(full agentMessage), dotenv # comment trap, onSubscriptionEnd was dead code. Added Section 5.1h, Phase 3g, delivery receipt wire format in 7.5, END in 7.4, 8 S11 task IDs (all DONE). |
