Security

Your packets are nobody's business but yours.

Wi-Fi is a shared medium. Cafes, hotels, dorms, LAN parties. Anyone on the same network can see your traffic. Dish was built with that in mind.

Threat model

Dish is built to stay safe on networks you don't fully trust:

Pairing

The first time you connect Dish to Satellite, Satellite throws a 4-digit PIN up on your gaming PC. The PIN lives for five minutes, then expires. You type it into Dish on your phone, laptop, or other client device.

Pairing happens over HTTPS on port 9443, on a self-signed certificate Satellite generates locally. The transport protects against passive eavesdroppers; the PIN protects against anyone tricking Satellite into trusting them. The client sends a 32-byte X25519 public key, Satellite computes the shared secret with crypto_scalarmult, and from then on both ends hold a 256-bit symmetric key derived without ever sending it on the wire. The PIN itself is destroyed once pairing finishes.

The shared key is stored locally on both ends and used to authenticate every subsequent reconnect. On the Satellite side it lives in your platform's per-user config directory, mode-protected to your account (Windows DPAPI on V1); on the Android client side it lives in the app's private SharedPreferences, sandboxed by Android to the Dish app and excluded from cloud backup and device transfer. No key ever leaves either device.

Per-packet AEAD

Every controller packet (gamepad input, heartbeat, controller-add, rumble, motion, battery, touchpad, lightbar) is sealed with ChaCha20-Poly1305 (IETF), the same AEAD that TLS 1.3, WireGuard, and SSH use. ChaCha20 is fast and constant-time on every CPU we care about, including Android's ARMv8 and Apple Silicon.

The wire format is small and rigid: 4-byte session token, 4-byte counter, ciphertext, 16-byte Poly1305 authentication tag. The token doubles as additional authenticated data (AAD), so swapping it into a replayed packet causes verification to fail. The nonce is the counter zero-padded to 12 bytes, so no nonce reuse is possible without rolling the session.

Replay protection

Satellite stores the highest counter it has accepted on each connection. Any packet whose counter is less than or equal to that high-water mark is dropped before it ever reaches the gamepad backend. Captured ciphertext from yesterday's session, or even from one second ago, can't be re-played to trigger a button press.

What you don't have to worry about

Zero analytics, zero ads

Dish and Satellite collect no usage data, no identifiers, no analytics, no advertising IDs. The only outbound TinkerNorth-touched flow on Dish for Android is opt-out Firebase Crashlytics for crash and ANR reports: stack trace, device model, install UUID, and that's it. Never gamepad input, never Satellite IPs, never Wi-Fi SSIDs. There is a one-tap opt-out in the app's Settings under Diagnostics, honoured before any crash can be uploaded on the next start. Satellite itself ships with no crash-reporting SDK of any kind. The full scope is on the Dish for Android privacy policy.

We don't run an account system. There's no cloud. Nothing to breach because there's nothing on a server to leak.

Auditable supply chain

Every tagged release ships with:

The verify-blob recipe lives in SECURITY.md in the Satellite repo. If a download doesn't match the published checksum, don't install it.

Open source and auditable

Every line of crypto-touching code lives in a public repo. Satellite uses libsodium; Dish for Android uses libsodium-stable built via the NDK and Android's standard Conscrypt for the HTTPS pairing transport. No homebrew crypto. No "we wrote our own ChaCha."

Distributed under the GNU Lesser General Public License v3.0 or later. Found something off? SECURITY.md in the Satellite repo has our disclosure policy.