diff --git a/README_DE.md b/README_DE.md index fd8eb35..3f3a2ee 100644 --- a/README_DE.md +++ b/README_DE.md @@ -10,367 +10,173 @@ Eine Instant-Messaging-App im WeChat-Stil mit Ende-zu-Ende-Verschlüsselung übe --- ui -## Features -| Feature | Description | -|---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +## Funktionen + +| Funktion | Beschreibung | +|----------|-------------| +| 🔐 E2E-Verschlüsselung | Zustandsloses ECDH + XSalsa20-Poly1305 — ephemere Schlüssel pro Nachricht, Forward Secrecy | +| 🗝️ Zero-Knowledge-Server | Server speichert nur Chiffretext; private Schlüssel verlassen niemals das Gerät | +| 📹 Video-/Sprachanrufe | WebRTC P2P (1:1) + Mesh (Gruppe), Cloudflare TURN für NAT-Traversal | +| 👥 Gruppenchat | Bis zu 2000 Mitglieder, Klartextnachrichten, Nicht-stören-Modus, Mitgliederverwaltung | +| ⏱️ Auto-Löschung | 5 Stufen (nie/1T/3T/1W/1M), in DMs beidseitig einstellbar, in Gruppen nur Inhaber | +| 🔔 Push-Benachrichtigungen | Web Push (VAPID) + OneSignal Dual-Kanal | +| 🌐 Mehrsprachig | ZH/EN/JA/KO/FR/DE/RU/ES — automatische Erkennung + manuelle Umschaltung | +| 📱 iOS ohne Unternehmenszertifikat | PWA über Safari „Zum Home-Bildschirm", funktioniert dauerhaft ohne Apple-Signatur | +| 💬 Rich Messaging | Text, Bilder, Sprachnachrichten, 64-Emoji-Panel, Lesebestätigungen | +| 🌐 Momente | Sozialer Feed: Text + bis zu 9 Fotos, Likes (Freunde-Avatare), Kommentare, Tag-basierte Sichtbarkeit | +| 🏷️ Freunde-Tags | Mehrere Tags pro Freund (12-Farben-Palette), Kontakte nach Tags filtern | +| 🗂️ R2-Speicher | Cloudflare R2 für Bild-/Audiodateien — optionale CDN-URL | +| 🏗️ Self-Hosting | Docker Compose Ein-Befehl-Deployment | --- -## Tech Stack +## Technologie-Stack ``` Backend (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — Benutzer, Nachrichten (verschlüsselt) + Redis — Online-Status + Knotenübergreifendes Routing + Cloudflare R2 — Bild-/Audio-Dateispeicher (S3-kompatible API) + JWT + bcrypt Authentifizierung Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) + Natives HTML + Vanilla JS (ESM, kein Bundler) libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls + WebRTC API — Video-/Sprachanrufe PWA: manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +Kryptographieschicht + Zustandsloses ECDH + XSalsa20-Poly1305 — ephemeres Schlüsselpaar pro Nachricht + 4-Stufen-Schlüsselpersistenz: Speicher → localStorage → sessionStorage → IndexedDB + Alle privaten Schlüssel nur auf dem Gerät — nie an den Server gesendet ``` --- -## Quick Start +## Schnellstart -### Option 0: Zeabur One-Click Cloud Deploy +### Option 0: Zeabur Ein-Klick-Cloud-Deployment [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> Nach dem Deployment ist ein manueller Schritt erforderlich: +> 1. Zeabur-Konsole → **server**-Dienst → Umgebungsvariablen → `ZEABUR_WEB_URL` kopieren +> 2. **client**-Dienst → Umgebungsvariablen → `SERVER_URL` = kopierter Wert +> 3. Client-Dienst neu starten -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: - ```bash - hostname -i - ``` - ---- - -### Option 1: Docker Compose (Recommended — no local build needed) +### Option 1: Docker Compose (empfohlen) ```bash -# Clone the repository git clone && cd paperphone - -# Copy and edit environment variables cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. - -# Pull images and start everything +# Variablen bearbeiten: DB_PASS / JWT_SECRET / R2_* usw. docker compose up -d - -# Check service status -docker compose ps - -# Open in browser open http://localhost ``` -> Pre-built images on Docker Hub: -> - `facilisvelox/paperphone-client:latest` -> - `facilisvelox/paperphone-server:latest` -> -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. +> Docker-Hub-Images: `facilisvelox/paperphone-client:latest` und `facilisvelox/paperphone-server:latest` -### Option 2: Manual Local Start - -#### 1. Prepare the environment +### Option 2: Lokaler manueller Start ```bash -# Copy and edit environment variables -cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# Backend +cd server && npm install && npm run dev # → http://localhost:3000 -# Note: the server auto-runs schema.sql on first startup -``` - -#### 2. Start the backend - -```bash -cd server -npm install -npm run dev # → http://localhost:3000 -``` - -#### 3. Start the frontend - -```bash -npx serve client -p 8080 -# → http://localhost:8080 +# Frontend +npx serve client -p 8080 # → http://localhost:8080 ``` --- -## Video Call Configuration - -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. - -### Using Cloudflare TURN (Recommended) - -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +## Videoanruf-Konfiguration — Cloudflare TURN ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) +| Typ | Transport | Empfohlen für | +|-----|-----------|---------------| +| 1:1 Video | WebRTC P2P + TURN | Alle Szenarien | +| 1:1 Sprache | WebRTC P2P + TURN | Alle Szenarien | +| Gruppenanruf | WebRTC Mesh | Bis zu 6 Teilnehmer | -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. - -### Call Types - -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +> **Ohne Konfiguration**: Nur STUN. LAN-Anrufe funktionieren ohne Einrichtung. --- -## Push Notification Configuration +## Push-Benachrichtigungen -Offline message notifications are delivered through **two channels** for maximum delivery rate: - -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | - -### Configuring Web Push - -1. Generate VAPID keys (one-time): +| Kanal | Plattformen | Konfiguration | +|-------|-------------|---------------| +| Web Push | Browser + iOS PWA (Safari 16.4+) | VAPID-Schlüssel | +| OneSignal | Native Apps über Median.co | App ID + REST Key | ```bash -cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +--- -```env -VAPID_PUBLIC_KEY=your_public_key_here -VAPID_PRIVATE_KEY=your_private_key_here -VAPID_SUBJECT=mailto:admin@your-domain.com -``` +## iOS — Permanente Installation ohne Zertifikat -3. Restart the server — users can enable notifications from the Settings page - -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. - -### Configuring OneSignal (Median.co Native Apps) - -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: - -```env -ONESIGNAL_APP_ID=your_onesignal_app_id -ONESIGNAL_REST_KEY=your_onesignal_rest_api_key -``` - -> **When not configured**: push notifications are silently disabled — all other features work normally. +1. Auf HTTPS-Server bereitstellen → 2. In Safari öffnen → 3. Teilen ⬆️ → 4. „Zum Home-Bildschirm" --- -## iOS — Permanent No-Cert Deployment +## Sicherheitsmodell -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** - -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. - ---- - -## Production Deployment (Nginx) - -```nginx -server { - listen 443 ssl http2; - server_name your.domain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - # Frontend static files - location / { - root /path/to/paperphone/client; - try_files $uri /index.html; - } - - # REST API - location /api/ { - proxy_pass http://localhost:3000; - proxy_set_header Host $host; - } - - # WebSocket (messaging + call signalling) - location /ws { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 3600s; - } -} +``` +Registrierung: IK + SPK + 10×OPK lokal generiert, öffentliche Schlüssel hochgeladen +Nachricht: Ephemeres ECDH → X25519 → XSalsa20-Poly1305 +Server sieht: ✅ Chiffretext + Routing-Metadaten ❌ Klartext / private Schlüssel ``` --- -## Project Structure +## Datenbankschema -``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) -``` +11 Tabellen, automatisch beim ersten Start erstellt: + +| Tabelle | Zweck | +|---------|-------| +| `users` | Benutzerprofile + ECDH/OPK-Schlüssel | +| `prekeys` | X3DH-Einmal-Prekeys | +| `friends` | Freundschaftsbeziehungen | +| `groups` / `group_members` | Gruppenchats + Mitglieder | +| `messages` | Verschlüsselte Nachrichten | +| `moments` / `moment_images` | Soziale Beiträge + Bilder | +| `moment_likes` / `moment_comments` | Likes + Kommentare | +| `push_subscriptions` | Web Push (VAPID) | +| `onesignal_players` | OneSignal-Geräte (Median.co) | --- -## Database Schema +## Umgebungsvariablen -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | +| Variable | Beschreibung | Standard | +|----------|-------------|----------| +| `PORT` | Server-Port | `3000` | +| `JWT_SECRET` | JWT-Signaturschlüssel (**in Produktion ändern**) | dev_secret | +| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL-Verbindung | — | +| `REDIS_HOST`/`REDIS_PASS` | Redis-Verbindung | — | +| `R2_ACCOUNT_ID` | Cloudflare-Konto-ID | — | +| `R2_ACCESS_KEY_ID` | R2-API-Zugriffsschlüssel | — | +| `R2_SECRET_ACCESS_KEY` | R2-API-Geheimschlüssel | — | +| `R2_BUCKET` | R2-Bucket-Name | — | +| `R2_PUBLIC_URL` | Öffentliche R2-URL (optional) | — | +| `CF_CALLS_APP_ID` | Calls App ID (optional) | — | +| `CF_CALLS_APP_SECRET` | Calls Secret (optional) | — | +| `VAPID_PUBLIC_KEY` | VAPID öffentlicher Schlüssel (optional) | — | +| `VAPID_PRIVATE_KEY` | VAPID privater Schlüssel (optional) | — | +| `ONESIGNAL_APP_ID` | OneSignal App ID (optional) | — | +| `ONESIGNAL_REST_KEY` | OneSignal REST Key (optional) | — | --- -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## Lizenz MIT © PaperPhone Contributors diff --git a/README_ES.md b/README_ES.md index e4cef23..2129c04 100644 --- a/README_ES.md +++ b/README_ES.md @@ -10,367 +10,173 @@ Una aplicación de mensajería instantánea cifrada de extremo a extremo estilo --- ui -## Features -| Feature | Description | +## Características + +| Función | Descripción | |---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +| 🔐 Cifrado E2E | ECDH sin estado + XSalsa20-Poly1305 — claves efímeras por mensaje, secreto hacia adelante | +| 🗝️ Servidor de conocimiento cero | El servidor solo almacena texto cifrado; las claves privadas nunca salen del dispositivo | +| 📹 Videollamadas/voz | WebRTC P2P (1:1) + Mesh (grupo), Cloudflare TURN para atravesar NAT | +| 👥 Chat grupal | Hasta 2000 miembros, mensajes en texto plano, modo No molestar, gestión de miembros | +| ⏱️ Eliminación automática | 5 niveles (nunca/1d/3d/1sem/1mes), en DMs ambas partes, en grupos solo propietario | +| 🔔 Notificaciones push | Web Push (VAPID) + OneSignal canal doble | +| 🌐 Multilingüe | ZH/EN/JA/KO/FR/DE/RU/ES — detección automática + selección manual | +| 📱 iOS sin certificado empresarial | PWA vía Safari "Añadir a inicio", sin firma de Apple | +| 💬 Mensajería rica | Texto, imágenes, mensajes de voz, 64 emojis, confirmaciones de lectura | +| 🌐 Momentos | Feed social: texto + hasta 9 fotos, likes (avatares de amigos), comentarios, visibilidad por etiquetas | +| 🏷️ Etiquetas de amigos | Múltiples etiquetas por amigo (paleta de 12 colores), filtrar contactos por etiqueta | +| 🗂️ Almacenamiento R2 | Cloudflare R2 para imágenes/audio — URL CDN opcional | +| 🏗️ Auto-alojable | Despliegue Docker Compose en un comando | --- -## Tech Stack +## Stack tecnológico ``` Backend (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — usuarios, mensajes (texto cifrado) + Redis — presencia en línea + enrutamiento entre nodos + Cloudflare R2 — almacenamiento de archivos (API compatible S3) + Autenticación JWT + bcrypt Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) + HTML nativo + Vanilla JS (ESM, sin bundler) libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls + API WebRTC — videollamadas / llamadas de voz PWA: manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +Capa criptográfica + ECDH sin estado + XSalsa20-Poly1305 — par de claves efímeras por mensaje + Persistencia de claves en 4 niveles: memoria → localStorage → sessionStorage → IndexedDB + Todas las claves privadas solo en el dispositivo — nunca se envían al servidor ``` --- -## Quick Start +## Inicio rápido -### Option 0: Zeabur One-Click Cloud Deploy +### Opción 0: Zeabur — Despliegue en la nube con un clic [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> Se requiere un paso manual después del despliegue: +> 1. Consola Zeabur → servicio **server** → Variables de entorno → copiar el valor de `ZEABUR_WEB_URL` +> 2. Servicio **client** → Variables de entorno → agregar `SERVER_URL` = valor copiado +> 3. Reiniciar el servicio client -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: - ```bash - hostname -i - ``` - ---- - -### Option 1: Docker Compose (Recommended — no local build needed) +### Opción 1: Docker Compose (recomendado) ```bash -# Clone the repository git clone && cd paperphone - -# Copy and edit environment variables cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. - -# Pull images and start everything +# Editar: DB_PASS / JWT_SECRET / R2_* etc. docker compose up -d - -# Check service status -docker compose ps - -# Open in browser open http://localhost ``` -> Pre-built images on Docker Hub: -> - `facilisvelox/paperphone-client:latest` -> - `facilisvelox/paperphone-server:latest` -> -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. +> Imágenes Docker Hub: `facilisvelox/paperphone-client:latest` y `facilisvelox/paperphone-server:latest` -### Option 2: Manual Local Start - -#### 1. Prepare the environment +### Opción 2: Inicio local manual ```bash -# Copy and edit environment variables -cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# Backend +cd server && npm install && npm run dev # → http://localhost:3000 -# Note: the server auto-runs schema.sql on first startup -``` - -#### 2. Start the backend - -```bash -cd server -npm install -npm run dev # → http://localhost:3000 -``` - -#### 3. Start the frontend - -```bash -npx serve client -p 8080 -# → http://localhost:8080 +# Frontend +npx serve client -p 8080 # → http://localhost:8080 ``` --- -## Video Call Configuration - -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. - -### Using Cloudflare TURN (Recommended) - -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +## Configuración de videollamadas — Cloudflare TURN ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) +| Tipo | Transporte | Recomendado para | +|------|-----------|------------------| +| Video 1:1 | WebRTC P2P + TURN | Todos los escenarios | +| Voz 1:1 | WebRTC P2P + TURN | Todos los escenarios | +| Llamada grupal | WebRTC Mesh | Hasta 6 participantes | -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. - -### Call Types - -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +> **Sin configuración**: solo STUN. Las llamadas en LAN funcionan sin configuración adicional. --- -## Push Notification Configuration +## Notificaciones push -Offline message notifications are delivered through **two channels** for maximum delivery rate: - -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | - -### Configuring Web Push - -1. Generate VAPID keys (one-time): +| Canal | Plataformas | Configuración | +|-------|-------------|---------------| +| Web Push | Navegadores + iOS PWA (Safari 16.4+) | Claves VAPID | +| OneSignal | Apps nativas Median.co | App ID + REST Key | ```bash -cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +--- -```env -VAPID_PUBLIC_KEY=your_public_key_here -VAPID_PRIVATE_KEY=your_private_key_here -VAPID_SUBJECT=mailto:admin@your-domain.com -``` +## iOS — Instalación permanente sin certificado -3. Restart the server — users can enable notifications from the Settings page - -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. - -### Configuring OneSignal (Median.co Native Apps) - -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: - -```env -ONESIGNAL_APP_ID=your_onesignal_app_id -ONESIGNAL_REST_KEY=your_onesignal_rest_api_key -``` - -> **When not configured**: push notifications are silently disabled — all other features work normally. +1. Desplegar en servidor HTTPS → 2. Abrir en Safari → 3. Compartir ⬆️ → 4. «Añadir a pantalla de inicio» --- -## iOS — Permanent No-Cert Deployment +## Modelo de seguridad -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** - -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. - ---- - -## Production Deployment (Nginx) - -```nginx -server { - listen 443 ssl http2; - server_name your.domain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - # Frontend static files - location / { - root /path/to/paperphone/client; - try_files $uri /index.html; - } - - # REST API - location /api/ { - proxy_pass http://localhost:3000; - proxy_set_header Host $host; - } - - # WebSocket (messaging + call signalling) - location /ws { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 3600s; - } -} +``` +Registro: IK + SPK + 10×OPK generados localmente, claves públicas subidas +Mensaje: ECDH efímero → X25519 → XSalsa20-Poly1305 +El servidor ve: ✅ texto cifrado + metadatos de enrutamiento ❌ texto plano / claves privadas ``` --- -## Project Structure +## Esquema de base de datos -``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) -``` +11 tablas, creadas automáticamente en el primer inicio: + +| Tabla | Propósito | +|-------|-----------| +| `users` | Perfiles + claves públicas ECDH/OPK | +| `prekeys` | Pre-claves X3DH de un solo uso | +| `friends` | Relaciones de amistad | +| `groups` / `group_members` | Grupos + miembros | +| `messages` | Mensajes cifrados | +| `moments` / `moment_images` | Publicaciones + imágenes | +| `moment_likes` / `moment_comments` | Likes + comentarios | +| `push_subscriptions` | Web Push (VAPID) | +| `onesignal_players` | Dispositivos OneSignal | --- -## Database Schema +## Variables de entorno -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | +| Variable | Descripción | Predeterminado | +|----------|-------------|----------------| +| `PORT` | Puerto del servidor | `3000` | +| `JWT_SECRET` | Clave de firma JWT (**cambiar en producción**) | dev_secret | +| `DB_HOST`/`DB_PASS`/`DB_NAME` | Conexión MySQL | — | +| `REDIS_HOST`/`REDIS_PASS` | Conexión Redis | — | +| `R2_ACCOUNT_ID` | ID de cuenta Cloudflare | — | +| `R2_ACCESS_KEY_ID` | Clave de acceso R2 API | — | +| `R2_SECRET_ACCESS_KEY` | Clave secreta R2 API | — | +| `R2_BUCKET` | Nombre del bucket R2 | — | +| `R2_PUBLIC_URL` | URL pública R2 (opc.) | — | +| `CF_CALLS_APP_ID` | Calls App ID (opc.) | — | +| `CF_CALLS_APP_SECRET` | Calls Secret (opc.) | — | +| `VAPID_PUBLIC_KEY` | Clave pública VAPID (opc.) | — | +| `VAPID_PRIVATE_KEY` | Clave privada VAPID (opc.) | — | +| `ONESIGNAL_APP_ID` | OneSignal App ID (opc.) | — | +| `ONESIGNAL_REST_KEY` | OneSignal REST Key (opc.) | — | --- -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## Licencia MIT © PaperPhone Contributors diff --git a/README_FR.md b/README_FR.md index 8538958..c199b27 100644 --- a/README_FR.md +++ b/README_FR.md @@ -10,367 +10,147 @@ Une application de messagerie instantanée chiffrée de bout en bout, style WeCh --- ui -## Features -| Feature | Description | -|---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +## Fonctionnalités + +| Fonction | Description | +|----------|-------------| +| 🔐 Chiffrement E2E | ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message, secret de transmission | +| 🗝️ Serveur à connaissance nulle | Le serveur ne stocke que le texte chiffré ; les clés privées ne quittent jamais l'appareil | +| 📹 Appels vidéo/audio | WebRTC P2P (1:1) + Mesh (groupe), Cloudflare TURN pour traversée NAT | +| 👥 Chat de groupe | Jusqu'à 2000 membres, messages en texte brut, mode Ne pas déranger | +| ⏱️ Suppression auto | 5 niveaux (jamais/1j/3j/1sem/1mois) | +| 🔔 Notifications push | Web Push (VAPID) + OneSignal double canal | +| 🌐 Multilingue | 中/EN/日/한/FR/DE/RU/ES — détection auto + sélection manuelle | +| 📱 iOS PWA | Safari « Ajouter à l'écran d'accueil », sans certificat Apple | +| 💬 Messagerie riche | Texte, images, audio, 64 émojis, accusés de réception | +| 🌐 Moments | Fil social : texte + 9 photos, likes (avatars), commentaires, visibilité par tags | +| 🏷️ Tags d'amis | Plusieurs tags par ami (12 couleurs), filtrage des contacts | +| 🗂️ Stockage R2 | Cloudflare R2 pour images/audio — CDN optionnel | +| 🏗️ Auto-hébergeable | Docker Compose en une commande | --- -## Tech Stack +## Stack technique ``` Backend (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — utilisateurs, messages chiffrés + Redis — présence en ligne + routage inter-nœuds + Cloudflare R2 — stockage fichiers (API S3) + JWT + bcrypt Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) - libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls - PWA: manifest.json + Service Worker + HTML natif + Vanilla JS (ESM, sans bundler) + libsodium-wrappers (WebAssembly) + WebRTC API — appels vidéo/audio + PWA : manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +Cryptographie + ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message + Persistance 4 niveaux : mémoire → localStorage → sessionStorage → IndexedDB + Clés privées uniquement sur l'appareil ``` --- -## Quick Start +## Démarrage rapide -### Option 0: Zeabur One-Click Cloud Deploy +### Option 0 : Zeabur en un clic [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> Étape manuelle requise après déploiement : +> 1. Console Zeabur → **server** → Variables → copier `ZEABUR_WEB_URL` +> 2. **client** → Variables → ajouter `SERVER_URL` = valeur copiée +> 3. Redémarrer le client -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: - ```bash - hostname -i - ``` - ---- - -### Option 1: Docker Compose (Recommended — no local build needed) +### Option 1 : Docker Compose (recommandé) ```bash -# Clone the repository git clone && cd paperphone - -# Copy and edit environment variables cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. - -# Pull images and start everything +# Éditer les variables docker compose up -d - -# Check service status -docker compose ps - -# Open in browser open http://localhost ``` -> Pre-built images on Docker Hub: -> - `facilisvelox/paperphone-client:latest` -> - `facilisvelox/paperphone-server:latest` -> -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. - -### Option 2: Manual Local Start - -#### 1. Prepare the environment +### Option 2 : Démarrage local ```bash -# Copy and edit environment variables -cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# Backend +cd server && npm install && npm run dev -# Note: the server auto-runs schema.sql on first startup -``` - -#### 2. Start the backend - -```bash -cd server -npm install -npm run dev # → http://localhost:3000 -``` - -#### 3. Start the frontend - -```bash +# Frontend npx serve client -p 8080 -# → http://localhost:8080 ``` --- -## Video Call Configuration - -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. - -### Using Cloudflare TURN (Recommended) - -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +## Appels vidéo — Cloudflare TURN ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) - -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. - -### Call Types - -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +> Sans configuration : STUN uniquement, appels LAN fonctionnent. --- -## Push Notification Configuration +## Notifications push -Offline message notifications are delivered through **two channels** for maximum delivery rate: - -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | - -### Configuring Web Push - -1. Generate VAPID keys (one-time): +| Canal | Plateformes | Configuration | +|-------|-------------|---------------| +| Web Push | Navigateurs + iOS PWA 16.4+ | Clés VAPID | +| OneSignal | Apps Median.co | App ID + REST Key | ```bash -cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +--- -```env -VAPID_PUBLIC_KEY=your_public_key_here -VAPID_PRIVATE_KEY=your_private_key_here -VAPID_SUBJECT=mailto:admin@your-domain.com -``` +## iOS — Sans certificat -3. Restart the server — users can enable notifications from the Settings page - -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. - -### Configuring OneSignal (Median.co Native Apps) - -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: - -```env -ONESIGNAL_APP_ID=your_onesignal_app_id -ONESIGNAL_REST_KEY=your_onesignal_rest_api_key -``` - -> **When not configured**: push notifications are silently disabled — all other features work normally. +1. Déployer avec HTTPS → 2. Ouvrir dans Safari → 3. Partager ⬆️ → 4. « Ajouter à l'écran d'accueil » --- -## iOS — Permanent No-Cert Deployment +## Sécurité -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** - -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. - ---- - -## Production Deployment (Nginx) - -```nginx -server { - listen 443 ssl http2; - server_name your.domain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - # Frontend static files - location / { - root /path/to/paperphone/client; - try_files $uri /index.html; - } - - # REST API - location /api/ { - proxy_pass http://localhost:3000; - proxy_set_header Host $host; - } - - # WebSocket (messaging + call signalling) - location /ws { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 3600s; - } -} +``` +Inscription : IK + SPK + 10×OPK générés localement, clés publiques uploadées +Message : ECDH éphémère → X25519 → XSalsa20-Poly1305 +Serveur voit : ✅ texte chiffré + métadonnées ❌ texte clair / clés privées ``` --- -## Project Structure +## Variables d'environnement -``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) -``` +| Variable | Description | Défaut | +|----------|-------------|--------| +| `PORT` | Port serveur | `3000` | +| `JWT_SECRET` | Clé JWT (**changer en prod**) | dev_secret | +| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL | — | +| `REDIS_HOST`/`REDIS_PASS` | Redis | — | +| `R2_ACCOUNT_ID` | Cloudflare ID | — | +| `R2_ACCESS_KEY_ID` | Clé d'accès R2 | — | +| `R2_SECRET_ACCESS_KEY` | Clé secrète R2 | — | +| `R2_BUCKET` | Nom du bucket | — | +| `R2_PUBLIC_URL` | URL publique (optionnel) | — | +| `CF_CALLS_APP_ID` | Calls App ID (optionnel) | — | +| `CF_CALLS_APP_SECRET` | Calls Secret (optionnel) | — | +| `VAPID_PUBLIC_KEY` | Clé publique VAPID (optionnel) | — | +| `VAPID_PRIVATE_KEY` | Clé privée VAPID (optionnel) | — | +| `ONESIGNAL_APP_ID` | OneSignal ID (optionnel) | — | +| `ONESIGNAL_REST_KEY` | OneSignal Key (optionnel) | — | --- -## Database Schema - -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | - ---- - -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## Licence MIT © PaperPhone Contributors diff --git a/README_JA.md b/README_JA.md index 9cd0a33..adbe012 100644 --- a/README_JA.md +++ b/README_JA.md @@ -10,112 +10,113 @@ WeChatスタイルのエンドツーエンド暗号化インスタントメッ --- ui -## Features -| Feature | Description | -|---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +## 機能一覧 + +| 機能 | 説明 | +|------|------| +| 🔐 エンドツーエンド暗号化 | ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時鍵、前方秘匿性 | +| 🗝️ ゼロ知識サーバー | サーバーは暗号文のみ保存。秘密鍵はデバイスから離れません | +| 📹 ビデオ/音声通話 | WebRTC P2P(1:1)+ Mesh(グループ)、Cloudflare TURN による NAT トラバーサル | +| 👥 グループチャット | 最大2000人、プレーンテキストメッセージ(暗号化なし)、通知オフモード、メンバー管理 | +| ⏱️ メッセージ自動削除 | 5段階(なし/1日/3日/1週間/1ヶ月)、DM は双方設定可、グループはオーナーのみ | +| 🔔 プッシュ通知 | Web Push (VAPID) + OneSignal デュアルチャネル — オフラインでも通知 | +| 🌐 多言語対応 | 中国語・英語・日本語・韓国語・フランス語・ドイツ語・ロシア語・スペイン語(自動検出+手動切替) | +| 📱 iOS 永久署名不要 | PWA — Safari「ホーム画面に追加」でエンタープライズ証明書なしで利用可能 | +| 💬 リッチメッセージ | テキスト・画像・音声・絵文字パネル(64種)・既読確認 | +| 🌐 モーメンツ | テキスト+最大9枚写真、いいね(友達アバター表示)、コメント、タグベースの公開範囲制御 | +| 🏷️ フレンドタグ | 友達に複数タグを設定(12色プリセット)、タグ別に連絡先をフィルタリング | +| 🗂️ R2 オブジェクトストレージ | Cloudflare R2 で画像/音声ファイルを保存 — オプションの CDN 直リンク | +| 🏗️ セルフホスト対応 | Docker Compose ワンコマンドデプロイ、Node.js + Redis マルチノード対応 | --- -## Tech Stack +## 技術スタック ``` -Backend (server/) +バックエンド (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — ユーザー・メッセージの永続化 + Redis — オンラインプレゼンス+クロスノードルーティング + Cloudflare R2 — 画像/音声ファイルストレージ(S3互換API) + JWT + bcrypt 認証 -Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) - libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls +フロントエンド (client/) + ネイティブ HTML + Vanilla JS(ESM、バンドラー不要) + libsodium-wrappers(WebAssembly — Curve25519 / XSalsa20-Poly1305) + WebRTC API — ビデオ/音声通話 PWA: manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +暗号化レイヤー + ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時 ECDH 鍵ペア + 秘密鍵4層永続化: メモリ → localStorage → sessionStorage → IndexedDB + 秘密鍵はデバイスにのみ保存、サーバーに送信されることはありません ``` --- -## Quick Start +## クイックスタート -### Option 0: Zeabur One-Click Cloud Deploy +### 方法0:Zeabur ワンクリッククラウドデプロイ [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> テンプレートのデプロイ後、1つの手動手順が必要です。これを行わないとログイン/登録が機能しません: +> 1. Zeabur コンソール → **server サービス** → 環境変数 → `ZEABUR_WEB_URL` の値をコピー(例:`http://10.43.x.x:3000`) +> 2. **client サービス** → 環境変数 → 変数 `SERVER_URL` を追加 = 上記でコピーした値 +> 3. client サービスを再起動 -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: +**既知の注意事項:** +- 初回起動時、サーバーは自動的にすべてのデータベーステーブルを作成します(`CREATE TABLE IF NOT EXISTS`)— SQL の手動インポートは不要 +- Redis はクラスター内でパスワードなしで動作します +- MySQL アクセスが拒否された場合、server サービスの `DB_PASS` を MySQL サービスの `MYSQL_ROOT_PASSWORD` に手動設定してください +- サービスコンテナの**内部IP**を取得するには、Zeabur コンソールで該当サービスのターミナルを開き、以下を実行: ```bash hostname -i ``` --- -### Option 1: Docker Compose (Recommended — no local build needed) +### 方法1:Docker Compose(推奨 — ローカルビルド不要) ```bash -# Clone the repository +# リポジトリをクローン git clone && cd paperphone -# Copy and edit environment variables +# 環境変数をコピーして編集 cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. +# DB_PASS / JWT_SECRET / R2_* などを編集 -# Pull images and start everything +# イメージを取得して起動 docker compose up -d -# Check service status +# サービスステータスを確認 docker compose ps -# Open in browser +# ブラウザで開く open http://localhost ``` -> Pre-built images on Docker Hub: +> Docker Hub のビルド済みイメージ: > - `facilisvelox/paperphone-client:latest` > - `facilisvelox/paperphone-server:latest` > -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. +> **注意**:サーバーは初回起動時に自動的にデータベーススキーマを初期化します — SQL の手動インポートは不要です。 -### Option 2: Manual Local Start +### 方法2:ローカル手動起動 -#### 1. Prepare the environment +#### 1. 環境を準備 ```bash -# Copy and edit environment variables +# 環境変数をコピーして編集 cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# DB_HOST / DB_PASS / REDIS_HOST / R2_* などを入力 -# Note: the server auto-runs schema.sql on first startup +# 注:サーバーは初回起動時に自動で schema.sql を実行します ``` -#### 2. Start the backend +#### 2. バックエンドを起動 ```bash cd server @@ -123,7 +124,7 @@ npm install npm run dev # → http://localhost:3000 ``` -#### 3. Start the frontend +#### 3. フロントエンドを起動 ```bash npx serve client -p 8080 @@ -132,54 +133,54 @@ npx serve client -p 8080 --- -## Video Call Configuration +## ビデオ通話の設定 -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. +ビデオ通話と音声通話は WebRTC P2P を使用し、同一 LAN 内ではすぐに使えます。異なるネットワーク間の通話には、NAT トラバーサル用の TURN サーバーが必要です。 -### Using Cloudflare TURN (Recommended) +### Cloudflare TURN の使用(推奨) -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → App を作成 +2. **App ID** と **App Secret**(Token Key)をコピー +3. `server/.env` に追加: ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) +4. バックエンドを再起動 — TURN クレデンシャルは通話セッションごとに自動取得されます(TTL: 86,400秒) -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. +> **未設定時**:STUN のみにフォールバックします(Google + Cloudflare パブリック STUN)。LAN 内の通話は追加設定なしで動作します。 -### Call Types +### 通話タイプ -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +| タイプ | 通信方式 | 推奨用途 | +|--------|----------|----------| +| 1:1 ビデオ通話 | WebRTC P2P + TURN | すべてのシナリオ | +| 1:1 音声通話 | WebRTC P2P + TURN | すべてのシナリオ | +| グループ通話 | WebRTC Mesh(フルメッシュ) | 最大6人 | --- -## Push Notification Configuration +## プッシュ通知の設定 -Offline message notifications are delivered through **two channels** for maximum delivery rate: +オフラインメッセージ通知は**2つのチャネル**で配信され、配信率を最大化します: -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | +| チャネル | 対応プラットフォーム | 設定 | +|----------|----------------------|------| +| Web Push (VAPID) | ブラウザ (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID キー | +| OneSignal | Median.co 経由のネイティブ Android/iOS アプリ | OneSignal App ID + REST Key | -### Configuring Web Push +### Web Push の設定 -1. Generate VAPID keys (one-time): +1. VAPID キーを生成(1回のみ): ```bash cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +2. `server/.env` に追加: ```env VAPID_PUBLIC_KEY=your_public_key_here @@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here VAPID_SUBJECT=mailto:admin@your-domain.com ``` -3. Restart the server — users can enable notifications from the Settings page +3. サーバーを再起動 — ユーザーは設定ページから通知を有効にできます -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. +> **iOS ユーザー**は先に「ホーム画面に追加」(PWA)を行う必要があり、iOS 16.4以上のみサポートされます。 -### Configuring OneSignal (Median.co Native Apps) +### OneSignal の設定(Median.co ネイティブアプリ) -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: +1. [OneSignal Dashboard](https://onesignal.com) でアプリを作成し、Firebase を設定 +2. Median.co で OneSignal を有効にし、App ID を入力 +3. OneSignal の **App ID** と **REST API Key** を `server/.env` に追加: ```env ONESIGNAL_APP_ID=your_onesignal_app_id ONESIGNAL_REST_KEY=your_onesignal_rest_api_key ``` -> **When not configured**: push notifications are silently disabled — all other features work normally. +> **未設定時**:プッシュ通知は自動的に無効化されます — 他の機能には影響しません。 --- -## iOS — Permanent No-Cert Deployment +## iOS — 証明書不要の永続デプロイ -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** +1. HTTPS ドメインのサーバーにデプロイ(WebRTC と Web Crypto API には HTTPS が必要) +2. **Safari** で `https://your.domain.com` を開く +3. 画面下部の共有ボタン ⬆️ をタップ +4. **ホーム画面に追加** → **追加** を選択 -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. +ネイティブアプリと同様に動作します — Apple エンタープライズ証明書不要、期限なし。 --- -## Production Deployment (Nginx) +## 本番デプロイ(Nginx) ```nginx server { @@ -227,7 +228,7 @@ server { ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; - # Frontend static files + # フロントエンド静的ファイル location / { root /path/to/paperphone/client; try_files $uri /index.html; @@ -239,7 +240,7 @@ server { proxy_set_header Host $host; } - # WebSocket (messaging + call signalling) + # WebSocket(メッセージング+通話シグナリング) location /ws { proxy_pass http://localhost:3000; proxy_http_version 1.1; @@ -252,125 +253,69 @@ server { --- -## Project Structure +## データベーススキーマ + +11テーブル、サーバー初回起動時に自動作成(`CREATE TABLE IF NOT EXISTS`): + +| テーブル | 用途 | +|----------|------| +| `users` | ユーザー情報 + ECDH/OPK 公開鍵 | +| `prekeys` | X3DH ワンタイムプリキープール | +| `friends` | 友達関係(pending / accepted / blocked) | +| `groups` / `group_members` | グループチャット+メンバー(通知オフ状態含む) | +| `messages` | 暗号化メッセージ(オフラインバッファ、配信後削除可能) | +| `moments` | ソーシャル投稿(テキスト ≤ 1024文字) | +| `moment_images` | 投稿画像(1投稿最大9枚) | +| `moment_likes` | いいね(ユーザーごと投稿ごとにユニーク) | +| `moment_comments` | コメント(≤ 512文字/件) | +| `push_subscriptions` | Web Push サブスクリプション(VAPID) | +| `onesignal_players` | OneSignal デバイス登録(Median.co) | + +--- + +## セキュリティモデル ``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) +登録時: + デバイスが IK(アイデンティティキー)+ SPK(署名付きプリキー)+ 10× OPK(ワンタイムプリキー)を生成 + 公開鍵はアップロード、秘密鍵はデバイスに保存(4層永続化) + +メッセージ送信時: + 送信者が受信者の IK 公開鍵を取得 + 一時 ECDH 鍵ペアを生成(メッセージごとに新しいペア) + X25519 ECDH → 共有秘密 → XSalsa20-Poly1305 暗号化 + 一時公開鍵はメッセージヘッダーで送信、使用後に破棄 + +サーバーが見るもの: + ✅ 暗号文ブロブ+ルーティングメタデータ(送受信者UUID、タイムスタンプ) + ❌ 平文 / 秘密鍵 / 一時鍵 / 通話内容 ``` --- -## Database Schema +## 環境変数リファレンス -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | +| 変数 | 説明 | デフォルト | +|------|------|------------| +| `PORT` | サーバーポート | `3000` | +| `JWT_SECRET` | JWT 署名キー(**本番環境では必ず変更**) | dev_secret | +| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 接続設定 | — | +| `REDIS_HOST` / `REDIS_PASS` | Redis 接続設定 | — | +| `R2_ACCOUNT_ID` | Cloudflare アカウント ID | — | +| `R2_ACCESS_KEY_ID` | R2 API トークンのアクセスキー | — | +| `R2_SECRET_ACCESS_KEY` | R2 API トークンのシークレットキー | — | +| `R2_BUCKET` | R2 バケット名 | — | +| `R2_PUBLIC_URL` | R2 公開 URL(任意)— CDN 直リンクを有効化 | — | +| `CF_CALLS_APP_ID` | Cloudflare Calls App ID(任意) | — | +| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret(任意) | — | +| `VAPID_PUBLIC_KEY` | Web Push VAPID 公開鍵(任意) | — | +| `VAPID_PRIVATE_KEY` | Web Push VAPID 秘密鍵(任意) | — | +| `VAPID_SUBJECT` | VAPID 連絡先メール(任意) | `mailto:admin@paperphone.app` | +| `ONESIGNAL_APP_ID` | OneSignal App ID(任意、Median.co用) | — | +| `ONESIGNAL_REST_KEY` | OneSignal REST API Key(任意) | — | --- -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## ライセンス MIT © PaperPhone Contributors diff --git a/README_KO.md b/README_KO.md index 5a182dc..d8d55eb 100644 --- a/README_KO.md +++ b/README_KO.md @@ -10,112 +10,113 @@ WeChat 스타일의 종단간 암호화 인스턴트 메시징 앱. 무상태 EC --- ui -## Features -| Feature | Description | -|---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +## 기능 + +| 기능 | 설명 | +|------|------| +| 🔐 종단간 암호화 | 무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 키, 전방 비밀성 | +| 🗝️ 제로 지식 서버 | 서버는 암호문만 저장, 개인 키는 기기를 떠나지 않음 | +| 📹 영상/음성 통화 | WebRTC P2P (1:1) + Mesh (그룹), Cloudflare TURN을 통한 NAT 트래버설 | +| 👥 그룹 채팅 | 최대 2000명, 일반 텍스트 메시지 (비암호화), 방해 금지 모드, 멤버 관리 | +| ⏱️ 메시지 자동 삭제 | 5단계 (안함/1일/3일/1주/1개월), DM에서 양쪽 설정 가능, 그룹은 방장만 | +| 🔔 알림 | Web Push (VAPID) + OneSignal 이중 채널 — 오프라인에서도 알림 수신 | +| 🌐 다국어 | 중국어·영어·일본어·한국어·프랑스어·독일어·러시아어·스페인어 (자동 감지 + 수동 전환) | +| 📱 iOS — 기업 인증서 불필요 | Safari "홈 화면에 추가"를 통한 PWA, Apple 서명 없이 영구 작동 | +| 💬 풍부한 메시징 | 텍스트, 이미지, 음성 메시지, 이모지 패널 (64종), 읽음 확인 | +| 🌐 모먼트 | 텍스트 + 최대 9장 사진, 좋아요 (친구 아바타 표시), 댓글, 태그 기반 공개 범위 제어 | +| 🏷️ 친구 태그 | 친구에게 여러 태그 할당 (12색 프리셋), 태그별 연락처 필터링 | +| 🗂️ R2 오브젝트 스토리지 | Cloudflare R2로 이미지/음성 파일 저장 — 선택적 공개 CDN URL | +| 🏗️ 셀프 호스팅 가능 | Docker Compose 원커맨드 배포, Node.js + Redis 멀티 노드 지원 | --- -## Tech Stack +## 기술 스택 ``` -Backend (server/) +백엔드 (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — 사용자, 메시지 영속화 (암호문) + Redis — 온라인 상태 + 크로스 노드 라우팅 + Cloudflare R2 — 이미지/음성 파일 저장소 (S3 호환 API) + JWT + bcrypt 인증 -Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) +프론트엔드 (client/) + 네이티브 HTML + Vanilla JS (ESM, 번들러 불필요) libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls + WebRTC API — 영상/음성 통화 PWA: manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +암호화 레이어 + 무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 ECDH 키페어 + 개인 키 4단계 영속화: 메모리 → localStorage → sessionStorage → IndexedDB + 모든 개인 키는 기기에만 저장 — 절대 서버로 전송되지 않음 ``` --- -## Quick Start +## 빠른 시작 -### Option 0: Zeabur One-Click Cloud Deploy +### 방법 0: Zeabur 원클릭 클라우드 배포 [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> 템플릿 배포 후 수동 단계 하나가 필요합니다. 이 작업을 하지 않으면 로그인/회원가입이 작동하지 않습니다: +> 1. Zeabur 콘솔 → **server 서비스** → 환경 변수 → `ZEABUR_WEB_URL` 값 복사 (예: `http://10.43.x.x:3000`) +> 2. **client 서비스** → 환경 변수 → 변수 `SERVER_URL` 추가 = 위에서 복사한 값 +> 3. client 서비스 재시작 -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: +**알려진 참고 사항:** +- 첫 시작 시 서버가 자동으로 모든 데이터베이스 테이블을 생성합니다 (`CREATE TABLE IF NOT EXISTS`) — SQL 수동 임포트 불필요 +- Redis는 클러스터 내에서 비밀번호 없이 작동합니다 +- MySQL 접근이 거부되면 server 서비스의 `DB_PASS`를 MySQL 서비스의 `MYSQL_ROOT_PASSWORD` 값으로 수동 설정하세요 +- 서비스 컨테이너의 **내부 IP**를 확인하려면 Zeabur 콘솔에서 해당 서비스의 터미널을 열고 실행: ```bash hostname -i ``` --- -### Option 1: Docker Compose (Recommended — no local build needed) +### 방법 1: Docker Compose (권장 — 로컬 빌드 불필요) ```bash -# Clone the repository +# 저장소 클론 git clone && cd paperphone -# Copy and edit environment variables +# 환경 변수 복사 및 편집 cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. +# DB_PASS / JWT_SECRET / R2_* 등 입력 -# Pull images and start everything +# 이미지 풀 및 시작 docker compose up -d -# Check service status +# 서비스 상태 확인 docker compose ps -# Open in browser +# 브라우저에서 열기 open http://localhost ``` -> Pre-built images on Docker Hub: +> Docker Hub의 사전 빌드 이미지: > - `facilisvelox/paperphone-client:latest` > - `facilisvelox/paperphone-server:latest` > -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. +> **참고**: 서버는 첫 시작 시 자동으로 데이터베이스 스키마를 초기화합니다 — SQL 수동 임포트가 필요하지 않습니다. -### Option 2: Manual Local Start +### 방법 2: 로컬 수동 시작 -#### 1. Prepare the environment +#### 1. 환경 준비 ```bash -# Copy and edit environment variables +# 환경 변수 복사 및 편집 cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# DB_HOST / DB_PASS / REDIS_HOST / R2_* 등 입력 -# Note: the server auto-runs schema.sql on first startup +# 참고: 서버는 첫 시작 시 자동으로 schema.sql을 실행합니다 ``` -#### 2. Start the backend +#### 2. 백엔드 시작 ```bash cd server @@ -123,7 +124,7 @@ npm install npm run dev # → http://localhost:3000 ``` -#### 3. Start the frontend +#### 3. 프론트엔드 시작 ```bash npx serve client -p 8080 @@ -132,54 +133,54 @@ npx serve client -p 8080 --- -## Video Call Configuration +## 영상 통화 설정 -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. +영상 및 음성 통화는 WebRTC P2P를 사용하며 동일 LAN에서 바로 사용할 수 있습니다. 다른 네트워크 간 통화에는 NAT 트래버설을 위한 TURN 서버가 필요합니다. -### Using Cloudflare TURN (Recommended) +### Cloudflare TURN 사용 (권장) -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → 앱 생성 +2. **App ID**와 **App Secret** (토큰 키) 복사 +3. `server/.env`에 추가: ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) +4. 백엔드 재시작 — TURN 자격 증명은 통화 세션마다 자동 발급됩니다 (TTL: 86,400초) -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. +> **미설정 시**: STUN 전용으로 폴백합니다 (Google + Cloudflare 공개 STUN). LAN 통화는 추가 설정 없이 작동합니다. -### Call Types +### 통화 유형 -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +| 유형 | 전송 방식 | 권장 사용 | +|------|-----------|-----------| +| 1:1 영상 통화 | WebRTC P2P + TURN | 모든 시나리오 | +| 1:1 음성 통화 | WebRTC P2P + TURN | 모든 시나리오 | +| 그룹 영상/음성 | WebRTC Mesh (풀 메시) | 최대 6명 | --- -## Push Notification Configuration +## 푸시 알림 설정 -Offline message notifications are delivered through **two channels** for maximum delivery rate: +오프라인 메시지 알림은 **두 채널**을 통해 배달되어 최대 전달률을 보장합니다: -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | +| 채널 | 플랫폼 | 설정 | +|------|--------|------| +| Web Push (VAPID) | 브라우저 (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID 키 | +| OneSignal | Median.co 경유 네이티브 Android/iOS 앱 | OneSignal App ID + REST Key | -### Configuring Web Push +### Web Push 설정 -1. Generate VAPID keys (one-time): +1. VAPID 키 생성 (1회만): ```bash cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +2. `server/.env`에 추가: ```env VAPID_PUBLIC_KEY=your_public_key_here @@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here VAPID_SUBJECT=mailto:admin@your-domain.com ``` -3. Restart the server — users can enable notifications from the Settings page +3. 서버 재시작 — 사용자가 설정 페이지에서 알림을 활성화할 수 있습니다 -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. +> **iOS 사용자**는 먼저 "홈 화면에 추가" (PWA)를 해야 하며, iOS 16.4 이상만 지원됩니다. -### Configuring OneSignal (Median.co Native Apps) +### OneSignal 설정 (Median.co 네이티브 앱) -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: +1. [OneSignal Dashboard](https://onesignal.com)에서 앱 생성 및 Firebase 설정 +2. Median.co에서 OneSignal 활성화 후 App ID 입력 +3. OneSignal **App ID**와 **REST API Key**를 `server/.env`에 추가: ```env ONESIGNAL_APP_ID=your_onesignal_app_id ONESIGNAL_REST_KEY=your_onesignal_rest_api_key ``` -> **When not configured**: push notifications are silently disabled — all other features work normally. +> **미설정 시**: 푸시 알림이 자동으로 비활성화됩니다 — 다른 기능에는 영향 없습니다. --- -## iOS — Permanent No-Cert Deployment +## iOS — 인증서 없는 영구 배포 -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** +1. HTTPS 도메인 서버에 배포 (WebRTC 및 Web Crypto API에 HTTPS 필요) +2. **Safari**에서 `https://your.domain.com` 열기 +3. 화면 하단의 공유 버튼 ⬆️ 탭 +4. **홈 화면에 추가** → **추가** 선택 -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. +네이티브 앱과 동일하게 작동합니다 — Apple 기업 인증서 불필요, 만료 없음. --- -## Production Deployment (Nginx) +## 프로덕션 배포 (Nginx) ```nginx server { @@ -227,7 +228,7 @@ server { ssl_certificate /path/to/cert.pem; ssl_certificate_key /path/to/key.pem; - # Frontend static files + # 프론트엔드 정적 파일 location / { root /path/to/paperphone/client; try_files $uri /index.html; @@ -239,7 +240,7 @@ server { proxy_set_header Host $host; } - # WebSocket (messaging + call signalling) + # WebSocket (메시징 + 통화 시그널링) location /ws { proxy_pass http://localhost:3000; proxy_http_version 1.1; @@ -252,125 +253,69 @@ server { --- -## Project Structure +## 데이터베이스 스키마 + +11개 테이블, 서버 첫 시작 시 자동 생성 (`CREATE TABLE IF NOT EXISTS`): + +| 테이블 | 용도 | +|--------|------| +| `users` | 사용자 프로필 + ECDH/OPK 공개 키 | +| `prekeys` | X3DH 원타임 프리키 풀 | +| `friends` | 친구 관계 (pending / accepted / blocked) | +| `groups` / `group_members` | 그룹 채팅 + 멤버 (알림 끄기 상태 포함) | +| `messages` | 암호화된 메시지 (오프라인 버퍼, 전달 후 삭제 가능) | +| `moments` | 소셜 게시물 (텍스트 ≤ 1024자) | +| `moment_images` | 게시물 이미지 (게시물당 최대 9개) | +| `moment_likes` | 좋아요 (사용자당 게시물당 고유) | +| `moment_comments` | 댓글 (≤ 512자) | +| `push_subscriptions` | Web Push 구독 (VAPID) | +| `onesignal_players` | OneSignal 기기 등록 (Median.co) | + +--- + +## 보안 모델 ``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) +등록 시: + 기기가 IK (아이덴티티 키) + SPK (서명 프리키) + 10× OPK (원타임 프리키) 생성 + 공개 키는 업로드, 개인 키는 기기에 보관 (4단계 영속화) + +메시지 전송 시: + 발신자가 수신자의 IK 공개 키를 가져옴 + 임시 ECDH 키페어 생성 (메시지마다 새로운 페어) + X25519 ECDH → 공유 비밀 → XSalsa20-Poly1305 암호화 + 임시 공개 키는 메시지 헤더로 전송, 사용 후 파기 + +서버가 보는 것: + ✅ 암호문 블롭 + 라우팅 메타데이터 (발신자/수신자 UUID, 타임스탬프) + ❌ 평문 / 개인 키 / 임시 키 / 통화 내용 ``` --- -## Database Schema +## 환경 변수 -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | +| 변수 | 설명 | 기본값 | +|------|------|--------| +| `PORT` | 서버 포트 | `3000` | +| `JWT_SECRET` | JWT 서명 키 (**프로덕션에서 반드시 변경**) | dev_secret | +| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 연결 | — | +| `REDIS_HOST` / `REDIS_PASS` | Redis 연결 | — | +| `R2_ACCOUNT_ID` | Cloudflare 계정 ID | — | +| `R2_ACCESS_KEY_ID` | R2 API 토큰 액세스 키 | — | +| `R2_SECRET_ACCESS_KEY` | R2 API 토큰 시크릿 키 | — | +| `R2_BUCKET` | R2 버킷 이름 | — | +| `R2_PUBLIC_URL` | R2 공개 기본 URL (선택) — CDN 직접 링크 활성화 | — | +| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (선택) | — | +| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (선택) | — | +| `VAPID_PUBLIC_KEY` | Web Push VAPID 공개 키 (선택) | — | +| `VAPID_PRIVATE_KEY` | Web Push VAPID 개인 키 (선택) | — | +| `VAPID_SUBJECT` | VAPID 연락처 이메일 (선택) | `mailto:admin@paperphone.app` | +| `ONESIGNAL_APP_ID` | OneSignal App ID (선택, Median.co용) | — | +| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (선택) | — | --- -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## 라이선스 MIT © PaperPhone Contributors diff --git a/README_RU.md b/README_RU.md index 0e6026b..c2254fd 100644 --- a/README_RU.md +++ b/README_RU.md @@ -10,367 +10,173 @@ --- ui -## Features -| Feature | Description | -|---------|-------------| -| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy | -| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device | -| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal | -| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management | -| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups | -| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline | -| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch | -| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing | -| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators | -| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control | -| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag | -| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL | -| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready | +## Возможности + +| Функция | Описание | +|---------|----------| +| 🔐 Сквозное шифрование | Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерные ключи на каждое сообщение, прямая секретность | +| 🗝️ Сервер с нулевым знанием | Сервер хранит только шифротекст; закрытые ключи никогда не покидают устройство | +| 📹 Видео/аудио звонки | WebRTC P2P (1:1) + Mesh (группа), Cloudflare TURN для NAT-обхода | +| 👥 Групповой чат | До 2000 участников, обычный текст (без шифрования), режим «Не беспокоить», управление участниками | +| ⏱️ Автоудаление | 5 уровней (никогда/1д/3д/1нед/1мес), в ЛС — обе стороны, в группах — только владелец | +| 🔔 Push-уведомления | Web Push (VAPID) + OneSignal — двойной канал | +| 🌐 Многоязычность | ZH/EN/JA/KO/FR/DE/RU/ES — автоопределение + ручной выбор | +| 📱 iOS без сертификата | PWA через Safari «На экран Домой», без корпоративного сертификата Apple | +| 💬 Расширенные сообщения | Текст, изображения, голосовые сообщения, 64 эмодзи, подтверждения прочтения | +| 🌐 Моменты | Социальная лента: текст + до 9 фото, лайки (аватары друзей), комментарии, управление видимостью по тегам | +| 🏷️ Теги друзей | Несколько тегов на друга (палитра из 12 цветов), фильтрация контактов по тегам | +| 🗂️ Хранилище R2 | Cloudflare R2 для изображений/аудио — опциональный CDN URL | +| 🏗️ Самостоятельное размещение | Docker Compose в одну команду | --- -## Tech Stack +## Технологический стек ``` -Backend (server/) +Бэкенд (server/) Node.js 20 + Express + ws - MySQL 8.0 — users, messages (persisted ciphertext) - Redis — online presence + cross-node routing - Cloudflare R2 — image/voice file storage (S3-compatible API) - JWT + bcrypt authentication + MySQL 8.0 — пользователи, сообщения (шифротекст) + Redis — статус онлайн + маршрутизация между узлами + Cloudflare R2 — хранилище файлов (S3-совместимый API) + JWT + bcrypt аутентификация -Frontend (client/) - Native HTML + Vanilla JS (ESM, no bundler required) +Фронтенд (client/) + Нативный HTML + Vanilla JS (ESM, без сборщика) libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305) - WebRTC API — video / voice calls + WebRTC API — видео/аудио звонки PWA: manifest.json + Service Worker -Cryptographic Layer - Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message - Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB - All private keys stored on-device only — never sent to the server +Криптографический слой + Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерная пара ключей на сообщение + 4-уровневое хранение ключей: память → localStorage → sessionStorage → IndexedDB + Закрытые ключи только на устройстве — никогда не отправляются на сервер ``` --- -## Quick Start +## Быстрый старт -### Option 0: Zeabur One-Click Cloud Deploy +### Вариант 0: Zeabur — облачное развёртывание в один клик [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev) > [!NOTE] -> One manual step is required after the template deploys, otherwise login/register won't work: -> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`) -> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above -> 3. Restart the client service +> После развёртывания шаблона необходим один ручной шаг: +> 1. Консоль Zeabur → сервис **server** → Переменные → скопировать `ZEABUR_WEB_URL` +> 2. Сервис **client** → Переменные → добавить `SERVER_URL` = скопированное значение +> 3. Перезапустить сервис client -**Known notes:** -- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed -- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient) -- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service -- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run: - ```bash - hostname -i - ``` - ---- - -### Option 1: Docker Compose (Recommended — no local build needed) +### Вариант 1: Docker Compose (рекомендуется) ```bash -# Clone the repository git clone && cd paperphone - -# Copy and edit environment variables cp server/.env.example server/.env -# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc. - -# Pull images and start everything +# Заполнить: DB_PASS / JWT_SECRET / R2_* и т.д. docker compose up -d - -# Check service status -docker compose ps - -# Open in browser open http://localhost ``` -> Pre-built images on Docker Hub: -> - `facilisvelox/paperphone-client:latest` -> - `facilisvelox/paperphone-server:latest` -> -> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required. +> Образы Docker Hub: `facilisvelox/paperphone-client:latest` и `facilisvelox/paperphone-server:latest` -### Option 2: Manual Local Start - -#### 1. Prepare the environment +### Вариант 2: Локальный ручной запуск ```bash -# Copy and edit environment variables -cp server/.env.example server/.env -# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc. +# Бэкенд +cd server && npm install && npm run dev # → http://localhost:3000 -# Note: the server auto-runs schema.sql on first startup -``` - -#### 2. Start the backend - -```bash -cd server -npm install -npm run dev # → http://localhost:3000 -``` - -#### 3. Start the frontend - -```bash -npx serve client -p 8080 -# → http://localhost:8080 +# Фронтенд +npx serve client -p 8080 # → http://localhost:8080 ``` --- -## Video Call Configuration - -Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal. - -### Using Cloudflare TURN (Recommended) - -1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App -2. Copy the **App ID** and **App Secret** (Token Key) -3. Add them to `server/.env`: +## Настройка видеозвонков — Cloudflare TURN ```env CF_CALLS_APP_ID=your_app_id_here CF_CALLS_APP_SECRET=your_app_secret_here ``` -4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s) +| Тип | Транспорт | Рекомендуется для | +|-----|-----------|-------------------| +| 1:1 Видео | WebRTC P2P + TURN | Все сценарии | +| 1:1 Голос | WebRTC P2P + TURN | Все сценарии | +| Групповой | WebRTC Mesh | До 6 участников | -> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration. - -### Call Types - -| Type | Transport | Recommended for | -|------|-----------|-----------------| -| 1:1 Video Call | WebRTC P2P + TURN | All scenarios | -| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios | -| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants | +> **Без настройки**: только STUN. Звонки в ЛВС работают без дополнительных настроек. --- -## Push Notification Configuration +## Push-уведомления -Offline message notifications are delivered through **two channels** for maximum delivery rate: - -| Channel | Platforms | Configuration | -|---------|-----------|---------------| -| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys | -| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key | - -### Configuring Web Push - -1. Generate VAPID keys (one-time): +| Канал | Платформы | Настройка | +|-------|-----------|-----------| +| Web Push | Браузеры + iOS PWA (Safari 16.4+) | Ключи VAPID | +| OneSignal | Нативные приложения Median.co | App ID + REST Key | ```bash -cd server npx web-push generate-vapid-keys ``` -2. Add to `server/.env`: +--- -```env -VAPID_PUBLIC_KEY=your_public_key_here -VAPID_PRIVATE_KEY=your_private_key_here -VAPID_SUBJECT=mailto:admin@your-domain.com -``` +## iOS — Постоянная установка без сертификата -3. Restart the server — users can enable notifications from the Settings page - -> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported. - -### Configuring OneSignal (Median.co Native Apps) - -1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase -2. Enable OneSignal in Median.co and enter the App ID -3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`: - -```env -ONESIGNAL_APP_ID=your_onesignal_app_id -ONESIGNAL_REST_KEY=your_onesignal_rest_api_key -``` - -> **When not configured**: push notifications are silently disabled — all other features work normally. +1. Развернуть на HTTPS-сервере → 2. Открыть в Safari → 3. Поделиться ⬆️ → 4. «На экран Домой» --- -## iOS — Permanent No-Cert Deployment +## Модель безопасности -1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs) -2. Open `https://your.domain.com` in **Safari** -3. Tap the Share button ⬆️ at the bottom of the screen -4. Select **Add to Home Screen** → **Add** - -The app behaves identically to a native app — no Apple enterprise certificate required, no expiry. - ---- - -## Production Deployment (Nginx) - -```nginx -server { - listen 443 ssl http2; - server_name your.domain.com; - - ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - - # Frontend static files - location / { - root /path/to/paperphone/client; - try_files $uri /index.html; - } - - # REST API - location /api/ { - proxy_pass http://localhost:3000; - proxy_set_header Host $host; - } - - # WebSocket (messaging + call signalling) - location /ws { - proxy_pass http://localhost:3000; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_read_timeout 3600s; - } -} +``` +Регистрация: IK + SPK + 10×OPK генерируются локально, публичные ключи загружаются +Сообщение: Эфемерный ECDH → X25519 → XSalsa20-Poly1305 +Сервер видит: ✅ шифротекст + метаданные маршрутизации ❌ открытый текст / закрытые ключи ``` --- -## Project Structure +## Схема базы данных -``` -paperphone/ -├── docker-compose.yml -├── server/ -│ ├── .env # Environment variables (incl. Cloudflare TURN keys) -│ └── src/ -│ ├── app.js # Express application -│ ├── routes/ -│ │ ├── auth.js # Register / Login (incl. X3DH public key upload) -│ │ ├── users.js # User search / Prekey bundle download -│ │ ├── friends.js # Friend requests / Accept (incl. offline push) -│ │ ├── groups.js # Group management -│ │ ├── messages.js # Historical messages (paginated ciphertext) -│ │ ├── upload.js # Cloudflare R2 file upload -│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set) -│ │ ├── moments.js # Moments feed (posts / likes / comments) -│ │ ├── calls.js # TURN credential issuance -│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal) -│ ├── services/ -│ │ ├── push.js # Web Push VAPID service -│ │ └── onesignal.js # OneSignal REST API service -│ └── ws/ -│ └── wsServer.js # WebSocket router (messages + call signalling + offline push) -│ -└── client/ - ├── index.html # SPA entry + PWA meta + Median push bridge - ├── manifest.json # PWA manifest - ├── sw.js # Service Worker (offline cache + push notifications) - └── src/ - ├── style.css # Premium design system (dark/light, glassmorphism) - ├── app.js # Router + global state + incoming call listener - ├── api.js # HTTP client - ├── socket.js # WebSocket client (auto-reconnect) - ├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es) - ├── services/ - │ ├── webrtc.js # WebRTC manager — CallManager class - │ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge) - ├── crypto/ - │ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768 - │ └── keystore.js # IndexedDB private key store - ├── pages/ - │ ├── login.js # Login / Register (key generation, language picker) - │ ├── chats.js # Chat list - │ ├── chat.js # Chat window (E2E encryption, call buttons) - │ ├── groups.js # Group list (create group, search) - │ ├── groupInfo.js # Group info (member management, DND, leave/disband) - │ ├── contacts.js # Contacts (friend requests, online status) - │ ├── discover.js # Discover page - │ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA) - │ └── call.js # Call UI (incoming / active / multi-party video) -``` +11 таблиц, создаются автоматически при первом запуске: + +| Таблица | Назначение | +|---------|------------| +| `users` | Профили + публичные ключи ECDH/OPK | +| `prekeys` | Одноразовые предключи X3DH | +| `friends` | Связи дружбы | +| `groups` / `group_members` | Группы + участники | +| `messages` | Зашифрованные сообщения | +| `moments` / `moment_images` | Посты + изображения | +| `moment_likes` / `moment_comments` | Лайки + комментарии | +| `push_subscriptions` | Web Push (VAPID) | +| `onesignal_players` | OneSignal устройства | --- -## Database Schema +## Переменные окружения -11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`): - -| Table | Purpose | -|-------|---------| -| `users` | User profiles + ECDH/OPK public keys | -| `prekeys` | X3DH one-time prekey pool | -| `friends` | Friendship relationships (pending / accepted / blocked) | -| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) | -| `messages` | Encrypted payloads (offline buffer, deletable after delivery) | -| `moments` | Social posts (text ≤ 1024 chars) | -| `moment_images` | Post images (up to 9 per post) | -| `moment_likes` | Likes (unique per user per post) | -| `moment_comments` | Comments (≤ 512 chars each) | -| `push_subscriptions` | Web Push subscriptions (VAPID) | -| `onesignal_players` | OneSignal device registrations (Median.co) | +| Переменная | Описание | По умолчанию | +|------------|----------|-------------| +| `PORT` | Порт сервера | `3000` | +| `JWT_SECRET` | Ключ подписи JWT (**изменить в продакшене**) | dev_secret | +| `DB_HOST`/`DB_PASS`/`DB_NAME` | Подключение MySQL | — | +| `REDIS_HOST`/`REDIS_PASS` | Подключение Redis | — | +| `R2_ACCOUNT_ID` | ID аккаунта Cloudflare | — | +| `R2_ACCESS_KEY_ID` | Ключ доступа R2 API | — | +| `R2_SECRET_ACCESS_KEY` | Секретный ключ R2 API | — | +| `R2_BUCKET` | Имя бакета R2 | — | +| `R2_PUBLIC_URL` | Публичный URL R2 (опц.) | — | +| `CF_CALLS_APP_ID` | Calls App ID (опц.) | — | +| `CF_CALLS_APP_SECRET` | Calls Secret (опц.) | — | +| `VAPID_PUBLIC_KEY` | Публичный ключ VAPID (опц.) | — | +| `VAPID_PRIVATE_KEY` | Закрытый ключ VAPID (опц.) | — | +| `ONESIGNAL_APP_ID` | OneSignal App ID (опц.) | — | +| `ONESIGNAL_REST_KEY` | OneSignal REST Key (опц.) | — | --- -## Security Model - -``` -On Registration: - Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys) - Public keys are uploaded; private keys stay on-device (four-tier persistence) - -On Each Message: - Sender fetches recipient's IK public key - Generates a fresh ephemeral ECDH keypair (per-message) - X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt - Ephemeral public key sent in message header; destroyed after use - -What the Server Sees: - ✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps) - ❌ Plaintext / private keys / ephemeral keys / call content -``` - ---- - -## Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `PORT` | Server port | `3000` | -| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret | -| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — | -| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — | -| `R2_ACCOUNT_ID` | Cloudflare account ID | — | -| `R2_ACCESS_KEY_ID` | R2 API token access key | — | -| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — | -| `R2_BUCKET` | R2 bucket name | — | -| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — | -| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — | -| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — | -| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — | -| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — | -| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` | -| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — | -| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — | - ---- - -## License +## Лицензия MIT © PaperPhone Contributors diff --git a/client/nginx.conf b/client/nginx.conf index 0da0e4b..6b9cdba 100644 --- a/client/nginx.conf +++ b/client/nginx.conf @@ -1,4 +1,13 @@ -# Nginx config for PaperPhone — serves client and proxies API to server +# ───────────────────────────────────────────────────────────────── +# Nginx config for PaperPhone — serves PWA + reverse-proxy to API +# +# This file is an envsubst template. At container start the +# Dockerfile CMD substitutes ${SERVER_URL} and writes the final +# config to /etc/nginx/conf.d/paperphone.conf. +# +# Docker Compose: SERVER_URL = http://server:3000 +# Zeabur: SERVER_URL = ${ZEABUR_WEB_URL} (e.g. http://10.43.x.x:3000) +# ───────────────────────────────────────────────────────────────── server { listen 80; @@ -7,39 +16,42 @@ server { root /usr/share/nginx/html; index index.html; - # Gzip compression + # ── Compression ────────────────────────────────────────────── gzip on; gzip_types text/plain text/css application/javascript application/json application/wasm; gzip_min_length 1024; + gzip_vary on; - # Security headers + # ── Security headers ───────────────────────────────────────── add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "no-referrer" always; - # Cache PWA assets aggressively (content-hashed in prod) + # ── Static asset caching ───────────────────────────────────── location ~* \.(css|js|wasm|png|jpg|webp|ico|svg|woff2?)$ { expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; } - # Service worker — no-cache so updates propagate immediately + # Service worker — always revalidate so updates propagate location = /sw.js { add_header Cache-Control "no-cache, no-store, must-revalidate"; expires 0; } - # WebSocket upgrade — proxy to backend + # ── WebSocket — proxy to backend ───────────────────────────── location /ws { proxy_pass ${SERVER_URL}; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 3600s; + proxy_send_timeout 3600s; } - # REST API — proxy to backend + # ── REST API — proxy to backend ────────────────────────────── location /api/ { proxy_pass ${SERVER_URL}; proxy_http_version 1.1; @@ -48,14 +60,18 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 51m; + + # Long-poll & upload timeout + proxy_read_timeout 120s; + proxy_send_timeout 120s; } - # Health check from backend + # ── Health check (forwarded to backend) ────────────────────── location /health { proxy_pass ${SERVER_URL}; } - # SPA fallback — all other routes serve index.html + # ── SPA fallback ───────────────────────────────────────────── location / { try_files $uri $uri/ /index.html; } diff --git a/docker-compose.yml b/docker-compose.yml index dab965d..e164759 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - # ───────────────────────────────────────────────────────────────── # PaperPhone — Full-Stack Docker Compose # @@ -8,14 +6,17 @@ version: "3.9" # server — Node.js API + WebSocket # mysql — Message store / user DB # redis — Online presence & session cache -# minio — S3-compatible object storage for media uploads +# +# File storage: +# Cloudflare R2 (configured via R2_* env vars in server/.env) +# No MinIO / S3 container needed. # # Images: # docker pull facilisvelox/paperphone-client:latest # docker pull facilisvelox/paperphone-server:latest # # Quick start: -# cp server/.env.example server/.env # edit secrets & CF_CALLS_APP_ID +# cp server/.env.example server/.env # edit secrets & R2 keys # docker compose up -d # ───────────────────────────────────────────────────────────────── @@ -26,6 +27,8 @@ services: image: facilisvelox/paperphone-client:latest ports: - "80:80" + environment: + SERVER_URL: http://server:3000 depends_on: server: condition: service_healthy @@ -40,17 +43,16 @@ services: environment: DB_HOST: mysql DB_PORT: 3306 + DB_USER: ${DB_USER:-paperphone} + DB_PASS: ${DB_PASS:-changeme} + DB_NAME: paperphone REDIS_HOST: redis REDIS_PORT: 6379 - MINIO_ENDPOINT: minio - MINIO_PORT: 9000 depends_on: mysql: condition: service_healthy redis: condition: service_healthy - minio: - condition: service_started healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] interval: 30s @@ -97,30 +99,9 @@ services: networks: - paperphone - # ── MinIO (S3-compatible) ───────────────────────────────────── - minio: - image: minio/minio:latest - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin} - volumes: - - minio_data:/data - ports: - - "9001:9001" # MinIO console (remove in prod if not needed) - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - paperphone - volumes: mysql_data: redis_data: - minio_data: networks: paperphone: diff --git a/server/.env b/server/.env index 8b932b7..1c96307 100644 --- a/server/.env +++ b/server/.env @@ -10,12 +10,13 @@ DB_NAME=paperphone REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASS= -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=paperphone + +# Cloudflare R2 (S3-compatible object storage) +R2_ACCOUNT_ID= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +R2_BUCKET=paperphone +R2_PUBLIC_URL= # Cloudflare Calls (TURN service) # Get from: Cloudflare Dashboard → Calls → Your App → API Keys diff --git a/server/.env.docker b/server/.env.docker index 771788f..917f4f5 100644 --- a/server/.env.docker +++ b/server/.env.docker @@ -1,5 +1,5 @@ # ───────────────────────────────────────────────────────────────── -# PaperPhone — Production Environment Variables +# PaperPhone — Production Environment Variables (Docker Compose) # Copy this file to server/.env and fill in real values # ───────────────────────────────────────────────────────────────── @@ -13,7 +13,7 @@ JWT_SECRET=CHANGE_ME_USE_OPENSSL_RAND_HEX_32 JWT_EXPIRES_IN=7d # ── MySQL ───────────────────────────────────────────────────────── -# In Docker, DB_HOST is the service name: mysql +# In Docker Compose, DB_HOST is the service name: mysql DB_HOST=mysql DB_PORT=3306 DB_USER=paperphone @@ -21,17 +21,30 @@ DB_PASS=CHANGE_ME_DB_PASSWORD DB_NAME=paperphone # ── Redis ───────────────────────────────────────────────────────── -# In Docker, REDIS_HOST is the service name: redis +# In Docker Compose, REDIS_HOST is the service name: redis REDIS_HOST=redis REDIS_PORT=6379 -# Optional password (matches redis command --requirepass) REDIS_PASS= -# ── MinIO ───────────────────────────────────────────────────────── -# In Docker, MINIO_ENDPOINT is the service name: minio -MINIO_ENDPOINT=minio -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=CHANGE_ME_MINIO_ACCESS -MINIO_SECRET_KEY=CHANGE_ME_MINIO_SECRET -MINIO_BUCKET=paperphone +# ── Cloudflare R2 (S3-compatible object storage) ───────────────── +# Get from: Cloudflare Dashboard → R2 → Manage API Tokens +R2_ACCOUNT_ID= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +R2_BUCKET=paperphone +R2_PUBLIC_URL= + +# ── Cloudflare TURN (optional — for cross-network WebRTC calls) ── +# Get from: Cloudflare Dashboard → Workers & Pages → Calls +CF_CALLS_APP_ID= +CF_CALLS_APP_SECRET= + +# ── Web Push / VAPID (optional) ────────────────────────────────── +# Generate with: npx web-push generate-vapid-keys +VAPID_PUBLIC_KEY= +VAPID_PRIVATE_KEY= +VAPID_SUBJECT=mailto:admin@paperphone.app + +# ── OneSignal (optional — Median.co native push) ───────────────── +ONESIGNAL_APP_ID= +ONESIGNAL_REST_KEY= diff --git a/server/.env.example b/server/.env.example index 2abb183..5409191 100644 --- a/server/.env.example +++ b/server/.env.example @@ -18,21 +18,26 @@ REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASS= -# MinIO -MINIO_ENDPOINT=localhost -MINIO_PORT=9000 -MINIO_USE_SSL=false -MINIO_ACCESS_KEY=minioadmin -MINIO_SECRET_KEY=minioadmin -MINIO_BUCKET=paperphone +# Cloudflare R2 (S3-compatible object storage) +# Get from Cloudflare Dashboard → R2 → Manage API Tokens +R2_ACCOUNT_ID= +R2_ACCESS_KEY_ID= +R2_SECRET_ACCESS_KEY= +R2_BUCKET=paperphone +R2_PUBLIC_URL= -# Web Push (VAPID) +# Cloudflare TURN (optional — for cross-network WebRTC calls) +# Get from Cloudflare Dashboard → Workers & Pages → Calls +CF_CALLS_APP_ID= +CF_CALLS_APP_SECRET= + +# Web Push (VAPID) — optional # Generate with: npx web-push generate-vapid-keys VAPID_PUBLIC_KEY= VAPID_PRIVATE_KEY= VAPID_SUBJECT=mailto:admin@paperphone.app -# OneSignal (Median.co native push) +# OneSignal (Median.co native push) — optional # Get from OneSignal Dashboard → Settings → Keys & IDs ONESIGNAL_APP_ID= ONESIGNAL_REST_KEY= diff --git a/zeabur.yaml b/zeabur.yaml index f348a9a..9499418 100644 --- a/zeabur.yaml +++ b/zeabur.yaml @@ -25,10 +25,11 @@ spec: ## Features - 📹 **Video & Voice Calls** — WebRTC P2P (1:1) + Mesh (group up to 6) - - 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French + - 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French, German, Russian, Spanish - 📱 **iOS PWA** — "Add to Home Screen" via Safari, no enterprise cert needed - 💬 Rich messaging: text, images, voice, emoji, delivery receipts, typing indicators - 🗂️ **Cloudflare R2** file storage (images & voice messages) + - 🏷️ **Friend Tags** — tag-based contact filtering & Moments visibility control ## After Deployment 1. Open the domain assigned to the **client** service. @@ -386,10 +387,11 @@ localization: ## 功能亮点 - 📹 **视频/语音通话** — WebRTC P2P(1:1)+ Mesh 多人(≤6 人) - - 🌐 **多语言** — 中文、英文、日语、韩语、法语 + - 🌐 **多语言** — 中文、英文、日语、韩语、法语、德语、俄语、西班牙语 - 📱 **iOS 永久免签** — Safari「添加到主屏幕」,无需企业证书 - 💬 富文本消息、图片、语音、Emoji、送达回执、打字状态 - 🗂️ **Cloudflare R2** 对象存储(图片与语音消息) + - 🏷️ **好友标签** — 标签分类筛选通讯录 + 朋友圈可见性控制 ## 部署后操作 1. 打开分配给 **client** 服务的域名。