Security Architecture

Security is not optional

BeachViber routes remote commands to Claude Code on your machine. That power demands an architecture where security is the foundation, not an afterthought.

Three pillars of trust

🔐

Zero-Trust Relay

The cloud relay is treated as an untrusted intermediary. It routes encrypted payloads it cannot decrypt. A fully compromised relay learns nothing about your code.

Fail Closed

When the approval system is unreachable, all tool executions are denied. No timeouts that default to "allow". No silent failures. Absence of approval = denial.

🛡

Least Privilege

Only read-only tools auto-approve. Every tool that writes files, executes commands, or has side effects requires explicit human approval from your phone.

💪

Your Existing Permissions

BeachViber uses Claude Code's built-in tool permission system — the same one already running on your desktop. We never use --dangerously-skip-permissions. Ever.

End-to-end security architecture

Complete system diagram showing encryption boundaries, trust zones, and data flow between all components.

SECURITY_ARCHITECTURE.md
## BeachViber Security Architecture ## Encryption boundaries, trust zones, and data flow ### Trust Zones TRUSTED UNTRUSTED TRUSTED Phone Cloud Relay Desktop ┌─────────────────────┐ ┌─────────────────────────┐ ┌──────────────────────┐ │ │ │ │ Mobile App (PWA) │ │ AWS Lambda + APIGW │ │ Desktop Agent │ │ │ │ Encrypt plaintext │ │ Route opaque blobs │ │ Decrypt to plaintext with NaCl box() │ │ CANNOT read content │ │ with NaCl box.open() │ │ CANNOT modify content │ │ Keys stay on device │ │ CANNOT forge messages │ │ Keys stay on device │ │ │ │ └──────────┬──────────┘ └────────────┬────────────┘ └───────────┬──────────┘ │ │ │ nacl.box(msg, nonce, pk, sk)nacl.box.open(ct, n, pk, sk) ───────────────────────────►───────────────────────────► │ │ │ ### Encryption Protocol Algorithm: X25519 key exchange + XSalsa20-Poly1305 AEAD Library: TweetNaCl (audited, minimal, zero-dependency) Key size: 256-bit (Curve25519 public keys) Nonce: 192-bit random per message (crypto.getRandomValues) Auth: Poly1305 MAC (tamper detection built-in) PFS: Per-pairing keypairs (compromise one ≠ compromise all) ### Tool Approval Flow Claude Code Desktop Agent Relay Phone ───────────── ───────────── ───── ───── PreToolUse hook fires ──────────────────────────► Is it read-only? YES: auto-approve ◄─ ─ ─ ─ ─{approve}─ ─ ─ ─ NO: ask phone encrypt(request) ──────────────────►────────────────► User reviews tool + args encrypt(response) ◄──────────────────◄──────────────── {approve} or {deny} ◄────────────────────────── TIMEOUT (5 min): ◄─ ─ ─ ─ ─{DENY}─ ─ ─ ─ ─ ### Pairing Security 1. Desktop generates X25519 keypair 2. Public key embedded in QR code 3. Phone scans QR, generates own keypair 4. Phone sends its public key to desktop via relay 5. 6-digit verification code displayed on both devices 6. User confirms codes match (prevents MITM on relay) 7. Shared secret established ✔

Hardened authentication layer

Multiple layers of authentication protect user accounts and device registration.

🔑 Password Security

  • bcrypt hashing with per-user salt rounds
  • Minimum password strength validation via zod schemas
  • Password changes invalidate all existing sessions
  • Account enumeration prevention (identical responses for existing/new accounts)

🎫 JWT Token Management

  • Algorithm pinned to HS256 (prevents alg: none attacks)
  • Short-lived access tokens with refresh rotation
  • Atomic refresh token rotation (DynamoDB conditional delete)
  • Refresh token reuse detection triggers session revocation

🔗 OAuth Security

  • GitHub OAuth with CSRF state parameter validation
  • Timing-safe state comparison prevents bypass
  • No automatic account linking by email (prevents hijack)
  • OAuth tokens never stored or logged on relay

📡 Device Tokens

  • Unique device tokens for each desktop agent
  • Tokens scoped to specific user account
  • Registration requires authenticated API call
  • Tokens validated on every WebSocket connection

End-to-end encryption in depth

🔐 Cryptographic Primitives

All encryption uses the NaCl (Networking and Cryptography library) via TweetNaCl.js, a minimal, audited, zero-dependency implementation.

crypto-primitives.md
## Cryptographic Stack Key Exchange X25519 (Curve25519 Diffie-Hellman) ├── 256-bit keys, ECDH-based shared secret └── One keypair per pairing (per-project isolation) Symmetric Cipher XSalsa20-Poly1305 (AEAD) ├── 256-bit key, 192-bit nonce ├── Authenticated encryption (tamper = rejection) └── Random nonce per message (crypto.getRandomValues) Message Auth Poly1305 MAC ├── 128-bit authentication tag ├── Built into NaCl box construction └── Any modification → decryption fails Nonce Generation crypto.getRandomValues(new Uint8Array(24)) ├── CSPRNG from OS entropy source └── No counter mode (no state to desync) ## What the Relay Sees Plaintext prompts Opaque encrypted blob Code content Message type envelope Tool arguments Sender/receiver IDs File paths Timestamp Session transcripts Message size

