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:
commit
937bf8ad2d
36 files changed
+2325
No files matched your search
@@ -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()
|
||||
);
|
||||
Reference in new issue
Block a user