167 lines
7.1 KiB
Markdown
167 lines
7.1 KiB
Markdown
# 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:** LiveKit SFU (Self-hosted via Docker) for Voice/Screen Sharing.
|
|
|
|
---
|
|
|
|
## 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.
|
|
|
|
- **Voice/Video:** LiveKit SFU (Self-hosted via Docker). Handles selective forwarding for scalable voice and screen sharing. |