7.1 KiB
7.1 KiB
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
cryptomodule. - Database: PostgreSQL (Persistent data/Key bundles) + Redis (Real-time presence/Typing).
- Storage: Local Filesystem or MinIO (Encrypted file blobs).
- Media: LiveKit SFU (Self-hosted via Docker) for Voice/Screen Sharing.
2. ACCOUNT LIFECYCLE (ZERO-KNOWLEDGE)
A. Account Creation
- User Input: Username + Password.
- Entropy: Client (Electron) generates a random 128-bit Master Key (MK) and a random Salt.
- 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.
- Locking the MK: Client encrypts MK using DEK via
AES-GCM. - Auth Proof: Client hashes the DAK:
HAK = SHA-256(DAK). - Key Generation: Client generates RSA-2048 (Sharing) and Ed25519 (Signing) pairs. Private keys are encrypted with the unlocked MK.
- Server Storage: Server receives and stores:
Username,Salt,Encrypted MK,HAK,Encrypted Private Keys, andRaw Public Keys.
B. Login Handshake
- Salt Request: Client asks for
SaltforUsername.- Security Fix: Server returns a fake deterministic salt if the user doesn't exist to prevent enumeration.
- Local Compute: Client computes
DAKfrom password and received salt. - Authentication: Client sends
DAKto server. - Verification: Server checks if
SHA-256(DAK) == HAK. - Retrieval: Server sends
Encrypted MKandEncrypted Private Keys. - Decryption: Client uses
DEKto unlock theMK, then usesMKto 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: trueandsandbox: true.- Use a
preload.jsscript to expose only necessary crypto functions viacontextBridge.
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)
-- 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.
-
Voice/Video: LiveKit SFU (Self-hosted via Docker). Handles selective forwarding for scalable voice and screen sharing.