A secure, end-to-end encrypted messaging application implementing the J-PAKE (Password Authenticated Key Exchange by Juggling) protocol for cryptographically secure communication without prior key distribution.
This project was developed as an academic exercise for CYBS 5041: Applied Cryptography at New Mexico Institute of Mining and Technology (New Mexico Tech) in Fall 2025. The implementation demonstrates practical application of advanced cryptographic protocols in a real-world context, serving both as a learning tool and a reference implementation for secure password-authenticated key exchange.
- Practical implementation of the J-PAKE protocol (RFC 8236)
- Understanding of zero-knowledge proofs in cryptographic systems
- Experience with end-to-end encryption in distributed systems
- Application of HKDF (SHA-256) for key separation and transcript binding
- Integration of cryptographic primitives with modern web technologies
Lockstep Chat demonstrates a practical implementation of the J-PAKE protocol (RFC 8236) in a real-world messaging application. The system ensures that messages are end-to-end encrypted, with the server acting solely as a relay without access to passwords, session keys, or message content.
- Password-Authenticated Key Exchange: Secure key establishment using only a shared password
- End-to-End Encryption: Messages encrypted with AES-256-GCM using derived session keys
- Zero-Knowledge Proofs: Schnorr proofs prevent man-in-the-middle attacks
- Perfect Forward Secrecy: Each session generates unique ephemeral keys
- HKDF-based Key Separation: Separate keys for encryption, MAC, and key confirmation
- Secure Password Handling: Passwords are immediately hashed and never stored in plaintext
- Real-time Communication: WebSocket-based messaging with Socket.IO
- Modern UI: Responsive design with light/dark theme support
- Privacy by Design: Server has zero knowledge of passwords or message content
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client A │◄────────┤ Server ├────────►│ Client B │
│ (React) │ │ (Node.js) │ │ (React) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
│ │
└───────────── E2E Encrypted Channel ──────────────┘
(Established via J-PAKE)
Frontend
- React 18.2.0
- Socket.IO Client 4.6.0
- React Icons 5.3.0
- Web Crypto API
- Modern CSS with CSS Variables
Backend
- Node.js
- Express 4.18.2
- Socket.IO 4.6.0
- Helmet 7.1 (security headers)
- CORS middleware
- Node.js (v18 or higher)
- npm (v6 or higher)
- Clone the repository:
git clone https://github.com/vermi/lockstep-chat.git
cd lockstep-chat- Run the application with a single command:
macOS/Linux:
./start.shWindows:
start.batCross-platform (Node.js):
node start.jsThe startup script will:
- Check for Node.js installation
- Install dependencies automatically if needed
- Start both server and client
- Open your browser to the application
If you prefer to run components separately:
- Install server dependencies:
cd server
npm install- Install client dependencies:
cd ../client
npm installSimply run the appropriate startup script for your platform:
./start.sh(macOS/Linux)start.bat(Windows)node start.js(Cross-platform)
- Start the server:
cd server
npm startThe server will run on http://localhost:3001
- Start the client (in a new terminal):
cd client
npm startThe client will open at http://localhost:3000
- Open two browser windows at
http://localhost:3000 - Enter the following in both windows:
- Username: Unique identifier for each participant
- Room ID: Same room identifier for both participants
- Shared Password: Same password (this is never transmitted)
- Click "Join Room" in both windows
- The J-PAKE protocol will automatically establish a secure session
- Once connected, messages are end-to-end encrypted
The implementation follows RFC 8236 specification:
- Each party generates random exponents
x1,x2 - Computes
g1 = g^x1andg2 = g^x2 - Generates Schnorr zero-knowledge proofs for
x1andx2 - Exchanges
g1,g2and proofs
- Each party computes a combined generator
- Derives value
AorBusing password-derived scalars - Generates zero-knowledge proof for the combined exponent
- Exchanges computed values and proofs
Note: Round 2 is ordered deterministically to avoid race conditions. Alice sends A first; after receiving A, Bob sends B.
- Both parties compute the shared secret
K - Derive
encKeyandmacKeyusing HKDF(SHA-256) withsalt = H(transcript)andinfo = "lockstep/v1:enc|mac"(512 bits total) - Derive a dedicated key-confirmation key
k'using HKDF(SHA-256) withinfo = "lockstep/v1:kc" - Establish AES-256-GCM using
encKey; AAD = contextBytes || messageCounter
| Parameter | Value | Notes |
|---|---|---|
| Prime Group | 2048-bit safe prime | RFC 3526 Group 14 |
| Generator | g = 2² mod p | q-subgroup generator, validated |
| Password Hashing | SHA-256 | Immediate hashing with room ID as salt |
| Password-to-Scalar | HKDF | Maps to s ∈ [1, q-1], never 0 mod q |
| Key Derivation | HKDF(SHA-256) | salt = H(transcript) |
| Encryption | AES-256-GCM | 96-bit IV (4 random + 8 counter), 128-bit tag |
| ZKP Binding | SHA-256 | Binds p, q, g, value, V, context, transcript, label |
This implementation includes several security enhancements beyond the base J-PAKE protocol:
-
Immediate Password Hashing: Passwords are hashed with SHA-256 immediately upon entry, using the room ID as salt. The plaintext password is never stored in memory or passed to the J-PAKE protocol.
-
HKDF-based Key Separation: From the shared secret K, derive encKey and macKey via HKDF(SHA-256) with salt = H(transcript) and info = "lockstep/v1:enc|mac"; derive a dedicated key-confirmation key k' with info = "lockstep/v1:kc".
-
Secure Memory Management: All sensitive data (password hashes, session keys) are explicitly cleared from memory when no longer needed, reducing the risk of memory disclosure attacks.
-
Transcript/Context Binding: ZKP challenges and HKDF salts bind the session context and a canonical protocol transcript to prevent mix-and-match or MITM attacks.
| Property | Description |
|---|---|
| Mutual Authentication | Both parties prove knowledge of the shared password |
| Forward Secrecy | Compromise of long-term secrets doesn't affect past sessions |
| Offline Dictionary Resistance | Online attacks only; offline attacks prevented |
| No PKI Required | No certificates or pre-shared keys needed |
| Memory Protection | Passwords never stored in plaintext; sensitive data cleared |
| Replay Protection | Counters and IV-reuse guards prevent message replays |
The project includes comprehensive test suites validating cryptographic correctness, protocol security, and end-to-end integration.
- Node.js ≥18 (required for native WebCrypto and ESM support)
cd client
npm install
npm test # Run all test suitesnpm run test:unit # Unit tests only (~5 seconds)
npm run test:integration # Integration tests only (~20 seconds)
npm run test:security # Security tests only (~15 seconds)
npm run test:watch # Watch mode (auto-rerun on changes)
npm run test:coverage # Generate coverage report| Suite | File | Purpose | Runtime |
|---|---|---|---|
| Unit | jpake.test.js |
Cryptographic primitives, RFC 8236 compliance | ~5 seconds |
| Integration | integration.test.js |
Full client-server protocol flows over Socket.IO | ~20 seconds |
| Security | security.test.js |
Attack resistance, input validation, authorization | ~15 seconds |
Unit Tests (jpake.test.js):
- Group math (MODP Group 14, q-subgroup validation)
- Password-to-scalar mapping (deterministic, context-bound)
- Schnorr zero-knowledge proofs (generation and verification)
- Full J-PAKE protocol flow (Round 1, Round 2, key derivation)
- Key confirmation mechanism (explicit KC₁/KC₂ exchange)
- AES-256-GCM AEAD channel (encryption, decryption, AAD binding)
- Replay protection (IV uniqueness, counter tracking)
- State machine integrity (sequential round enforcement)
- RFC 8236 §2.2 compliance (UserID validation in ZKPs)
Integration Tests (integration.test.js):
- Room management (join, leave, capacity limits)
- Participant tracking and notifications
- Protocol initiation (ready_for_protocol signaling)
- Full J-PAKE exchange over WebSocket
- Encrypted message delivery and ordering
Security Tests (security.test.js):
- Input validation (injection prevention, size limits)
- Authorization enforcement (room membership checks)
- Cryptographic attack resistance (tampered proofs, subgroup attacks)
- Context binding (cross-room/session attack prevention)
- Replay and reorder protection
- State machine enforcement
client/tests/
├── setup.mjs # WebCrypto polyfill for Node.js
├── helpers.js # Shared test utilities
├── jpake.test.js # Unit tests
├── integration.test.js # Integration tests
├── security.test.js # Security tests
└── helpers/
├── performanceCollector.js
├── statistics.js
└── reporter.js
- WebCrypto undefined: Ensure Node.js ≥18 and
tests/setup.mjsis loaded - ESM import errors: Verify
"type": "module"inpackage.json - Test timeouts: Increase timeout for network-bound tests
- Port conflicts: Integration tests use port 3002, security tests use port 3003
Performance testing was conducted using Playwright v1.56 to validate RFC 8236 compliance and measure cryptographic operation timing across multiple browser engines.
Key Findings:
- Full J-PAKE handshake completes in 711ms on Chromium (sub-second)
- All browsers successfully complete the protocol with consistent 28 modPow operations
- Firefox 2.0x slower than Chromium (expected due to BigInt performance)
- WebKit/Safari performance nearly identical to Chromium (~760ms, only 7% slower)
- RFC 8236 validated: 28 modPow calls = 14 conceptual exponentiations × 2 (proof verification)
| Browser | Wall Clock Time | modPow Ops | vs. Chromium | Status |
|---|---|---|---|---|
| Chromium | 711 ms | 28 | baseline | PASS |
| Firefox | 1,399 ms | 28 | +97% slower | PASS |
| WebKit | 759 ms | 28 | +6.8% slower | PASS |
| iOS (emulated) | 764 ms | 28 | +7.5% slower | PASS |
| Android (emulated) | 711 ms | 28 | +0% (identical) | PASS |
Round 1 Generation:
- Chromium: 69-70ms
- WebKit: 77-80ms
- Firefox: 184-192ms
Round 2 Generation:
- Chromium: 46ms
- WebKit: 51-52ms
- Firefox: 124-127ms
The RFC estimates approximately 14 modular exponentiations per party. Our measured 28 modPow operations aligns with this estimate:
| Metric | RFC 8236 Estimate | Measured | Compliance |
|---|---|---|---|
| Exponentiations per party | ~14 (conceptual) | 28 (modPow calls) | PASS |
| Protocol rounds | 2 + KC | 2 + KC | PASS |
| Group parameters | MODP 2048 | MODP 2048 | PASS |
| Proof type | Schnorr NIZK | Schnorr NIZK | PASS |
Note: The difference is measurement granularity—RFC counts abstract operations (e.g., "verify Schnorr proof" = 1), while we measure raw modPow() calls (e.g., "verify Schnorr proof" = g^r + D^c = 2).
| Phase | Approximate Size | Components |
|---|---|---|
| Round 1 | ~1.5 KB | 2 group elements + 2 Schnorr proofs |
| Round 2 | ~0.8 KB | 1 group element + 1 Schnorr proof |
| Key Confirmation | ~64 bytes | HMAC tag (256-bit) |
| Encrypted Message | Content + 64 bytes | AES-GCM: IV (12B) + tag (16B) + overhead |
Total protocol overhead: ~2.4 KB per handshake
# Start server and client first
cd server && npm start &
cd client && npm start &
# Run performance tests
cd client
npm run test:perf # All browsers
npm run test:perf:chromium # Chromium only
npm run test:perf:firefox # Firefox only
npm run test:perf:webkit # WebKit only
npm run test:perf:mobile # Mobile emulation
npm run test:perf:headed # Visual browser modeMobile tests use device emulation, not real hardware. Results may differ from actual devices due to:
- CPU performance differences (desktop vs. mobile ARM)
- Thermal throttling on real devices
- JavaScript engine variations (especially iOS Safari)
Estimated Real Device Performance:
- iOS Safari: 1.2-1.8 seconds
- Android Chrome: 0.9-1.2 seconds
| Event | Payload | Description |
|---|---|---|
join_room |
{roomId, username} |
Join a room |
jpake_round1 |
Round 1 data | J-PAKE round 1 exchange |
jpake_round2 |
Round 2 data | J-PAKE round 2 exchange |
kc1 |
{tag} |
Key confirmation (Alice tag) |
kc2 |
{tag} |
Key confirmation response (Bob tag) |
secure_message |
{message, ctr} |
Encrypted message with counter |
| Event | Payload | Description |
|---|---|---|
room_joined |
Room info | Confirmation of room join |
participant_joined |
User info | New participant notification |
participant_left |
User info | Participant departure |
ready_for_protocol |
{isInitiator, participantCount, participants, sessionId} |
Signal to start J-PAKE |
jpake_round1 |
Round 1 data | Relay round 1 from peer |
jpake_round2 |
Round 2 data | Relay round 2 from peer |
kc1 |
{tag} |
Relay key confirmation (Alice tag) |
kc2 |
{tag} |
Relay key confirmation (Bob tag) |
secure_message |
{sender, message, ctr} |
Relay encrypted message |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/status |
Server status and statistics |
| GET | /api/rooms |
List active rooms (debugging) |
lockstep-chat/
├── client/ # React frontend
│ ├── public/
│ │ ├── index.html # HTML template
│ │ └── favicon.ico # Application icon
│ ├── src/
│ │ ├── components/ # React components
│ │ │ └── SecurityTooltip.js # Security details modal
│ │ ├── utils/ # Utility modules
│ │ │ ├── validation.js # Input validation
│ │ │ ├── crypto.js # Cryptographic utilities
│ │ │ └── time.js # Time formatting
│ │ ├── App.js # Main application component
│ │ ├── App.css # Styles with theme support
│ │ ├── jpake.js # J-PAKE protocol implementation
│ │ └── index.js # React entry point
│ ├── tests/ # Test suites
│ │ ├── jpake.test.js # Unit tests
│ │ ├── integration.test.js # Integration tests
│ │ ├── security.test.js # Security tests
│ │ └── helpers/ # Test utilities
│ └── package.json # Client dependencies
├── server/ # Node.js backend
│ ├── server.js # WebSocket relay server
│ └── package.json # Server dependencies
├── start.sh # Startup script (macOS/Linux)
├── start.bat # Startup script (Windows)
├── start.js # Startup script (Node.js)
├── README.md # This file
└── LICENSE # MIT License
- HTTPS Required: Always use TLS in production
- Password Requirements: Enforce strong password policies
- Rate Limiting: Implement rate limiting to prevent brute force
- Input Validation: Validate all inputs on both client and server
- Content Security Policy: Implement CSP headers
| Limitation | Impact | Mitigation |
|---|---|---|
| Password Quality | Security depends on password entropy | Recommend strong passwords |
| Online Attacks | Vulnerable to online dictionary attacks | Rate limiting needed |
| Side Channels | JavaScript timing attacks possible | Browser crypto trusted |
| Browser Security | Relies on browser's crypto implementation | Use modern browsers |
WebCrypto undefined:
- Ensure Node.js ≥18 is installed
- Verify
tests/setup.mjsis loaded in Jest config
"Cannot use import statement outside a module":
- Ensure
package.jsonhas"type": "module"
Socket connection failures:
- Verify server running on port 3001
- Check CORS configuration
Test timeouts:
- Increase timeout for network-bound tests
- Ensure server is fully started
# Find and kill process on specific port
lsof -ti:3001 | xargs kill -9
lsof -ti:3002 | xargs kill -9Contributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests first (TDD approach)
- Commit your changes
- Push to the branch
- Open a Pull Request
- ES6+ JavaScript
- Functional React components with hooks
- Consistent error handling
- Comprehensive comments for complex operations
- Refactored: Extracted utility functions into dedicated modules
- Refactored: Extracted SecurityTooltip component
- Improved: Added comprehensive JSDoc documentation
- Enhanced: Better separation of concerns
- Changed: HKDF(SHA-256)-based key separation for
encKey/macKey - Changed: Password-to-scalar mapping
s ∈ [1, q-1]via HKDF - Improved: Transcript-bound HKDF salt and canonical transcript ordering
- Improved: Deterministic Round 2 ordering
- Improved: AES-GCM AAD binds session context and message counter
- Complete J-PAKE protocol implementation
- AES-256-GCM encryption
- WebSocket-based real-time communication
- React-based UI with theme support
If you use this implementation in your research, teaching, or projects:
@software{vermillion2025lockstep,
author = {Vermillion, Justin},
title = {Lockstep Chat: A J-PAKE Secure Messaging Application},
year = {2025},
institution = {New Mexico Institute of Mining and Technology},
course = {CYBS 5041: Applied Cryptography},
url = {https://github.com/vermi/lockstep-chat}
}- RFC 8236: J-PAKE: Password-Authenticated Key Exchange by Juggling
- RFC 8235: Schnorr Non-Interactive Zero-Knowledge Proof
- RFC 3526: More Modular Exponential (MODP) Diffie-Hellman groups
- NIST SP 800-56A: Key Establishment Schemes
- NIST SP 800-38D: Galois/Counter Mode (GCM)
MIT License - see LICENSE file for details
- J-PAKE protocol designed by F. Hao and P. Ryan
- RFC 8236 specification by F. Hao
- React team for the excellent framework
- Socket.IO team for real-time capabilities
Academic Disclaimer: This implementation was created as an educational project for CYBS 5041 at New Mexico Tech. While it demonstrates proper cryptographic principles and includes security enhancements, it is intended primarily for learning and demonstration purposes. For production use, consider additional security measures, professional cryptographic review, and compliance with relevant security standards.