Skip to content

Encrypted P2P Chat

Carter Perez edited this page Dec 9, 2025 · 1 revision

Encrypted P2P Chat

Real-time encrypted messaging application using Signal Protocol encryption with passwordless WebAuthn authentication.

Overview

A secure chat application implementing end-to-end encryption with forward secrecy and break-in recovery. Features passwordless authentication via WebAuthn/Passkeys and real-time messaging through WebSockets and SurrealDB live queries.

Status: Complete | Difficulty: Advanced

Tech Stack

Backend

Technology Version Purpose
FastAPI 0.121+ Async Python web framework
PostgreSQL 16 User auth & credentials
SurrealDB 1.0 Real-time messaging (live queries)
Redis 7.1 Challenge/session storage
SQLModel 0.0.27 SQLAlchemy + Pydantic hybrid
Alembic 1.17+ Async migrations
PyNaCl 1.6 Encryption primitives
liboqs-python 0.14 Post-quantum cryptography

Frontend

Technology Version Purpose
SolidJS 1.9 Fine-grained reactivity
TypeScript 5.9 Type safety
Vite 7.2 Build tool (Node 20.19+)
Tailwind CSS v4 Styling
@tanstack/solid-query 5.90 Server state
nanostores 1.1 Lightweight state

Infrastructure

  • Docker + Docker Compose
  • Nginx reverse proxy
  • Makefile automation

Features

Authentication

  • Passwordless login with WebAuthn/Passkeys (FIDO2)
  • Discoverable credentials (device-based)
  • Multi-device support
  • Signature counter verification

Encryption (Signal Protocol)

  • Double Ratchet for message encryption
  • X3DH key exchange for async messaging
  • Forward secrecy
  • Break-in recovery
  • Out-of-order message handling
  • Per-message AES-256-GCM

Real-Time Messaging

  • WebSocket connections with management
  • SurrealDB live queries
  • Online/offline presence
  • Typing indicators
  • Read receipts
  • Heartbeat keep-alive

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Frontend (SolidJS)                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  Auth   β”‚  β”‚   Chat   β”‚  β”‚  Rooms  β”‚  β”‚  Crypto   β”‚ β”‚
β”‚  β”‚ WebAuthnβ”‚  β”‚ Messages β”‚  β”‚  List   β”‚  β”‚  Service  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚            β”‚             β”‚             β”‚
        β–Ό            β–Ό             β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              WebSocket + REST API (Nginx)                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   Backend (FastAPI)                      β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    Services                       β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β”‚  β”‚   Auth   β”‚  β”‚ Message  β”‚  β”‚   Presence   β”‚   β”‚   β”‚
β”‚  β”‚  β”‚ WebAuthn β”‚  β”‚ Encrypt  β”‚  β”‚   Tracking   β”‚   β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚   β”‚
β”‚  β”‚  β”‚  Prekey  β”‚  β”‚WebSocket β”‚  β”‚   Ratchet    β”‚   β”‚   β”‚
β”‚  β”‚  β”‚  X3DH    β”‚  β”‚ Manager  β”‚  β”‚   State      β”‚   β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                  Encryption Core                  β”‚   β”‚
β”‚  β”‚  double_ratchet.py  |  x3dh_manager.py           β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                  β”‚                  β”‚
         β–Ό                  β–Ό                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ PostgreSQL  β”‚    β”‚  SurrealDB  β”‚    β”‚    Redis    β”‚
β”‚ Users/Creds β”‚    β”‚  Messages   β”‚    β”‚  Sessions   β”‚
β”‚ Keys/Prekeysβ”‚    β”‚ Live Queriesβ”‚    β”‚ Challenges  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Exchange Flow (X3DH)

Alice                                              Bob
  β”‚                                                  β”‚
  β”‚  1. Fetch Bob's prekey bundle                   β”‚
  β”‚  ──────────────────────────────────────────────>β”‚
  β”‚                                                  β”‚
  β”‚  2. Bundle: IdentityKey, SignedPrekey,          β”‚
  β”‚     OneTimePrekey, Signature                    β”‚
  β”‚  <──────────────────────────────────────────────│
  β”‚                                                  β”‚
  β”‚  3. Verify signature                            β”‚
  β”‚  4. Generate ephemeral keypair                  β”‚
  β”‚  5. Compute shared secrets (DH):                β”‚
  β”‚     DH1 = DH(IK_A, SPK_B)                       β”‚
  β”‚     DH2 = DH(EK_A, IK_B)                        β”‚
  β”‚     DH3 = DH(EK_A, SPK_B)                       β”‚
  β”‚     DH4 = DH(EK_A, OPK_B)                       β”‚
  β”‚  6. Derive root key: HKDF(DH1||DH2||DH3||DH4)  β”‚
  β”‚  7. Initialize Double Ratchet                   β”‚
  β”‚                                                  β”‚
  β”‚  8. Send initial message + EK_A public         β”‚
  β”‚  ──────────────────────────────────────────────>β”‚
  β”‚                                                  β”‚
  β”‚                    9. Bob computes same secrets β”‚
  β”‚                   10. Initializes Double Ratchetβ”‚
  β”‚                                                  β”‚

Double Ratchet

Each message uses a unique key derived from the ratchet state:

Root Key ─────┬─────> Chain Key 1 ──> Message Key 1
              β”‚                   ──> Message Key 2
              β”‚
              └─────> Chain Key 2 ──> Message Key 3
                                  ──> Message Key 4
  • Symmetric ratchet: Derives new message keys
  • DH ratchet: Updates root key on each reply
  • Forward secrecy: Old keys deleted after use
  • Break-in recovery: New DH ratchet heals compromise

Quick Start

cd PROJECTS/encrypted-p2p-chat

# Copy environment file
cp .env.example .env

# Start development environment
make dev

# Access at http://localhost:3000

Configuration

Key environment variables:

# PostgreSQL
DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/chat

# SurrealDB
SURREAL_URL=ws://localhost:8000/rpc
SURREAL_NS=chat
SURREAL_DB=messages

# Redis
REDIS_URL=redis://localhost:6379

# WebAuthn
RP_ID=localhost
RP_NAME=Encrypted Chat
RP_ORIGIN=http://localhost:3000

Database Models

Model Database Purpose
User PostgreSQL User accounts
Credential PostgreSQL WebAuthn credentials
IdentityKey PostgreSQL Long-term identity keys
SignedPrekey PostgreSQL Signed prekeys for X3DH
OneTimePrekey PostgreSQL One-time prekeys
RatchetState PostgreSQL Double ratchet state
SkippedMessageKey PostgreSQL Out-of-order handling
Messages SurrealDB Encrypted message storage

Frontend Stores

Store Purpose
authStore Authentication state
messagesStore Message state
roomsStore Chat room state
presenceStore Online/offline status
typingStore Typing indicators
settingsStore User preferences

Development

# Backend
make backend-lint
make backend-test

# Frontend
make frontend-lint
make frontend-build

# Both
make lint
make test

Security Considerations

  • All messages encrypted client-side before transmission
  • Server never sees plaintext messages
  • Keys stored securely, old keys deleted
  • WebAuthn prevents phishing attacks
  • No passwords to steal or crack

Source Code

View on GitHub

Clone this wiki locally