Initial CloudLink scaffold: E2E encrypted PWA with messaging and cloud drive

- Zero-knowledge architecture: all encryption/decryption client-side via Web Crypto API
- AES-GCM 256 for messages, files, and key wrapping; PBKDF2 for password-based key derivation
- Node.js/TypeScript backend: JWT auth with refresh rotation, argon2id passwords, TOTP 2FA
- PostgreSQL schema: encrypted blobs only, server never stores plaintext
- Real-time messaging via WebSocket relay (server routes ciphertext, cannot read it)
- Encrypted file upload/download with per-file keys wrapped by user master key
- React PWA frontend: Vite + Tailwind + service worker for offline support
- Docker Compose: postgres + backend + frontend + nginx with TLS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-28 09:41:18 -04:00
commit 937bf8ad2d
36 files changed
+2325

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- Users: minimal plaintext metadata, everything sensitive is client-encrypted
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
-- Argon2id salt used client-side for key derivation (base64)
kdf_salt TEXT NOT NULL,
-- Client-side encrypted master key (base64 ciphertext + nonce)
encrypted_master_key TEXT NOT NULL,
-- TOTP secret (server-side, not part of user's encrypted data)
totp_secret TEXT,
totp_enabled BOOLEAN NOT NULL DEFAULT false,
-- Backup codes for 2FA (hashed)
totp_backup_codes TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_seen_at TIMESTAMPTZ
);
-- Refresh tokens for JWT rotation
CREATE TABLE IF NOT EXISTS refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Direct message conversations
CREATE TABLE IF NOT EXISTS conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS conversation_members (
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Per-conversation encrypted key (client encrypts shared key with each member's public key)
encrypted_key TEXT NOT NULL,
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (conversation_id, user_id)
);
-- Messages: ciphertext only — server cannot read content
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- AES-GCM encrypted content (base64: nonce + ciphertext + tag)
ciphertext TEXT NOT NULL,
-- Optional: encrypted filename if message contains a file reference
file_id UUID,
sent_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_messages_conversation ON messages(conversation_id, sent_at DESC);
-- Files: encrypted before upload, metadata stored here
CREATE TABLE IF NOT EXISTS files (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- Encrypted filename (client-side)
encrypted_name TEXT NOT NULL,
-- MIME type (not sensitive, kept plaintext for serving)
mime_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL,
-- Path on disk (opaque UUID-based, not guessable)
storage_path TEXT NOT NULL UNIQUE,
-- AES-GCM file encryption key, encrypted with owner's master key
encrypted_file_key TEXT NOT NULL,
-- Optional: shared with a conversation
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Soft delete
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_files_owner ON files(owner_id) WHERE deleted_at IS NULL;
-- Public keys for E2E key exchange
CREATE TABLE IF NOT EXISTS user_keys (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
-- X25519 public key for ECDH (base64)
public_key TEXT NOT NULL,
-- Signed with user's identity key so others can verify
key_signature TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);