Fail-closed execution control

The tool approval system is the critical security boundary between Claude Code's capabilities and your machine. BeachViber works within Claude Code's existing permission model — we never bypass it with --dangerously-skip-permissions.

Auto-Approved (Read-Only)

These tools cannot modify your system and are approved automatically by the desktop agent:

Read Glob Grep WebSearch WebFetch TodoRead TodoWrite Task

Requires Phone Approval

These tools have side effects and require explicit approval from your phone before execution:

Bash Write Edit NotebookEdit MultiEdit

🛠 Approval Server Implementation

  • Unix domain socket IPC (no network exposure)
  • Socket directory created with umask(0o077) and 0700 permissions
  • TOCTOU race condition mitigated via atomic umask-based creation
  • 5-minute approval timeout - unanswered requests are denied
  • Socket unreachable = tool denied (fail-closed, never fail-open)
  • PreToolUse hook installed on agent start, removed on SIGINT/SIGTERM
  • Claude Code runs with your existing desktop permissions — --dangerously-skip-permissions is never used

Relay and infrastructure hardening

🛡

CORS Policy

CORS restricted to process.env.WEBAPP_URL only. No wildcard origins. Prevents cross-origin attacks from malicious sites.

📄

Content Security Policy

Strict CSP headers limit script sources, connection endpoints, and frame embedding. X-Frame-Options: DENY prevents clickjacking.

🕒

DynamoDB TTL

Pairing codes, verification codes, and temporary records auto-expire via DynamoDB TTL. No stale sensitive data persists in the database.

🔎

Input Validation

All API inputs validated with zod schemas at the boundary. Strict type checking prevents injection and malformed data from reaching handlers.

🔒

TLS Enforcement

TLS certificate verification enabled on all outbound connections. No certificate pinning bypass. HTTPS enforced for all API and WebSocket traffic.

📝

No Debug Logging

All debug logging removed from production. No secrets, tokens, keys, or sensitive data written to logs. Console output sanitized.

Local file system protection

filesystem-security.md
## Desktop Agent File Permissions ~/.beachviber/ drwx------ (0700) ├── agents/ drwx------ (0700) │ ├── <project-hash>.json -rw------- (0600) │ │ Contains: keypair, device token, pairing state │ └── ... ├── sessions/ drwx------ (0700) │ └── <session-id>/ drwx------ (0700) └── config.json -rw------- (0600) ## Unix Socket Security Location: /tmp/beachviber-approval-<pid>.sock Created: With process umask(0o077) Access: Owner-only (same UID as agent process) Cleanup: Removed on SIGINT, SIGTERM, and process exit No other users can connect to the approval socket No network exposure (Unix domain socket only) TOCTOU mitigated with atomic umask creation ## Hook Lifecycle 1. Agent starts PreToolUse hook installed in Claude settings 2. Agent running Hook intercepts every tool call via approval-hook.mjs 3. Agent stops SIGINT/SIGTERM handler removes hook from settings 4. Crash/kill -9 Next startup detects stale hook and cleans up

Security audit status

Ongoing security hardening with tracked issues. Critical and high-priority items are resolved.

Category Issue Status
CRITICAL Approval hook fails closed (deny when unreachable) Fixed
CRITICAL TLS certificate verification enabled Fixed
CRITICAL OAuth state parameter timing-safe validation Fixed
CRITICAL Timing-safe comparisons for all secret values Fixed
CRITICAL Relay messages wrapped in typed envelope Fixed
CRITICAL Secrets stored with restrictive file permissions Fixed
HIGH Password reset invalidates all sessions Fixed
HIGH JWT algorithm pinned to HS256 Fixed
HIGH GitHub OAuth no auto-link by email Fixed
HIGH Atomic refresh token rotation Fixed
HIGH CORS hardened to specific origin Fixed
HIGH CSP meta tag + X-Frame-Options DENY Fixed
HIGH Verification codes use CSPRNG Fixed
HIGH Account enumeration prevention Fixed
HIGH Unix socket TOCTOU fixed with umask Fixed
19
Issues resolved
6
Critical fixes shipped
22
Medium fixes shipped
0
Open critical issues

Why we made these choices

?

Why NaCl instead of Web Crypto API?

TweetNaCl is a minimal, audited, constant-time implementation with zero dependencies. It provides the exact primitives we need (box/unbox) without the complexity and footgun potential of the Web Crypto API. The library is small enough to audit in a single sitting.

?

Why Unix sockets for tool approval IPC?

Unix domain sockets provide kernel-enforced access control (UID-based), have no network attack surface, and are cleaned up by the OS. They're the most secure IPC mechanism available for same-machine communication between the agent and Claude Code hook.

?

Why per-project keypairs?

Compromising one project's keypair doesn't compromise others. Each pairing gets its own X25519 keypair, stored in a separate config file with 0600 permissions. This limits the blast radius of any single key compromise.

?

Why fail-closed instead of fail-open?

The tool approval system controls execution of arbitrary shell commands on your machine. The safe default for a system this powerful is always denial. If we can't reach you to ask permission, we don't act.

?

Why no --dangerously-skip-permissions?

Claude Code's CLI has a flag that bypasses all tool permission checks. BeachViber never uses it. Instead, we layer on top of Claude Code's existing permission system — the same one you already trust on your desktop. Your machine, your permissions, no shortcuts.