Files
cloudlink/README.md
T
nmemmertandClaude Sonnet 4.6 99efd4be55
Build & Release / Docker / backend (push) Successful in 1m32s
Build & Release / Docker / frontend (push) Successful in 1m32s
Build & Release / Desktop / Linux & Windows (push) Successful in 18m38s
Build & Release / Deploy to server (push) Successful in 2m49s
Build & Release / Create release (push) Has been skipped
docs: update README for P-256 ECDH, first admin setup, release filenames
- Replace X25519 references with ECDH P-256 throughout encryption model
- Expand first admin user section: register normally, promote via SQL,
  then manage further admins through the UI
- Update desktop install table with actual DMG filenames and Mac entries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-28 11:44:03 -04:00

346 lines
10 KiB
Markdown

# 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 |
| ECDH keypair | P-256 ECDH | Private key encrypted with master key, stored on server; public key stored plaintext |
| Conversation key | AES-256-GCM | Per-conversation symmetric key, sealed with each member's P-256 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 P-256 public key (ephemeral ECDH + HKDF-SHA256 + 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
There are no default credentials. Register a normal account via the UI, then promote it to admin with one SQL command:
```bash
docker exec -i cloudlink-postgres-1 psql -U cloudlink -d cloudlink \
-c "UPDATE users SET is_admin = true WHERE email = 'you@example.com';"
```
After that, the **Admin** link appears in the sidebar. From there you can promote or demote any other user without touching SQL again.
> **Tip:** The container name may vary. Run `docker ps --format '{{.Names}}'` to confirm it.
---
## 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 |
|---|---|---|
| Mac (Apple Silicon) | `CloudLink-*-arm64.dmg` | Open DMG, drag to Applications, right-click → Open |
| Mac (Intel) | `CloudLink-*.dmg` | Open DMG, drag to Applications, right-click → Open |
| Windows | `CloudLink Setup *.exe` | Run installer, follow wizard |
| Linux | `CloudLink-*.AppImage` | `chmod +x CloudLink-*.AppImage && ./CloudLink-*.AppImage` |
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`