@minerouter/mrsap
v1.1.1
Published
MRSAP — MineRouter Secure Application Protocol. End-to-end encrypted client-server communication with OPAQUE passwordless auth, mTLS device certificates, and optional WireGuard transport.
Maintainers
Readme
MRSAP — MineRouter Secure Application Protocol
End-to-end encrypted client-server communication with zero-knowledge password authentication.
OPAQUE (RFC 9807) + mTLS + Ed25519 + MML (Message Layer)Why MRSAP?
Every API today sends the user's password over the wire — even if it's over HTTPS, the server receives the plaintext password. A data breach, a rogue admin, or a logging misconfiguration and passwords are compromised.
MRSAP changes this:
- The password is NEVER sent over the network. Not hashed, not encrypted — never transmitted.
- OPAQUE (RFC 9807) uses zero-knowledge proofs: the server never learns the password, yet the client proves knowledge of it.
- Each device gets a signed identity certificate, bound to the user.
- Every message is encrypted, sequenced, and verified.
- The server cannot reveal passwords even if fully compromised.
Key Features
| Feature | What it gives you | |---|---| | OPAQUE password auth | Password never leaves the client. Server doesn't know it. Zero-knowledge. | | Device certificates | Every device has a unique Ed25519 identity signed by your CA. | | MML Message Layer | Multiplexed streams, sequencing, heartbeat, priority per message. | | Session encryption | XChaCha20-Poly1305 per-message encryption with automatic key rotation. | | Transport agnostic | Works over TCP, TLS, WebSocket, WireGuard. You choose. | | Replay protection | Monotonic sequence numbers + timestamp window. | | No CA infrastructure | Your server IS the CA. Generate root keys once, issue device certs automatically. | | No X.509 complexity | Custom Ed25519-based certificate format — simple, auditable, no OpenSSL. |
How it works
CONNECTING ────► OPAQUE handshake ────► Device cert issued ────► Session active
│ │ │
password never identity signed every message
leaves client by server CA encrypted + signed1. Server setup
import { Server } from '@minerouter/mrsap'
const server = new Server({
port: 8443,
storage: myDatabase, // your Storage implementation
})
await server.listen()2. Client connect
import { Client } from '@minerouter/mrsap'
// Get credentials from login form — NEVER hardcode them
const username = loginForm.username.value
const password = loginForm.password.value
const client = new Client({
serverUrl: 'wss://api.myapp.com',
storage: myVault,
credentials: { username, password },
})
await client.connect()
// ◄── secure channel established
// ◄── password NEVER sent over network (used only for OPAQUE locally)3. Make API calls
// RPC-style call
const data = await client.call('/api/users', { method: 'GET' })
// With body
const user = await client.call('/api/users', {
method: 'POST',
body: { name: 'Alice', role: 'admin' },
})Complete Example
Server
import { Server, MemoryStorage } from '@minerouter/mrsap'
const server = new Server({
port: 8443,
storage: new MemoryStorage(),
})
server.on('connect', (session) => {
console.log(`User ${session.username} connected from device ${session.deviceId}`)
})
server.on('call', async (session, call) => {
if (call.path === '/api/ping') {
return { status: 200, data: { pong: true, user: session.username } }
}
return { status: 404, data: { error: 'not found' } }
})
await server.listen()Client
import { Client, MemoryStorage } from '@minerouter/mrsap'
const client = new Client({
serverUrl: 'wss://localhost:8443',
storage: new MemoryStorage(),
credentials: { username: 'admin', password: '********' }, // from login form
})
await client.connect()
const result = await client.call('/api/ping')
console.log(result.data) // { pong: true, user: 'admin' }API Reference
class Client
| Method | Description |
|---|---|
| new Client(config) | Create a client instance |
| await client.connect() | Establish secure channel (OPAQUE + cert) |
| await client.call(path, options) | Make an RPC call |
| client.isConnected | Whether channel is active |
| await client.heartbeat() | Send keepalive |
class Server
| Method | Description |
|---|---|
| new Server(config) | Create a server instance |
| await server.listen() | Start listening for connections |
| server.on(event, handler) | Register event handler |
| await server.close() | Stop the server |
Events
server.on('connect', (session: ServerSession) => void)
server.on('disconnect', (session: ServerSession) => void)
server.on('call', (session: ServerSession, call: Call) => CallResult)Configuration
type ClientConfig = {
serverUrl: string
storage: Storage // your storage implementation
credentials: { username: string; password: string }
}
type ServerConfig = {
port: number
storage: Storage
caKey?: string // optional: load existing CA
caCert?: string
}Storage Interface
Implement this to integrate with your database:
interface Storage {
getUser(username: string): Promise<UserData | null>
createUser(username: string, password: string, record: OpaqueRecord): Promise<UserRecord>
getDevice(deviceId: string): Promise<DeviceRecord | null>
saveDevice(device: DeviceRecord): Promise<void>
saveSession(id: string, data: unknown): Promise<void>
getSession(id: string): Promise<unknown | null>
deleteSession(id: string): Promise<void>
}Built-in implementations:
MemoryStorage— in-memory, for testingFileStorage— JSON file, for development
Security Model
Protected against
| Threat | MRSAP | |---|---| | Password interception | ❌ Impossible — password never leaves client | | Server data breach | ❌ No password stored, only OPAQUE records | | Replay attack | ❌ Monotonic seq + timestamp ±30s | | Session hijack | ❌ Requires device private key + session key | | MITM | ❌ TLS + certificate verification | | Device impersonation | ❌ Each device has unique Ed25519 cert signed by CA | | Token theft | ❌ No tokens — per-message HMAC + seq |
NOT protected against
| Threat | Mitigation | |---|---| | Malware on client device | Use MRCV (HWID-encrypted key storage, sold separately) | | Compromised CA key | Physical isolation of CA key (offline backup) | | Quantum computer | Will add PQ-hybrid (Kyber + Dilithium) in v2.0 |
Certificate chain
Root CA (Ed25519, offline storage)
└── Server identity (self-signed, known to client)
└── Device certificates (issued per-device after OPAQUE)
├── Ed25519 key pair (generated on device)
├── Bound to: device_id, username
└── Valid for: 365 daysMML — Message Layer
The MRSAP Message Layer provides the frame format for all communication:
┌──────────────────────────────────────────────┐
│ stream_id (2 bytes) — multiplexing stream │
│ type (1 byte) — request/response/etc │
│ flags (1 byte) — priority, EOF, ACK │
│ seq (4 bytes) — anti-replay counter │
│ ack (4 bytes) — last received seq │
│ length (4 bytes) — body length │
│ checksum (4 bytes) — integrity │
├──────────────────────────────────────────────┤
│ Body (XChaCha20-Poly1305, session key) │
└──────────────────────────────────────────────┘Types: HEARTBEAT, REQUEST, RESPONSE, STREAM, ERROR
Transport
MRSAP is transport-agnostic. Choose what fits your use case:
| Transport | Latency | Complexity | Use case | |---|---|---|---| | TCP + TLS | 2-3 RTT | ✅ Simple | Web APIs, mobile apps | | WebSocket | 1 RTT | ✅ Simple | Browser apps, WebSocket-native | | QUIC | 0-1 RTT | 🟡 Medium | Low-latency, unreliable networks | | WireGuard | 0 RTT | 🔴 Advanced | Persistent tunnels, gaming, streaming |
TCP + TLS is the default. Bring your own transport — the Client and Server classes accept raw byte streams.
Why not...
...just use HTTPS + JWT?
JWT requires the server to issue a token. The token can be stolen, replayed, or forged (if signing key leaks). MRSAP's OPAQUE + session key + per-message HMAC eliminates token theft entirely.
...WebAuthn / Passkeys?
WebAuthn is browser-specific and requires a compatible authenticator (hardware key, phone). MRSAP works on any device that can run Node.js — servers, CLIs, IoT, mobile backends.
...just use TLS?
TLS protects the transport layer but does NOT authenticate the user. You still need to send a password or token inside the TLS session. MRSAP adds user authentication + device identity + end-to-end encryption on top of TLS.
Performance
| Operation | Time | Frequency | |---|---|---| | OPAQUE login | ~300-1000ms | Once per session (Argon2id) | | Ed25519 sign | ~0.05ms | Every message | | XChaCha20 encrypt (1KB) | ~0.001ms | Every message | | MML frame encode/decode | ~0.001ms | Every message | | Session key rotation | ~0.01ms | Every 1000 messages |
Use Cases
- Backend APIs — secure microservice-to-microservice communication
- IoT devices — authenticate devices without storing passwords
- CLI tools — authenticate users for CLI commands without token files
- Multiplayer games — persistent encrypted tunnels for game servers
- Financial services — zero-knowledge password authentication
- Healthcare — HIPAA-compliant encrypted data channels
Comparison
| Feature | HTTPS + JWT | TLS + mTLS | MRSAP | |---|---|---|---| | Password on wire | ✅ Plaintext | ✅ Plaintext | ❌ Never | | Server knows password | ✅ Yes | ✅ Yes | ❌ No | | Device identity | ❌ None | ✅ Certificate | ✅ Certificate | | Token theft | ✅ Possible | ❌ N/A | ❌ Impossible | | Replay protection | ❌ None | ✅ TLS | ✅ Seq + HMAC | | Key rotation | ❌ Manual | ❌ Manual | ✅ Auto | | Multiplexing | ❌ One req/conn | ❌ One req/conn | ✅ Streams | | Open standard | ✅ HTTP | ✅ TLS | ✅ RFC 9807 |
Roadmap
- ✅ v1.0.0 — OPAQUE + mTLS + MML + session encryption
- 🔜 v1.1.0 — WireGuard transport
- 🔜 v1.2.0 — QUIC transport (0-RTT)
- 🔜 v2.0.0 — Post-quantum hybrid (Kyber + Dilithium)
- 🔜 v2.1.0 — Pub/Sub broker, file streaming
Platform Disclaimer
Tested on: Linux (Ubuntu 22.04+, Node 22).
Windows and macOS: Core tests pass on CI. OPAQUE WASM may require additional configuration.
Browser: Not currently supported (OPAQUE WASM works, but requires bundler configuration).
Feedback and bug reports welcome.
License
MIT
