-
-
Notifications
You must be signed in to change notification settings - Fork 77
Authentication & Security
Rein's security model is designed for local network use — it doesn't assume a public internet threat model, but does protect against unauthorized access from other devices on the same LAN.
| Threat | Mitigation |
|---|---|
| Unauthorized LAN device controlling desktop | Bearer token or ?token= required for all remote REST & WebSocket endpoints |
| Token theft via URL interception | Tokens generated with 128-bit CSPRNG, stored in localStorage on mobile client |
| Token brute-force | 128-bit cryptographic random UUID |
| Timing attacks on token comparison |
crypto.timingSafeEqual() used for all token verification |
| Remote code execution via config | Server-side allowlist of config keys + type/range validation in POST /api/config
|
| Request body attacks | Max body size: 1 MB (MAX_BODY_BYTES) |
| Cross-origin attacks | Server only responds to /api/* paths and /ws WebSocket upgrade |
| Token persistence | File permissions 0o600 (tokens.json) |
| Sensitive data in logs | Stream credentials redacted from log output |
export function generateToken(): string {
return crypto.randomUUID() // 128-bit CSPRNG — 2^122 entropy
}A UUID v4 is used: 122 bits of cryptographic randomness. This is generated via Node.js crypto.randomUUID(), which uses the OS CSPRNG.
POST /api/auth/token (localhost only)
└─ getActiveToken() → return existing if present (avoids QR regeneration)
└─ generateToken() → crypto.randomUUID()
└─ storeToken(token) → persist to tokens.json
└─ return { token }
Settings page encodes token into QR: http://<LAN_IP>:<PORT>/trackpad?token=<TOKEN>
Phone scans QR → opens /trackpad?token=<TOKEN>
└─ Token saved to localStorage ("rein_auth_token")
└─ All API calls include: Authorization: Bearer <TOKEN>
└─ WebSocket connection connects to: ws://<LAN_IP>:<PORT>/ws?token=<TOKEN>
Tokens are persisted to tokens.json (adjacent to the server source):
const TOKENS_FILE = path.resolve(__dirname, "../tokens.json")
const EXPIRY_MS = 10 * 24 * 60 * 60 * 1000 // 10 daysFile is written with mode 0o600:
await writeFile(TOKENS_FILE, JSON.stringify(tokens, null, 2), {
encoding: "utf-8",
mode: 0o600 // Owner read/write only — prevents other users from reading tokens
})To avoid frequent disk writes during an active session, saves are throttled to once per minute (except for forced saves on token create/delete):
const SAVE_THROTTLE_MS = 60 * 1000
if (!force && now - lastSaveTime < SAVE_THROTTLE_MS) returnTokens expire after 10 days of inactivity. purgeExpired() is called on read operations:
function purgeExpired(): void {
tokens = tokens.filter((t) => now - t.lastUsed < EXPIRY_MS)
}Each successful auth call runs touchToken(token) to reset the lastUsed timestamp.
All token lookups use crypto.timingSafeEqual() to prevent timing side-channel attacks:
function timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
}export function isLoopbackAddress(addr?: string): boolean {
return addr === "127.0.0.1" || addr === "::1" || addr === "::ffff:127.0.0.1"
}Localhost requests bypass token checks. This allows the Settings page (running in the browser or Electron on the desktop itself) to load and execute API calls without requiring a token.
Used for API endpoints:
function requireAuth(req: IncomingMessage, res: ServerResponse): boolean {
const addr = req.socket.remoteAddress
if (isLoopbackAddress(addr)) return true // Localhost always passes
const authHeader = req.headers.authorization ?? ""
let token = authHeader.startsWith("Bearer ") ? authHeader.slice(7).trim() : null
if (!token) {
const url = new URL(req.url ?? "", `http://${req.headers.host}`)
token = url.searchParams.get("token")
}
if (!token || !isKnownToken(token)) {
json(res, 401, { error: "Unauthorized" })
return false
}
return true
}During WebSocket upgrade in WebRTCManager (src/server/webRTC.ts):
if (url.pathname === "/ws") {
const addr = request.socket.remoteAddress
const isLocal = isLoopbackAddress(addr)
const token = url.searchParams.get("token")
if (!isLocal && (!token || !isKnownToken(token))) {
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n")
socket.destroy()
return
}
this.wss.handleUpgrade(...)
}The QR code generated on the settings page encodes:
http://<LAN_IP>:<PORT>/trackpad?token=<TOKEN>
On the phone:
- Token is extracted from
?token=query parameter. - Saved to
localStorageasrein_auth_token. - Sent with WebSocket signaling connection (
/ws?token=<token>) and included asAuthorization: Bearer <token>in API calls.
Rein is designed for trusted local networks:
- Network sniffing: HTTP is used by default on LANs. For untrusted network environments, use a VPN or TLS reverse proxy.
- Malicious LAN members: Anyone who captures the QR code or intercepts the token URL can interact with the desktop.
- Physical access: Anyone who can see the QR code on screen can scan it.
For public or enterprise deployments, Rein should be run behind a reverse proxy with TLS.