Skip to content

Authentication & Security

pinjinx edited this page Oct 23, 2026 · 2 revisions

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).


Security Model

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

Token Generation & Storage

export function generateToken(): string {
  return crypto.randomUUID() // 128-bit CSPRNG via OS entropy
}

Tokens are generated via Node.js crypto.randomUUID().

Token Persistence (src/server/tokenStore.ts)

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.

Timing-Safe Verification

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))
}

Authorization Middleware & Rules

Localhost Loopback Checks (isLoopbackAddress)

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/token enforce loopback validation and immediately return 403 to any remote network caller.

Remote Request Authorization (requireAuth)

All other API endpoints (/api/host/*, /api/rtc/*, /api/config, /api/files/*, /api/clipboard/*, /api/debug/*) check:

  1. If caller is loopback (isLoopbackAddress), allow immediately.
  2. Otherwise, extract token from Authorization: Bearer <token> or ?token=<token>.
  3. Verify token with isKnownToken(token).
  4. If invalid or missing, respond immediately with 401 Unauthorized.

File Transfer Security

The bidirectional file sharing feature (src/server/signalling/handlers/fileShare.ts) enforces strict filesystem isolation:

  1. 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)
  2. Directory Isolation: Files are uploaded strictly to the configured uploadDir (default: ~/Downloads/ReinShared).
  3. Stream Boundary Enforcement: Multipart parser accumulates data chunks and destroys the request stream if cumulative size exceeds 512 MB.

Clipboard Safety

Clipboard read and write operations (src/server/clipboard.ts) execute platform-native utilities without passing raw text through a shell:

  • Windows: Invokes powershell.exe with -NoProfile -Command and pipes text directly into [Console]::In.ReadToEnd() via child process stdin.
  • macOS: Spawns pbcopy and pbpaste directly with piped stdin/stdout.
  • Linux: Spawns wl-copy/wl-paste (Wayland) or xclip/xsel (X11) directly with arguments and piped streams.
  • Session Enforcement: Both /api/clipboard/copy and /api/clipboard/paste require an active WebRTC session ID belonging to an authenticated viewer.

QR Code & Connection Handshake

  1. Desktop Settings UI fetches POST /api/auth/token on localhost.
  2. Encodes connection URL into QR code:
    http://<LAN_IP>:<PORT>/trackpad?token=<TOKEN>
    
  3. Viewer scans QR code:
    • Client stores token in localStorage under rein_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).

Clone this wiki locally