██╗ ██╗ █████╗ ██╗ ██████╗ ███╗ ██╗██╗ ██╗██╗ ██╗ ██║ ██║██╔══██╗██║ ██╔═══██╗████╗ ██║╚██╗ ██╔╝╚██╗██╔╝ ███████║███████║██║ ██║ ██║██╔██╗ ██║ ╚████╔╝ ╚███╔╝ ██╔══██║██╔══██║██║ ██║ ██║██║╚██╗██║ ╚██╔╝ ██╔██╗ ██║ ██║██║ ██║███████╗╚██████╔╝██║ ╚████║ ██║ ██╔╝ ██╗ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝
Self-hostable E2EE Messenger · Signal Protocol (X3DH + Double Ratchet), implemented from scratch
Live Demo · Simulator · Security Audit · Threat Model · Benchmarks · Policy Brief
Halonyx is a self-hostable, end-to-end encrypted messenger built on the Signal Protocol — implemented from scratch rather than wrapped around libsignal. Every message is encrypted client-side with X3DH + Double Ratchet before it ever reaches the server; the relay only ever sees ciphertext. Files move directly peer-to-peer over WebTorrent (BitTorrent over WebRTC ), so the server is never in the data path for transfers either. Safety Numbers close the one gap E2EE alone can't: a compromised or malicious server substituting keys during setup.
It's a final-year project built as a deep, from-first-principles exploration of applied cryptography and is not a production messenger. The threat model and compliance brief below are written with that honesty in mind.
-
USID Identity — 256-bit pseudonymous identifier; no username or phone number required
-
End-to-End Encryption — full Signal Protocol: X3DH key exchange + Double Ratchet on every message
-
Forward Secrecy — per-message ephemeral keys; past messages stay safe even if current keys are compromised
-
Post-Compromise Security — DH ratchet step on every reply; session heals automatically after a breach
-
Safety Numbers — 60-digit fingerprint of both parties' identity keys; detects MITM key substitution out-of-band
-
Key Change Detection — automatic warning when a contact's identity key changes between sessions
-
P2P File Transfer — files shared via WebTorrent (BitTorrent over WebRTC); server is never in the data path
-
Live Transfer Stats — real-time upload/download speed, progress bar, and seeding ratio per torrent
-
Offline Mailbox — messages to offline peers are queued server-side and flushed on reconnect; at-most-once delivery
-
Real-Time Delivery — WebSocket messaging with queued-message status indicator (clock icon on undelivered messages)
-
Dual Database Isolation — identity metadata and operational data in separate SQLite databases, linked only by
SHA-256(USID) -
Emergency Broadcast — UDP-bridged system-wide alert reachable from any connected client
-
Web Audio Notifications — send and receive sounds synthesized via Web Audio API; no audio files required
-
Dark / Light Theme — fully adaptive UI with smooth transitions
-
Contact Management — add by USID, remove, search, duplicate auto-cleanup
-
Rate Limiting — per-IP rate limits on signup and key upload endpoints (
express-rate-limitv8) -
Interactive Simulator — a browser-based "How Halonyx Works" explainer that walks through X3DH and the Double Ratchet step by step
Prerequisites: Node.js 20, 22, or 24 (matches the CI matrix)
git clone https://github.com/ABHIRAM-CREATOR06/Halonyx.git
cd Halonyx
npm install
npm start # production
npm run dev # development (auto-reload via nodemon )Open http://localhost:3000. On Windows, run scripts/setup.bat (or scripts/start_server.bat / scripts/start_server.sh on Unix/macOS ).
You can run Halonyx in a container using Docker or Docker Compose:
Using Docker Compose (Recommended):
docker compose up -d --buildUsing Docker CLI:
docker build -t halonyx .
docker run -d -p 3000:3000 -v halonyx-data:/app/backend/db --name halonyx-app halonyxOpen http://localhost:3000 once the container is running. Database state is persisted in the Docker volume.
npm test # full suite (node --test )
npm run test:x25519 # X3DH + Double Ratchet crypto core onlyCI runs the full suite plus the X25519 protocol tests on Node 20, 22, and 24 on every push and PR, and fails the build outright if the X25519 tests get skipped instead of actually passing — a stale WebCrypto shim silently reporting green isn't allowed to pass. A native-binding sanity check for sqlite3 runs first, and npm audit runs non-blocking on top.
Open simulator/index.html directly in a browser for a standalone, dependency-free walkthrough of the protocol — no server required. It's the fastest way to understand X3DH and the Double Ratchet without reading the source.
Two isolated SQLite databases prevent cross-correlation of identity and operational data. Only SHA-256(USID) links them — plaintext identity is never stored anywhere.
Client (Browser)
└── HTTPS / WSS
└── Express REST API :3000
WebSocket Server :3000
UDP Broadcast :9000
│
┌───────┴──────────────┐
identity.db app.db keys.db
(name · email · (users · (public key
hashed_usid) contacts · bundles)
mailbox)
File Transfers
└── WebTorrent (BitTorrent over WebRTC)
└── Direct peer-to-peer — server not involved
Each user uploads a public key bundle on registration. The bundle is stored in keys.db and served via authenticated REST endpoints. It is used for:
-
X3DH session initialisation — recipient's pre-key bundle fetched before first message
-
Safety Number computation — identity public key (P-256) fetched to derive the 60-digit verification fingerprint
Registration:
Client generates P-256 identity key pair
└── POST /keys/upload → stores full X3DH bundle in keys.db
└── POST /update-pubkey → stores identity public key in app.db (for safety numbers)
Opening a chat:
Client fetches peer bundle
└── GET /keys/:hashedUsid → returns pre-key bundle for X3DH
Verifying identity:
Client fetches peer identity key
└── GET /public-key/:hashedUsid → returns identity public key hex for safety number computation
When a recipient is offline the server stores the encrypted message payload in a mailbox table. On their next WebSocket reconnect, all queued messages are flushed and immediately deleted — ensuring at-most-once delivery with no permanent server retention.
Sender → Server (recipient offline)
└── INSERT INTO mailbox (encrypted payload) ← stored, never dropped
└── { type: "queued" } ← sender sees clock icon
Recipient reconnects → Server
└── SELECT * FROM mailbox WHERE recipient = ?
└── forward each message via WebSocket
└── DELETE FROM mailbox WHERE recipient = ?
Files are never uploaded to the Halonyx server. Instead:
-
Sender seeds the file using WebTorrent — BitTorrent running entirely in the browser via WebRTC
-
A magnet URI is sent to the recipient through the encrypted message channel
-
Recipient's browser leeches directly from the sender over WebRTC data channels
-
Public trackers (
openwebtorrent.com,webtorrent.dev) handle peer discovery only — they never see file contents -
NAT traversal is supported via STUN and TURN servers (e.g.,
openrelay.metered.ca), so P2P connections succeed even behind strict firewalls or symmetric NATs -
Live upload speed, download speed, progress percentage, and seeding ratio are displayed in real time
Sender Browser Recipient Browser
└── WebTorrent.seed(file) → magnet URI (via encrypted WS)
└── WebRTC DataChannel ──────────────────────────────────→ WebTorrent.download()
(direct P2P, server not involved)
The full implementation — X3DH, the Double Ratchet, key management, session state, and crypto primitives — lives under protocol/, with a dedicated security analysis covering the threat model, per-primitive security properties, and known limitations of the ratchet.
Four Diffie-Hellman operations establish a shared secret with a party you have never contacted before:
DH1 = DH(IKa, SPKb) — Alice's identity × Bob's signed pre-key
DH2 = DH(EKa, IKb) — Alice's ephemeral × Bob's identity
DH3 = DH(EKa, SPKb) — Alice's ephemeral × Bob's signed pre-key
DH4 = DH(EKa, OPKb) — Alice's ephemeral × Bob's one-time pre-key
SK = HKDF(DH1 ‖ DH2 ‖ DH3 ‖ DH4)
The server relays an opaque x3dh_init packet to the recipient, who runs the responder path and derives the same SK independently.
After X3DH establishes the root key, every message advances the Double Ratchet:
-
Symmetric ratchet — each message derives a unique key from the current chain key; keys are used once and discarded
-
DH ratchet — every reply triggers a new DH exchange, deriving fresh root and chain keys
-
Compromising message N reveals nothing about messages 1…N-1 (forward secrecy) or N+1…∞ (post-compromise security)
Identity keys, signed pre-keys, one-time pre-keys, and Double Ratchet session state all persist across page reloads via IndexedDB:
-
Private keys stored as non-exportable
CryptoKeyobjects — never serialised to raw bytes -
Session state (root key, chain keys, ratchet DH keys) restored on reconnect
-
Each USID maps 1:1 to a stable cryptographic identity across sessions
Safety Numbers close the MITM gap. Even with perfect E2E encryption, a malicious server could substitute public keys during X3DH — reading all messages without either party knowing.
Each user generates a P-256 ECDH identity key pair at registration. The public key is uploaded to the server. To verify a session:
-
Alice fetches Bob's identity public key from
GET /public-key/:hashedUsid -
Both parties independently compute:
safetyNumber = SHA-256(
sort_lex([SHA256(aliceUsid) + alicePubKey,
SHA256(bobUsid) + bobPubKey])
)
→ formatted as 12 groups of 5 digits across 4 rows (60 digits total)
-
Alice and Bob compare the number over a voice call or in person
-
If they match → no MITM, session is cryptographically verified
-
If they differ → a key was substituted → attack detected
The last-seen safety number is stored in localStorage. On every subsequent verification:
-
Same number → keys unchanged, session is clean
-
Different number → contact may have re-registered, or a MITM substituted a key → prominent warning shown before proceeding
Without Safety Numbers (vulnerable):
Alice Server (malicious) Bob
│── GET /public-key ──→│ │
│←─ Mallory's key ─────│ ← server substitutes │
│ │ │
│ encrypts to Mallory │ │
│── ciphertext ────────→│── re-encrypts to Bob ────────→│
│ server reads everything │
With Safety Numbers (protected):
Alice sees: 12345 67890 11111
Bob sees: 72891 23456 78901 ← mismatch → attack caught ✅
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/signup |
POST | — | Register — returns usid + JWT (rate limited) |
/add-contact |
POST | ✓ | Add a contact by USID |
/contacts |
GET | ✓ | Fetch contact list (hashed USIDs) |
/contacts |
DELETE | ✓ | Remove a contact by hashed USID |
/cleanup-duplicates |
POST | ✓ | Remove duplicate contacts for current user |
/cleanup-all-duplicates |
POST | ✓ | Remove duplicate contacts for all users |
/keys/upload |
POST | ✓ | Upload full X3DH public key bundle (rate limited) |
/keys/replenish |
POST | ✓ | Append one-time pre-keys to existing bundle (rate limited) |
/keys/:hashedUsid |
GET | — | Fetch peer's X3DH key bundle |
/public-key/:hashedUsid |
GET | ✓ | Fetch peer's identity public key (for safety numbers) |
/update-pubkey |
POST | ✓ | Push identity public key without re-registering |
All authenticated routes require Authorization: Bearer <token>. Rate-limited endpoints use express-rate-limit v8 with draft-7 standard headers.
| Type | Direction | Description |
|---|---|---|
register |
Client → Server | Authenticate WS session with USID |
registered |
Server → Client | Identity confirmed; offline mailbox flushed |
message |
Bidirectional | Encrypted message payload; unencrypted payloads are rejected server-side |
x3dh_init |
Bidirectional | Relay X3DH handshake packet to recipient |
queued |
Server → Client | Recipient offline — message stored in mailbox |
emergency_broadcast |
Client → Server | UDP-bridged system-wide alert |
error |
Server → Client | Auth or routing failure |
| Primitive | Algorithm | Key Size |
|---|---|---|
| Symmetric Encryption | AES-256-GCM | 256 bits |
| Key Derivation | HKDF-SHA256 | 256 bits |
| Hashing | SHA-256 | 256 bits |
| Asymmetric Key Exchange | X25519 (ECDH) | 256 bits |
| Identity / Safety Numbers | P-256 (ECDH) | 256 bits |
| Message Authentication | HMAC-SHA256 | 256 bits |
| Pre-Key Signing | Ed25519 | 256 bits |
Guarantees: forward secrecy · post-compromise security · HMAC authentication · deniability · pseudonymity · MITM detection via safety numbers
JWT signing keys are generated with crypto.randomBytes(32) at boot rather than a fixed default, and tokens carry only a hashed identifier — never the raw USID.
Halonyx ships with a full doc set under specification_docs/, covering security, performance, and legal posture:
-
Security Audit Report — comprehensive full-stack security review conducted using the Cloudflare Security Suite, evaluating Signal Protocol cryptography, HTTP authentication, WebSocket relays, database isolation, and container deployment safety.
-
Data Threat Model — a STRIDE-style analysis covering 18 classified threats (T-01 through T-18) across the frontend, backend, transport, storage, and dependency supply chain — including a hardcoded JWT secret, unauthenticated WebSocket registration, WebRTC/WebTorrent IP leaks, and OPK exhaustion — each with severity ratings and a phased remediation roadmap.
-
Performance Benchmarks — latency and throughput for every layer of the stack: X3DH and Double Ratchet crypto operations, REST endpoints, WebSocket messaging (including offline mailbox store-and-flush), UDP emergency broadcast, SQLite read/write performance, and WebTorrent transfer over STUN/TURN.
-
Encryption Policy Brief — a comparative look at where Halonyx's architecture stands against live encryption policy in the EU (CSAR/"Chat Control"), the US (EARN IT Act), India (IT Rules 2021 traceability), the UK (Online Safety Act / Investigatory Powers Act), and the UN Convention against Cybercrime.
-
Protocol Implementation Notes and Protocol Security Analysis — a component-by-component breakdown of the Signal Protocol implementation (X3DH, Double Ratchet, key management, session handling) and its security properties, assumptions, and limitations.
The tests/ directory covers the crypto core and the surrounding server logic using Node's built-in test runner:
tests/
├── x25519_protocol.test.js # X3DH primitives over Web Crypto's X25519
├── x3dh.test.js # Full X3DH initiator/responder handshake
├── double_ratchet.test.js # Symmetric + DH ratchet correctness
├── crypto_utils.test.js # AES-256-GCM, HKDF, HMAC primitives
├── safety_number.test.js # Safety number derivation & formatting
├── auth_connect.test.js # Signup, JWT auth, WS registration flow
├── relay_metadata_static.test.js # Server never persists plaintext relay metadata
├── simulator_static.test.js # Simulator loads and runs standalone
├── email_utils.test.js # Email utility functions
├── utils.test.js # USID generation & hashing
└── verify-sqlite.js # Native sqlite3 binding sanity check
Run everything with npm test, or just the crypto core with npm run test:x25519. Both run automatically in CI on Node 20, 22, and 24.
Halonyx/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Node 20/22/24 matrix — tests, X25519 core, sqlite check, audit
│ │ ├── pages.yml # GitHub Pages deployment (frontend)
│ │ └── static.yml # Static deployment workflow
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── PULL_REQUEST_TEMPLATE.md
├── backend/
│ ├── server.js # Express + WebSocket + UDP + offline mailbox
│ │ # + /keys/upload, /public-key, /update-pubkey
│ ├── email.js # Email notification utilities
│ ├── utils.js # USID generation & hashing
│ ├── test_udp.js # UDP broadcast test script
│ └── db/
│ ├── app.db # users · contacts · mailbox
│ ├── identity.db # hashed_usid ↔ email/name metadata
│ ├── keys.db # X3DH public key bundles
│ ├── schema.sql # app.db schema (users · contacts)
│ ├── identity_schema.sql # identity.db schema
│ └── key_schema.sql # keys.db schema
├── frontend/
│ ├── index.html # Three-pane layout + Safety Numbers dialog
│ ├── css/style.css # Dark/light adaptive UI, Signal-style bubbles
│ └── js/app.js # WebTorrent · WS · E2EE wiring · Safety Numbers
├── protocol/
│ ├── README.md # Implementation overview
│ ├── SECURITY_ANALYSIS.md # Threat model & per-primitive security properties
│ ├── signal_protocol.js # Top-level façade: init, openSession, encrypt, decrypt
│ ├── x3dh.js # X3DH initiator + responder paths
│ ├── double_ratchet.js # Double Ratchet with HKDF chain KDF
│ ├── key_management.js # Key pair generation, pre-key bundles
│ ├── idb_key_store.js # IndexedDB persistence for keys and session state
│ ├── session.js # Session lifecycle management
│ ├── email_utils.js # Email utility functions
│ └── crypto_utils.js # AES-256-GCM, HKDF, HMAC, X25519 primitives
├── simulator/
│ ├── index.html # Standalone "How Halonyx Works" interactive explainer
│ └── readme.md
├── scripts/
│ ├── setup.bat # Interactive Windows setup & launch wizard
│ ├── start_server.bat # Windows server launcher
│ └── start_server.sh # Unix/macOS server launcher
├── specification_docs/
│ ├── security_docs/datathreat.md # STRIDE threat model — 18 classified threats
│ ├── benchmark/benchmark.md # Full-stack performance benchmarks
│ └── compliance_doc/encryption-policy-brief.md # EU/US/India/UK/UN encryption policy brief
├── tests/ # Node --test suite — crypto core + server logic
│ ├── x25519_protocol.test.js # X3DH primitives over Web Crypto's X25519
│ ├── x3dh.test.js # Full X3DH initiator/responder handshake
│ ├── double_ratchet.test.js # Symmetric + DH ratchet correctness
│ ├── crypto_utils.test.js # AES-256-GCM, HKDF, HMAC primitives
│ ├── safety_number.test.js # Safety number derivation & formatting
│ ├── auth_connect.test.js # Signup, JWT auth, WS registration flow
│ ├── relay_metadata_static.test.js # Server never persists plaintext relay metadata
│ ├── simulator_static.test.js # Simulator loads and runs standalone
│ ├── email_utils.test.js # Email utility functions
│ ├── utils.test.js # USID generation & hashing
│ └── verify-sqlite.js # Native sqlite3 binding sanity check
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── SECURITY.md
├── LICENSE # AGPL-3.0
├── README.md
├── .dockerignore # Excluded paths for Docker build context
├── .env.example # Environment configuration template
├── Dockerfile # Production Docker container image definition
├── docker-compose.yml # Docker Compose multi-container orchestration
└── package.json
-
End-to-end encrypted messaging (Signal Protocol — X3DH + Double Ratchet)
-
P2P file transfer (WebTorrent / BitTorrent over WebRTC)
-
Offline message mailbox with at-most-once delivery
-
Key bundle endpoints (upload, fetch, update)
-
IndexedDB key persistence across page reloads
-
Safety Numbers — 60-digit MITM detection fingerprint
-
Key change detection with session warning
-
Live torrent stats (speed, progress, ratio)
-
Web Audio notification sounds
-
Dark / light theme
-
Contact remove + duplicate cleanup
-
OPK replenishment monitoring
-
Interactive protocol simulator
-
CI test matrix (Node 20/22/24) with crypto-core enforcement
-
Containerized deployment with Docker and Docker Compose
-
Interactive one-click setup script (
scripts/setup.bat) for Windows -
Full STRIDE threat model (18 threats) + performance benchmark suite
-
Cross-jurisdiction encryption policy brief
-
Safety number QR code scan
-
Post-quantum cryptography (CRYSTALS-Dilithium / SPHINCS+)
-
Multi-device session sync
-
Group messaging via Sender Keys
-
Voice & video (WebRTC)
-
Push notifications (Web Push / VAPID)
Built at SNGCE, Kerala · APJ Abdul Kalam Technological University · 2026
| Name | Role |
|---|---|
| Abhiram P | Backend · Signal Protocol · Safety Numbers |
| Geo Jose | Frontend · UI/UX · Theme System |
| Anirudh | Frontend · Testing · WebTorrent Integration |
| Antony S Kannampuzha | Database · Infrastructure · Key Storage |
AGPL-3.0. See LICENSE.
Built as a deep exploration of applied cryptography and secure communication. Not intended for production deployment.