Repository navigation
Authentication & Security
Rein's security model is designed for local network use — it does not assume a public internet threat model, but does strictly protect against unauthorized access, eavesdropping, and accidental exposure from other devices on the same local area network (LAN).
| Threat | Mitigation |
|---|---|
| Unauthorized LAN device controlling desktop | Bearer token (Authorization: Bearer <token>) or ?token= query required for all remote REST & SSE endpoints |
| Token brute-force & prediction | 128-bit CSPRNG UUID v4 (2^122 bits of cryptographic entropy) |
| Timing attacks on token comparison |
crypto.timingSafeEqual() used for all token verification |
| Localhost privilege escalation | Sensitive token generation (POST /api/auth/token) restricted to loopback interfaces only (127.0.0.1, ::1, ::ffff:127.0.0.1); returns 403 Forbidden to remote callers |
| Command injection via clipboard | Host clipboard writers stream text directly to child process stdin via Node.js spawn with piped stdio, bypassing shell evaluation |
| Directory traversal via file sharing | File uploads sanitize paths using path.basename(), reject null bytes and . / .., and resolve exclusively within uploadDir
|
| Denial of service via oversized uploads | File upload body stream aborted if payload exceeds MAX_FILE_BYTES = 512 MB
|
| Directory browsing information disclosure |
GET /api/files/browse automatically filters hidden files (. prefix) and skips unreadable symlinks |
| Token file unauthorized reading | Persisted tokens.json written with file mode 0o600 (owner read/write only) |
| Configuration injection & tampering | Server validates property ranges and allowlists in POST /api/config
|
| Request body buffer exhaustion | JSON body parser enforces 1 MB maximum payload limit |
export function generateToken(): string {
return crypto.randomUUID() // 128-bit CSPRNG via OS entropy
}Tokens are generated via Node.js crypto.randomUUID().
Tokens persist to tokens.json in user data directories (REIN_DATA_DIR, $XDG_CONFIG_HOME/rein/, or ~/.config/rein/):
-
Owner-Only Permissions (
0o600):await writeFile(TOKENS_FILE, JSON.stringify(tokens, null, 2), { encoding: "utf-8", mode: 0o600 })
- Write Throttling: Saves are throttled to once per minute to prevent disk wear during active sessions, except for forced writes when generating or revoking tokens.
-
Expiration & Purging:
Tokens remain valid for 10 days from their last use (
EXPIRY_MS = 10 * 24 * 60 * 60 * 1000). Each authenticated request touches the token (lastUsed = Date.now()). Inactive tokens are purged automatically.
To eliminate timing side-channel attacks during authentication checks, all comparisons use crypto.timingSafeEqual():
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 Bypass: Local requests originating from loopback addresses bypass token checks. This allows the Settings page or desktop Electron window to manage the host without needing manual token configuration.
-
Localhost-Only Endpoints: Sensitive administrative actions like
POST /api/auth/tokenenforce loopback validation and immediately return403to any remote network caller.
All other API endpoints (/api/host/*, /api/rtc/*, /api/config, /api/files/*, /api/clipboard/*, /api/debug/*) check:
- If caller is loopback (
isLoopbackAddress), allow immediately. - Otherwise, extract token from
Authorization: Bearer <token>or?token=<token>. - Verify token with
isKnownToken(token). - If invalid or missing, respond immediately with
401 Unauthorized.
The bidirectional file sharing feature (src/server/signalling/handlers/fileShare.ts) enforces strict filesystem isolation:
-
Path Traversal Sanitization:
const normalizedName = name.replace(/\\/g, "/") const safeName = path.basename(normalizedName) if (!safeName || safeName === "." || safeName === "..") { return json(res, 400, { ok: false, error: "Invalid file name" }) } const filePath = path.join(uploadDir, safeName)
-
Directory Isolation:
Files are uploaded strictly to the configured
uploadDir(default:~/Downloads/ReinShared). -
Stream Boundary Enforcement:
Multipart parser accumulates data chunks and destroys the request stream if cumulative size exceeds
512 MB.
Clipboard read and write operations (src/server/clipboard.ts) execute platform-native utilities without passing raw text through a shell:
-
Windows: Invokes
powershell.exewith-NoProfile -Commandand pipes text directly into[Console]::In.ReadToEnd()via child processstdin. -
macOS: Spawns
pbcopyandpbpastedirectly with piped stdin/stdout. -
Linux: Spawns
wl-copy/wl-paste(Wayland) orxclip/xsel(X11) directly with arguments and piped streams. -
Session Enforcement: Both
/api/clipboard/copyand/api/clipboard/pasterequire an active WebRTC session ID belonging to an authenticated viewer.
- Desktop Settings UI fetches
POST /api/auth/tokenonlocalhost. - Encodes connection URL into QR code:
http://<LAN_IP>:<PORT>/trackpad?token=<TOKEN> - Viewer scans QR code:
- Client stores token in
localStorageunderrein_auth_token. - Sends
Authorization: Bearer <TOKEN>in all subsequent HTTP and SSE signaling requests (/api/rtc/offer,/api/rtc/answer,/api/rtc/ice,/api/rtc/session-sse).
- Client stores token in