# Secure Chat: Project Specification & Zero-Knowledge Architecture This document outlines the full architecture for a self-hosted, single-server, Discord-style replacement using the Zero-Knowledge model (MEGA.nz inspired). --- ## 1. CORE TECH STACK * **Backend:** Node.js (Express or Fastify) + Socket.io (Real-time). * **Desktop:** Electron (React/Vue frontend) + Node `crypto` module. * **Database:** PostgreSQL (Persistent data/Key bundles) + Redis (Real-time presence/Typing). * **Storage:** Local Filesystem or MinIO (Encrypted file blobs). * **Media:** WebRTC (P2P for Voice/Video) with mandatory DTLS/SRTP. --- ## 2. ACCOUNT LIFECYCLE (ZERO-KNOWLEDGE) ### A. Account Creation 1. **User Input:** Username + Password. 2. **Entropy:** Client (Electron) generates a random 128-bit **Master Key (MK)** and a random **Salt**. 3. **Derivation:** Client runs `PBKDF2-HMAC-SHA-512` (100k+ iterations) on Password + Salt. - Result = 256-bit Key. - **DEK (Derived Encryption Key):** Bits 0-127. - **DAK (Derived Authentication Key):** Bits 128-255. 4. **Locking the MK:** Client encrypts MK using DEK via `AES-GCM`. 5. **Auth Proof:** Client hashes the DAK: `HAK = SHA-256(DAK)`. 6. **Key Generation:** Client generates RSA-2048 (Sharing) and Ed25519 (Signing) pairs. Private keys are encrypted with the unlocked MK. 7. **Server Storage:** Server receives and stores: `Username`, `Salt`, `Encrypted MK`, `HAK`, `Encrypted Private Keys`, and `Raw Public Keys`. ### B. Login Handshake 1. **Salt Request:** Client asks for `Salt` for `Username`. - *Security Fix:* Server returns a fake deterministic salt if the user doesn't exist to prevent enumeration. 2. **Local Compute:** Client computes `DAK` from password and received salt. 3. **Authentication:** Client sends `DAK` to server. 4. **Verification:** Server checks if `SHA-256(DAK) == HAK`. 5. **Retrieval:** Server sends `Encrypted MK` and `Encrypted Private Keys`. 6. **Decryption:** Client uses `DEK` to unlock the `MK`, then uses `MK` to unlock Private Keys. ### C. Password Recovery - During setup, the user exports the raw **Master Key** (Recovery Key). - To reset: User provides the Recovery Key + New Password. The client generates a new DEK from the new password and re-encrypts the Master Key for the server. ### D. Session Persistence (Local Security) - To avoid re-entering passwords on every app launch, the **Master Key** is encrypted with a unique **Local Machine Key** and stored in the OS Keychain (using `electron-keytar`). - This keeps the MK safe on the physical disk even if the machine is stolen, as it requires OS-level user authentication to retrieve. --- ## 3. SECURITY & REAL-TIME FIXES ### A. Identity Protection - **Problem:** Attackers shouldn't know which usernames exist. - **Solution:** `FakeSalt = HMAC(ServerSecret, Username)`. Always return a salt, even for non-existent users. ### B. Forward Secrecy (Key Rotation) - **Problem:** Kicked users shouldn't read future messages. - **Solution:** When a user leaves, the Admin client generates a new **Channel Key**. It encrypts this new key for all remaining members using their Public Keys. ### C. Trust Verification (MITM Protection) - Users can verify each other via **Safety Numbers** (Fingerprints). These are short strings derived from their Public Keys. If the numbers match on both users' screens, the connection is confirmed as un-intercepted by the server. ### D. Electron Hardening - `contextIsolation: true` and `sandbox: true`. - Use a `preload.js` script to expose only necessary crypto functions via `contextBridge`. --- ## 4. END-TO-END ENCRYPTION (E2EE) LOGIC ### Message Integrity (Digital Signatures) - Every message is signed by the sender's **Ed25519 Private Key**. - The recipient verifies the signature using the sender's **Public Key** stored on the server. This prevents the server from tampering with or replaying encrypted messages. --- ## 5. DATABASE SCHEMA (POSTGRESQL) ```sql -- Core User Table CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username TEXT UNIQUE NOT NULL, client_salt TEXT NOT NULL, encrypted_master_key TEXT NOT NULL, -- MK encrypted by DEK hashed_auth_key TEXT NOT NULL, -- SHA256(DAK) public_identity_key TEXT NOT NULL, -- RSA Public Key for Encryption public_signing_key TEXT NOT NULL, -- Ed25519 Public Key for Signatures is_admin BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() ); -- Permission Roles CREATE TABLE roles ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, -- 'admin', 'moderator', 'member' permissions JSONB -- e.g. {"can_view_history": true} ); -- Channel Key Bundles (The bridge to E2EE) CREATE TABLE channel_keys ( channel_id UUID NOT NULL, user_id UUID NOT NULL, encrypted_key_bundle TEXT NOT NULL, -- Channel Key encrypted for this specific user key_version INTEGER DEFAULT 1, -- For rotation tracking PRIMARY KEY (channel_id, user_id) ); -- Message Storage CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel_id UUID NOT NULL, sender_id UUID NOT NULL, ciphertext TEXT NOT NULL, -- Encrypted content nonce TEXT NOT NULL, -- AES Initialization Vector signature TEXT NOT NULL, -- Ed25519 Signature key_version INTEGER NOT NULL, -- Link to specific key bundle created_at TIMESTAMP DEFAULT NOW() ); ## 5. ELECTRON PRELOAD (SECURE BRIDGE) ```javascript // preload.js const { contextBridge, ipcRenderer } = require('electron'); const crypto = require('node:crypto'); contextBridge.exposeInMainWorld('cryptoAPI', { // Perform heavy PBKDF2 asynchronously to keep the UI responsive deriveAuthKeys: (password, salt) => { return new Promise((resolve, reject) => { const iterations = 100000; crypto.pbkdf2(password, salt, iterations, 32, 'sha512', (err, derived) => { if (err) reject(err); resolve({ dek: derived.slice(0, 16).toString('hex'), dak: derived.slice(16, 32).toString('hex') }); }); }); }, encryptData: (plaintext, keyHex, iv) => { const key = Buffer.from(keyHex, 'hex'); const cipher = crypto.createCipheriv('aes-128-gcm', key, iv); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); return { content: encrypted, tag: cipher.getAuthTag().toString('hex') }; }, signMessage: (privateKey, message) => { return crypto.sign(null, Buffer.from(message), privateKey).toString('hex'); } }); ``` ## 6. REAL-TIME FEATURES - **Presence:** `user:status:{id}` stored in Redis with 60s TTL. Client sends heartbeat every 30s. - **Typing:** Socket.io event typing_start -> Room broadcast. Frontend clears name after 5s silence. - **DMs:** Use Signal Protocol's Double Ratchet. Server stores encrypted pre-key bundles. - **Files:** Encrypted with a unique File Key (AES-256). The File Key is sent inside the E2EE text message to the channel/user.