The Secure Chat Application (CryptNode) is a real-time communication platform utilizing a "Thick Client, Thin Server" architecture. The server acts solely as an ephemeral relay, while the client handles all business logic, encryption, storage, and state management.
- Language: Go (Golang)
- WebSocket Library: Gorilla WebSocket
- Database: SQLite (
go-sqlite3) — server-side only, stores ephemeral session metadata, pending friend requests, and offline notification queues (never stores message content) - Port: 9000
- Protocol: WebSocket (
ws://orwss://for production)
The relay server is intentionally minimal. It:
- Validates Authentication: Verifies Google ID Tokens against Google's OAuth API, or validates its own HMAC-signed session tokens
- Manages Sessions: Maintains ephemeral mappings of
SessionID → [Connected Clients] - Relays Encrypted Payloads: Forwards encrypted messages between clients in the same session
- Logs Connections: Records hashed connection attempts for security auditing
- Issues Session Tokens: Generates HMAC-signed tokens to reduce repeated OAuth calls
- Stores Pending Requests: Persists
FRIEND_REQUESTpayloads in SQLite so they can be delivered when the target user next comes online - Queues Offline Notifications: Stores
FRIEND_DENIED,USER_BLOCKED_EVENTnotifications for offline delivery - Hosts Static Website: Serves an informational/landing website with download links to precompiled binaries in
website/
- ❌ Decrypt message content (does not have session keys)
- ❌ Store chat history (all data is ephemeral, in-memory or pending-delivery only)
- ❌ View file contents (files are encrypted at the client)
- ❌ Access user profiles or contacts (no persistent user database)
type Client struct {
id string // Unique connection ID
email string // Google-verified email
conn *websocket.Conn // WebSocket connection
mu sync.Mutex // Thread-safe writes
publicKey string // Device ECDH public key (Base64)
}
type Session struct {
id string // Session ID (deterministic SHA-256 hash of sorted emails)
clients map[string]*Client // All clients in this session
mu sync.Mutex // Thread-safe access
}
type Server struct {
clients map[string]*Client // All connected clients
sessions map[string]*Session // All active sessions
emailToClientId map[string]string // Email → Active ClientID[] lookup (multi-device)
mu sync.Mutex // Thread-safe state mutations
logger *log.Logger // Connection logging
}The client is a React 18 application built with Vite and wrapped in Capacitor, enabling it to run as an Android app or Electron desktop app.
Primary business logic resides in singleton services that exist outside the React component tree, organized into domain-specific directories.
- Role: Central singleton controller. Acts as a facade over all sub-services. All UI interactions go through
ChatClient. - Key Functions:
init(): Loads sessions from SQLite on startup.login(token): Delegates toAuthService, triggers WS connection.lockSession(): Clears in-memory auth state, disconnects WS, returns to login screen without clearing the stored session token.logout(isManualLogout): Full logout — clears in-memory state AND wipes the stored session token fromSafeStorage.sendMessage(...): Delegates toMessageService.sendFile(...): Delegates toFileTransferService.startCall(...): Delegates toCallService.encryptForSession(sid, data, priority): Delegates toSessionService.encrypt().broadcastProfileUpdate(): Sends profile + manifest to all peers and own devices.
- Events: Extends
EventEmitter. Emits events likemessage,session_updated,auth_success,auth_error,inbound_requestto the UI. - Multi-Device Message Routing: For incoming
MSGframes withdata.payloads(a keyed map), extracts only the payload addressed to the current device's public key before decrypting.
- Role: Manages the raw WebSocket connection lifecycle.
- Key Features: Auto-reconnect with exponential backoff, message serialization/deserialization, event emitter for connection state changes (
WS_CONNECTED,WS_DISCONNECTED).
- Role: Manages a pool of
crypto.worker.tsWeb Workers to perform encryption/decryption off the main thread. - Priority Queue: Work items have a numeric priority (
p) so that high-priority frames (e.g., sync) don't get blocked by lower-priority work.
- Role: Manages the full lifecycle of peer connections (sessions) — from friend request to unfriend.
- Key Functions:
connectToPeer(email): SendsGET_PUBLIC_KEYto server.sendFriendRequest(email, remotePubKeys): Encrypts profile for each of target's device keys, sendsFRIEND_REQUEST.acceptFriend(email, remotePubKeys, senderHash): Encrypts accept payload, sendsFRIEND_ACCEPT, waits forFRIEND_ACCEPTED_ACK, then callsfinalizeSession.finalizeSession(sid, remotePubB64s, ...): The core key-derivation step. For each new peer public key, derives an AES-GCM session key via ECDH. Serializes keys as JWKs to SQLite and initializesWorkerManager.handleSessionList(list): Reconciles server session state on login, triggering key re-derivation if public keys have changed (e.g., peer reinstalled app).setPeerOnline(sid, online, newPeerPubKeys): Updates in-memory presence. If peer's public keys changed, re-derives session keys.blockUser / unblockUser: Manages theblocked_usersSQLite table.
- Finalization Lock: Uses a per-SID
Promisechain (finalizeLocks) to serialize concurrentfinalizeSessioncalls for the same SID, preventing race conditions during handshake.
- Role: Handles all message-level operations.
- Key Functions:
sendMessage(sid, text, ...): Encrypts and sends aMSGframe; saves message to SQLite.handleMsg(sid, payload, senderHash, priority): Decrypts incoming payload; dispatches to specific handlers based on inner message type (TEXT, DELETE, FILE_INFO, CALL_START, MANIFEST, etc.).editMessage(sid, messageId, newText): Sends an encryptedEDITinner packet; updates SQLite.deleteMessage(sid, messageId, forEveryone): Hard delete locally; optionally sendsDELETEinner packet to peer.deleteChatLocally(sid, removeFromUi): Deletes all messages for a session from SQLite without touching the session record.sendReaction(sid, messageId, emoji, action): SendsREACTIONinner packet; updates reaction data in SQLite.broadcastManifestToOwnDevices(): SendsMANIFEST(blocks, requests, aliases, profile, recent messages) to all sessions where the peer is the same user (i.e., own linked devices).sendManifestToPeer(sid): Pushes missed messages to a peer who just came online.coordinateSync(sid): Orchestrates both own-device and peer manifest pushes for a session.syncPendingMessages(): Retries sending messages in the local SQLite withstatus = 1(pending).broadcastProfileUpdate(): SendsPROFILE_VERSIONto all online sessions.broadcastSyncCallAction(type, callSid): Notifies own devices of call accept/end for multi-device awareness.
- Role: Manages Google OAuth tokens, device identity keys, and account switching.
- Two-Phase Account Switching:
switchAccountLocal(email): Phase 1 — unlocks local SQLite DB immediately (no network). Unblocks UI.switchAccountConnect(email, pubKey, token): Phase 2 — connects WebSocket + authenticates server in background.switchAccount(email): Full blocking version that awaits both phases.
- Identity Keys: On first login, generates a permanent ECDH P-256 identity key pair and stores it in
SafeStorage. On subsequent logins, loads the existing pair. - Database Key: Generates a 12-word BIP39 mnemonic as the SQLite encryption key, stored in
SafeStorage.
- Role: Manages the on-device registry of all known Google accounts (persisted as a JSON array in
SafeStorage). - Provides helpers for deterministic key name generation (
getStorageKey) and database name derivation (getDbName, using SHA-256 of email).
- Role: Interface for platform-specific secure storage.
- Implementation: Uses Capacitor's secure storage plugins (Keychain on iOS, Keystore on Android, encrypted
electron-storeon Desktop) to save all sensitive secrets.
- Role: All local persistence.
- Data: Stores decrypted chat history, contact sessions, pending requests, blocked users, vault items, device identity info.
- Encryption: Each account's database is encrypted using CapacitorSQLite's built-in encryption, keyed by the account's unique BIP39 mnemonic.
- Role: Manages file blobs on the local Capacitor Filesystem (for vault media, avatars, and received files).
- Role: Exports and restores all user data (messages, keys, vault media) as an AES-GCM-256 encrypted ZIP file protected by a mnemonic.
- Role: Manages WebRTC audio and video calls, relayed via TURN server.
- Signaling: SDP offers/answers and ICE candidates are exchanged end-to-end encrypted via the WebSocket channel, using the session's AES-GCM key. The relay server sees only opaque blobs.
- TURN: Requests ephemeral TURN credentials from the server (
GET_TURN_CREDS) for NAT traversal. - Media Encryption: DTLS-SRTP (standard WebRTC) encrypts the actual audio/video streams.
- Role: Handles sending and receiving files in 250KB encrypted chunks (~256,000 bytes per chunk).
- Role: Compresses images/videos before upload to reduce transfer size.
- Role: Interface for executing GGUF language models entirely on-device via a native llama.cpp backend exposed through Electron's preload as
window.llama. - Execution: Electron (Windows/Linux) only. Throws if
window.llamais not present. CapacitorFilesystemis used for model file download and storage management. - Features: Smart Compose, Quick Replies, Summarize, stateless session isolation between utility calls.
- Role: Client-side only TOTP (RFC 6238) generation and verification for Secure Vault access. The relay server is entirely uninvolved.
graph TD
App(App.tsx) --> IonApp
IonApp --> IonRouter
subgraph "Route: /login"
LoginScreen[Login / Account Selector]
AppLockScreen[AppLock PIN Screen]
end
subgraph "Route: /home (Main View)"
Home(Home.tsx) --> Sidebar
Home --> ChatWindow
Home --> Overlays
Sidebar --> SessionList
Sidebar --> UserProfile
Sidebar --> ToolButtons["[Vault, AI, Settings]"]
ChatWindow --> MessageList
ChatWindow --> MessageInput
ChatWindow --> FilePreview
Overlays --> CallOverlay(CallOverlay.tsx)
Overlays --> ConnectionSetup(ConnectionSetup.tsx)
Overlays --> SettingsOverlay
end
subgraph "Route: /secure-vault"
SecureChatWindow[SecureChatWindow.tsx]
end
subgraph "Route: /local-ai"
LocalLLMPage[LocalLLM Page]
end
The app uses an Event-Driven-State-Sync pattern.
- Source of Truth:
ChatClient(in-memory) andSQLite(on-disk) hold the true state. - Sync: React components subscribe to
ChatClientevents via hooks. - Bridge:
useChatLogic(and page-level hooks) listen to events likeChatClient.on('message'), re-query SQLite, and update React state (useState) to trigger re-renders.
- User taps "Sign in with Google" → Google OAuth consent →
id_tokenreceived. AuthService.login(id_token): a. Sets up device keys (generates or loads ECDH identity key pair). b. Opens/creates the user's encrypted SQLite database. c. Connects WebSocket to relay server.- On
WS_CONNECTED,ChatClientloads local sessions from SQLite, then sendsAUTH { token, publicKey }to server. - Server validates token, emits
AUTH_SUCCESS { email, newSessionToken }. - Client saves new session token to
SafeStorage, broadcastsauth_successevent. - Server immediately pushes
SESSION_LISTandPENDING_REQUESTSafterAUTH_SUCCESS.
- User A enters User B's email →
SessionService.connectToPeer(email)→ sendsGET_PUBLIC_KEY. - Server responds with
PUBLIC_KEY { publicKeys: [pubB64,...], targetEmail }(all of User B's device keys). SessionService.sendFriendRequest(email, publicKeys): For each of User B's public keys, derives a temporary shared key via ECDH and encrypts User A's profile into an encrypted packet. SendsFRIEND_REQUEST { targetEmail, payloads: [{publicKey, encryptedPacket}, ...] }.- Server stores request for offline delivery. If User B is online, forwards the payload array.
- User B's device receives
FRIEND_REQUEST, iterates over payloads, finds the one matching its own public key, decrypts User A's profile. UI shows incoming request. - User B clicks "Accept" →
SessionService.acceptFriend(targetEmail, remotePubKeys, senderHash): Encrypts own profile for each of User A's keys, sendsFRIEND_ACCEPT. - Server registers the session in its
friendstable, sendsFRIEND_ACCEPTED_ACKto User B andFRIEND_ACCEPTEDto User A. - Both clients derive AES-GCM-256 session keys from the ECDH shared secret. Keys stored in SQLite. Session is active.
- User types "Hello" → calls
ChatClient.sendMessage(sid, "Hello"). MessageServicegenerates a message ID (crypto.randomUUID()), timestamp, saves message to SQLite (status = pending). Thesidpassed in is the session ID — a SHA-256 hash of the two sorted email addresses, identifying which peer conversation this message belongs to.SessionService.encrypt(sid, plaintext, priority)→WorkerManagerencrypts "Hello" individually for each connected peer device and the user's own linked devices, using the appropriate per-device session key (AES-GCM with a fresh 12-byte random IV).- Sends
MSG { t, sid, data: { payloads: {[pubKey]: encryptedBlob} } }to server. - Server routes each encrypted blob to the correct device by matching the public key.
- Each receiving device decrypts with its specific local session key, saves "Hello" to SQLite.
- Receiving device sends
DELIVEREDback; sender updates message status to 2.
When a device comes online or state changes:
MessageService.broadcastManifestToOwnDevices()sends an encryptedMANIFESTinner packet to all sessions wherepeerEmailHashequals the current user's own email hash.- The MANIFEST contains sections:
blocks,requests,aliases,profile,messages(recent). - Receiving own-device merges each section with last-write-wins logic.
MessageService.sendManifestToPeer(sid)pushes missed messages to peer contacts who just came online. Both sides push symmetrically.