docs: comprehensive README covering architecture, encryption model, deployment, CI/CD, and desktop app
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
66973da859
commit
d92e899fc7
1 file changed
+342
@@ -0,0 +1,342 @@
|
||||
# CloudLink
|
||||
|
||||
End-to-end encrypted messaging and cloud drive. The server stores only ciphertext — it cannot read messages or files under any circumstance.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero-knowledge messaging** — X25519 ECDH key exchange; AES-256-GCM encrypted messages
|
||||
- **Encrypted drive** — Files are encrypted in-browser before upload; server stores opaque blobs
|
||||
- **Real-time delivery** — WebSocket relay; server forwards ciphertext without decrypting
|
||||
- **Two-factor authentication** — TOTP (Google Authenticator, Authy, etc.) with backup codes
|
||||
- **Progressive Web App** — installable from the browser on any OS
|
||||
- **Desktop app** — native Electron wrapper for Mac, Windows, and Linux
|
||||
- **Admin portal** — manage users, storage quotas, and admin flags
|
||||
- **CI/CD** — Gitea Actions builds Docker images and desktop installers on every push
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser / Desktop app
|
||||
│ HTTPS + WSS
|
||||
▼
|
||||
nginx (TLS termination, reverse proxy)
|
||||
│
|
||||
┌────┴──────┐
|
||||
│ frontend │ React 18 SPA served by nginx:alpine
|
||||
└───────────┘
|
||||
│ /api/* and /ws proxied
|
||||
▼
|
||||
┌────────────┐
|
||||
│ backend │ Express + TypeScript, port 3001
|
||||
└─────┬──────┘
|
||||
│
|
||||
┌─────▼──────┐
|
||||
│ PostgreSQL │ pg:16-alpine, persistent volume
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
All encryption and decryption happens exclusively in the browser/client. The backend is a dumb relay and storage layer.
|
||||
|
||||
---
|
||||
|
||||
## Encryption Model
|
||||
|
||||
### Keys
|
||||
|
||||
| Key | Algorithm | Where it lives |
|
||||
|---|---|---|
|
||||
| Password-derived key | PBKDF2-SHA256 (600 000 iterations) | In-memory only, derived at login |
|
||||
| Master key | AES-256-GCM | Encrypted with derived key, stored on server |
|
||||
| X25519 keypair | X25519 ECDH | Private key encrypted with master key, stored on server; public key stored plaintext |
|
||||
| Conversation key | AES-256-GCM | Per-conversation symmetric key, encrypted with each member's X25519 public key |
|
||||
| File key | AES-256-GCM | Per-file symmetric key, encrypted with owner's master key |
|
||||
|
||||
### Message flow
|
||||
|
||||
1. Alice opens a conversation with Bob
|
||||
2. Client generates a random AES-256-GCM conversation key
|
||||
3. Key is sealed with Bob's X25519 public key (ephemeral ECDH + HKDF + AES-GCM)
|
||||
4. Ciphertext of the key is stored in `conversation_members.encrypted_key`
|
||||
5. Each message is encrypted with the conversation key
|
||||
6. Server stores and relays only ciphertext — it never sees plaintext
|
||||
|
||||
### File flow
|
||||
|
||||
1. Client generates a random AES-256-GCM file key
|
||||
2. File is encrypted in-browser with the file key
|
||||
3. File key is encrypted with the user's master key
|
||||
4. Encrypted file and encrypted file key are uploaded; filename is also encrypted
|
||||
5. Server stores opaque bytes at a UUID-based path
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
### Backend
|
||||
- Node.js 20 + TypeScript
|
||||
- Express 4, Helmet, express-rate-limit
|
||||
- `ws` WebSocket server
|
||||
- PostgreSQL 16 with pgcrypto
|
||||
- Argon2id for password hashing
|
||||
- `jsonwebtoken` — 15-min access tokens + 30-day refresh tokens
|
||||
- `otplib` for TOTP 2FA
|
||||
- `multer` for file uploads
|
||||
- `zod` for request validation
|
||||
|
||||
### Frontend
|
||||
- React 18 + Vite + TypeScript
|
||||
- Tailwind CSS
|
||||
- Zustand (auth + crypto keys in-memory store)
|
||||
- TanStack Query v5
|
||||
- React Router v6
|
||||
- `vite-plugin-pwa` (PWA / installable)
|
||||
- Web Crypto API (no external crypto libraries)
|
||||
- `lucide-react` icons
|
||||
- `date-fns`
|
||||
|
||||
### Desktop
|
||||
- Electron 33
|
||||
- electron-store (persistent server URL)
|
||||
- electron-builder (Mac DMG, Windows NSIS, Linux AppImage)
|
||||
|
||||
### Infrastructure
|
||||
- Docker + Docker Compose
|
||||
- nginx (TLS, reverse proxy, HSTS, security headers)
|
||||
- Gitea Actions (CI/CD)
|
||||
|
||||
---
|
||||
|
||||
## Running Locally (Development)
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 20+
|
||||
- Docker + Docker Compose
|
||||
|
||||
### 1. Clone and configure
|
||||
|
||||
```bash
|
||||
git clone https://git.necloud.us/nate/cloudlink.git
|
||||
cd cloudlink
|
||||
cp .env.example .env # edit values
|
||||
```
|
||||
|
||||
`.env` minimum:
|
||||
|
||||
```env
|
||||
POSTGRES_DB=cloudlink
|
||||
POSTGRES_USER=cloudlink
|
||||
POSTGRES_PASSWORD=changeme
|
||||
JWT_SECRET=change-me-at-least-32-chars
|
||||
JWT_REFRESH_SECRET=change-me-at-least-32-chars
|
||||
MAX_FILE_SIZE_MB=100
|
||||
```
|
||||
|
||||
### 2. Start the database
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres
|
||||
```
|
||||
|
||||
### 3. Run the backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev # http://localhost:3001
|
||||
```
|
||||
|
||||
### 4. Run the frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev # http://localhost:5173
|
||||
```
|
||||
|
||||
Set `VITE_API_URL=http://localhost:3001/api` and `VITE_WS_URL=ws://localhost:3001/ws` in `frontend/.env.local`.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Docker)
|
||||
|
||||
### Prerequisites
|
||||
- Docker + Docker Compose on the server
|
||||
- SSL certificate at `nginx/ssl/fullchain.pem` and `nginx/ssl/privkey.pem`
|
||||
|
||||
### Option A — Build locally, run on server
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Option B — Pull pre-built images from CI (recommended)
|
||||
|
||||
After a successful Gitea Actions run:
|
||||
|
||||
```bash
|
||||
echo "$REGISTRY_TOKEN" | docker login git.necloud.us -u nate --password-stdin
|
||||
docker compose pull backend frontend
|
||||
docker compose up -d --no-build
|
||||
```
|
||||
|
||||
### Environment variables (production `.env`)
|
||||
|
||||
```env
|
||||
POSTGRES_DB=cloudlink
|
||||
POSTGRES_USER=cloudlink
|
||||
POSTGRES_PASSWORD=<strong-password>
|
||||
JWT_SECRET=<64-char-random-string>
|
||||
JWT_REFRESH_SECRET=<64-char-random-string>
|
||||
HOST_PORT=443
|
||||
MAX_FILE_SIZE_MB=100
|
||||
```
|
||||
|
||||
Generate strong secrets:
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
### First admin user
|
||||
|
||||
After the stack is up, register a normal account, then promote it:
|
||||
|
||||
```bash
|
||||
docker exec -i cloudlink-postgres-1 psql -U cloudlink -d cloudlink \
|
||||
-c "UPDATE users SET is_admin = true WHERE email = 'you@example.com';"
|
||||
```
|
||||
|
||||
Subsequent admins can be toggled from the Admin portal in the UI.
|
||||
|
||||
---
|
||||
|
||||
## Desktop App
|
||||
|
||||
### Install pre-built (from Gitea release)
|
||||
|
||||
Download the installer for your OS from [Releases](https://git.necloud.us/nate/cloudlink/releases):
|
||||
|
||||
| OS | File | Install |
|
||||
|---|---|---|
|
||||
| Windows | `CloudLink Setup.exe` | Run the installer, follow wizard |
|
||||
| Linux | `CloudLink.AppImage` | `chmod +x CloudLink.AppImage && ./CloudLink.AppImage` |
|
||||
| Mac | `CloudLink.dmg` | Open DMG, drag to Applications *(built locally — see below)* |
|
||||
|
||||
On first launch the app prompts for your CloudLink server URL (e.g. `https://cloudlink.example.com`). This is saved and used for all future launches.
|
||||
|
||||
### Build locally
|
||||
|
||||
```bash
|
||||
cd desktop
|
||||
npm install
|
||||
|
||||
npm run build:mac # requires macOS
|
||||
npm run build:win # requires macOS or Linux (Wine)
|
||||
npm run build:linux # any platform
|
||||
```
|
||||
|
||||
Outputs go to `desktop/dist/`.
|
||||
|
||||
---
|
||||
|
||||
## Admin Portal
|
||||
|
||||
Accessible at `/admin` for users with `is_admin = true`.
|
||||
|
||||
- View all users, registration date, last seen
|
||||
- Per-user storage usage vs quota (colour-coded bar)
|
||||
- Inline quota editing (MB)
|
||||
- Toggle admin flag per user
|
||||
- Aggregate stats (total users, total storage, total files)
|
||||
|
||||
---
|
||||
|
||||
## CI/CD (Gitea Actions)
|
||||
|
||||
Workflow: `.gitea/workflows/docker-build.yml`
|
||||
|
||||
| Job | Trigger | Action |
|
||||
|---|---|---|
|
||||
| `build-backend` | every push / PR | Build + push `backend:latest` and `backend:<sha>` to Gitea registry |
|
||||
| `build-frontend` | every push / PR | Build + push `frontend:latest` and `frontend:<sha>` |
|
||||
| `build-desktop` | every push / PR | Build Linux AppImage + Windows NSIS; upload as workflow artifacts (30-day retention) |
|
||||
| `deploy` | push to `master` | SSH into server, pull new images, restart containers |
|
||||
| `release` | push of a `v*` tag | Create Gitea release, attach desktop installers |
|
||||
|
||||
### Required secrets (Repo → Settings → Secrets)
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `REGISTRY_USER` | Gitea username (e.g. `nate`) |
|
||||
| `REGISTRY_TOKEN` | Gitea API token with `package:write` scope |
|
||||
| `DEPLOY_HOST` | Server hostname or IP |
|
||||
| `DEPLOY_USER` | SSH user |
|
||||
| `DEPLOY_SSH_KEY` | SSH private key (contents of `~/.ssh/id_ed25519`) |
|
||||
| `DEPLOY_PATH` | Absolute path to cloudlink on server (e.g. `/opt/cloudlink`) |
|
||||
|
||||
### Tagging a release
|
||||
|
||||
```bash
|
||||
git tag v1.2.0 && git push origin v1.2.0
|
||||
```
|
||||
|
||||
This triggers the `release` job which creates a Gitea release entry and attaches the built installers.
|
||||
|
||||
### Runner setup
|
||||
|
||||
A Gitea act runner is required. Install on your server:
|
||||
|
||||
```bash
|
||||
wget https://gitea.com/gitea/act_runner/releases/download/v0.2.11/act_runner-0.2.11-linux-amd64 -O act_runner
|
||||
chmod +x act_runner
|
||||
|
||||
# Get registration token from:
|
||||
# Gitea → Site Administration → Actions → Runners → Create Runner
|
||||
|
||||
./act_runner register \
|
||||
--no-interactive \
|
||||
--instance https://git.necloud.us \
|
||||
--token <TOKEN> \
|
||||
--name cloudlink-runner \
|
||||
--labels ubuntu-latest:docker://node:20-bullseye
|
||||
|
||||
./act_runner daemon
|
||||
```
|
||||
|
||||
The runner host must have Docker installed.
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Crypto keys are **never** written to disk or localStorage — they live in Zustand's in-memory store and are lost on page reload (by design). Users re-derive them from their password at each login.
|
||||
- The server stores only: `password_hash` (Argon2id), `kdf_salt`, `encrypted_master_key`, `encrypted_private_key`, and ciphertext blobs. No plaintext content.
|
||||
- JWTs are short-lived (15 min). Refresh tokens are rotated on use and hashed before storage.
|
||||
- nginx enforces HSTS, X-Frame-Options, X-Content-Type-Options, and a strict CSP.
|
||||
- File paths on disk are UUID-based — not guessable from filenames.
|
||||
- Rate limiting is applied to auth endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
See [`backend/src/db/schema.sql`](backend/src/db/schema.sql). Tables:
|
||||
|
||||
| Table | Purpose |
|
||||
|---|---|
|
||||
| `users` | Account credentials, KDF salt, encrypted master key, 2FA state, quota |
|
||||
| `user_keys` | X25519 public key + encrypted private key |
|
||||
| `refresh_tokens` | Hashed refresh tokens with expiry |
|
||||
| `conversations` | Direct message threads |
|
||||
| `conversation_members` | Per-member encrypted conversation key |
|
||||
| `messages` | Ciphertext-only message records |
|
||||
| `files` | Encrypted file metadata + storage path |
|
||||
|
||||
---
|
||||
|
||||
## Repository
|
||||
|
||||
Source: `https://git.necloud.us/nate/cloudlink`
|
||||
Container registry: `git.necloud.us/nate/cloudlink/{backend,frontend}:latest`
|
||||
Reference in new issue
Block a user