nmemmertandClaude Sonnet 4.6 7351f9048a
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
fix: replace X25519 with ECDH P-256 for Electron compatibility
X25519 Web Crypto support is not available in Electron's Chromium build
despite being in Chrome 113+. P-256 ECDH is universally supported across
all browsers and Electron versions with equivalent security.

- Replace { name: 'X25519' } with { name: 'ECDH', namedCurve: 'P-256' }
  throughout the keypair generation, sealing, and opening functions
- Wire format updated: ephemeral pub key grows from 32 to 65 bytes
  (uncompressed P-256 point: 0x04 || x || y)
- Existing accounts with X25519 keys must re-register

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

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

git clone https://git.necloud.us/nate/cloudlink.git
cd cloudlink
cp .env.example .env   # edit values

.env minimum:

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

docker compose up -d postgres

3. Run the backend

cd backend
npm install
npm run dev        # http://localhost:3001

4. Run the frontend

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

docker compose build
docker compose up -d

After a successful Gitea Actions run:

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)

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:

openssl rand -hex 32

First admin user

After the stack is up, register a normal account, then promote it:

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:

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

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

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:

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. 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

S
Description
CloudLink end-to-end encrypted messaging and cloud drive. The server stores only ciphertext — it cannot read messages or files under any circumstance.
Readme
536 KiB
0 Stars 1 Watchers 0 Forks
2026-08-31 15:59:06 +00:00
Languages
TypeScript 91.2%
JavaScript 7%
CSS 0.8%
Dockerfile 0.6%
HTML 0.4%