Skip to main content

Security & Encryption Architecture

Crosslink is built as a zero-trust transport: with no broker configured, a phone pairs and connects directly to the host over the address in the QR code. When a signaling service, relay, or developer-configured tunnel sits in the path, it only carries metadata or ciphertext. It does not get plaintext, session keys, or private device identity material.

Trust boundaries

The important boundary is not “client vs server”. It is “trusted endpoint vs everything else”.
  • The host owns application identity, capability policy, revocation, and the primary logs.
  • The browser client owns user approval and the user-visible pairing check.
  • Signaling only helps devices discover one another and route the pairing code.
  • Relay only forwards encrypted frames.
If you are debugging a security issue, first identify which boundary failed. A bad QR, a denied capability, a dropped WebSocket, and a revoked device all fail in different places and should look different in logs.

Cryptographic primitives

Crosslink uses the following primitives in the current implementation: The code treats these as implementation choices, not policy knobs. You should not swap them ad hoc in an app, because the rest of the stack expects the transcript, replay protection, and frame encoding to behave exactly the same way.

Pairing trust

Pairing uses two checks that serve different purposes:
  • The QR pins the host fingerprint and lists the routes the client is allowed to try.
  • The SAS digits let a human verify that the screen being scanned is the one the host intended.
That means a relay, signaling service, or tunnel can help with reachability without becoming an identity source. If the host fingerprint does not match, stop. If the SAS does not match, stop. The code path also avoids treating opaque continuation tokens as human-readable pairing codes. That distinction matters because the browser can safely hold a silent continuation token, but it cannot safely hold a shared secret that was meant to be compared by a human.

Capabilities and revocation

Crosslink is capability-based. A paired device only gets the capabilities the host grants, and those grants can be revoked later. Operationally, that means:
  • Keep capability names narrow and specific.
  • Mark dangerous actions with explicit approval.
  • Re-check capability state on every privileged action.
  • Treat revocation as immediate, not eventual.
There is no separate bearer token to “keep around” after revocation. If a device is revoked, the host should refuse subsequent calls and reconnect attempts.

What to log

Crosslink’s logger is structured and redacting by design. Use it for the event name, state transition, and sanitized metadata. Do not log raw request objects, private keys, seed material, auth headers, or full tokens.
Good log data looks like:
  • endpoint kind and transport selection
  • device ids, request ids, and method names
  • a redacted reason for a failure
  • whether a secret store, keychain, or encrypted-file fallback was used
Bad log data looks like:
  • raw Authorization headers
  • pairing codes pasted into logs
  • private keys, seeds, passphrases, or one-time tokens
  • opaque provider error objects copied to users verbatim
If you need more detail, raise the logger level first. Do not work around missing observability by dumping entire objects.

Debugging security-sensitive failures

When a security-sensitive path fails, use the narrowest useful diagnostic:
  1. Check server.status() on the host for the active network mode, transport reachability, and secret-store backend.
  2. Check the host’s advertised endpoints and confirm the route kind matches the intended mode.
  3. If the problem is remote access, run npm run check:remote or node scripts/check-remote-access.mjs <port> and compare the returned reason with the host logs.
  4. If the problem is pairing, verify the QR fingerprint and SAS first, before retrying with a new code.
  5. If the problem is authorization, inspect the capability policy and the requested capability, not just the call site.
The debugging goal is to explain the failure without expanding the trust boundary. You should not loosen TLS, disable fingerprint checks, or replace a structured error with a generic success path just to get a demo running.

Security checklist

  • Use consoleLogger() or another logger that preserves structured fields.
  • Keep secrets in the OS keychain, safeStorage, or encrypted-file fallback only.
  • Never echo tokens, seeds, or private keys back to the browser.
  • Require explicit approval for risky capabilities.
  • Revoke devices instead of rotating the whole app secret when one device is compromised.
  • Prefer https, wss, or localhost for anything that needs browser storage or a service worker.
  • Treat signaling and relay as untrusted transport helpers, not as identity sources.