Pairing token travels in cleartext, lands in cloud backups and never rotates #3

Closed
opened 2026-08-26 01:52:54 +02:00 by benjamin · 1 comment
Owner

Problem

The pairing token is a long-lived secret that grants full control of the TV, but it is exposed on three fronts:

Cleartext on the wire. Pairing.pairingUrl() builds http://<ip>:8765/?t=<token> and the manifest sets android:usesCleartextTraffic="true" (AndroidManifest.xml:18). The initial GET /?t=… is plaintext; the history.replaceState in the remote page only scrubs the address bar afterwards, not what was already transmitted. Any passive observer on the same Wi-Fi (open network, or WPA2 without client isolation — common on consumer routers) captures it.

Cloud backup. android:allowBackup="true" (AndroidManifest.xml:14) with the token stored in SharedPreferences (Pairing.kt) means it is included in Android Auto Backup to Google Drive. Anyone with access to that Google account — a family member, a later owner of a reset device, a compromised account — recovers the token offline, without ever touching the TV.

No rotation. The token is generated once and persisted forever. There is no way to revoke a leaked one short of clearing app data.

Cleartext is also process-wide rather than scoped to the LAN control channel, since there is no network_security_config.xml.

Suggested fix

  • Exclude the pairing prefs from backup (android:dataExtractionRules / fullBackupContent), or set allowBackup="false"
  • Offer "Kopplung zurücksetzen" in the settings so a leaked token can be revoked
  • Scope cleartext to the local server via a network security config instead of allowing it app-wide
  • Consider expiring the QR token after first use and issuing a per-device credential

Found by multi-agent code review; verified against current manifest and Pairing.kt.

## Problem The pairing token is a long-lived secret that grants full control of the TV, but it is exposed on three fronts: **Cleartext on the wire.** `Pairing.pairingUrl()` builds `http://<ip>:8765/?t=<token>` and the manifest sets `android:usesCleartextTraffic="true"` (`AndroidManifest.xml:18`). The initial `GET /?t=…` is plaintext; the `history.replaceState` in the remote page only scrubs the address bar afterwards, not what was already transmitted. Any passive observer on the same Wi-Fi (open network, or WPA2 without client isolation — common on consumer routers) captures it. **Cloud backup.** `android:allowBackup="true"` (`AndroidManifest.xml:14`) with the token stored in SharedPreferences (`Pairing.kt`) means it is included in Android Auto Backup to Google Drive. Anyone with access to that Google account — a family member, a later owner of a reset device, a compromised account — recovers the token offline, without ever touching the TV. **No rotation.** The token is generated once and persisted forever. There is no way to revoke a leaked one short of clearing app data. Cleartext is also process-wide rather than scoped to the LAN control channel, since there is no `network_security_config.xml`. ## Suggested fix - Exclude the pairing prefs from backup (`android:dataExtractionRules` / `fullBackupContent`), or set `allowBackup="false"` - Offer "Kopplung zurücksetzen" in the settings so a leaked token can be revoked - Scope cleartext to the local server via a network security config instead of allowing it app-wide - Consider expiring the QR token after first use and issuing a per-device credential Found by multi-agent code review; verified against current manifest and `Pairing.kt`.
Author
Owner

Fixed in dd612fc, shipped in v0.9.0 — with one deliberate exception noted below.

  • Backup: res/xml/backup_rules.xml and res/xml/data_extraction_rules.xml exclude pairing.xml and auth.xml from both cloud backup and device transfer, wired up in the manifest. The pairing token and the OIDC tokens no longer leave the device.
  • Rotation and revocation: Pairing.reset() drops the QR token, the four-digit code and every session token at once; it is reachable as "Kopplung zurücksetzen" in the settings, and the QR/code redraw immediately afterwards. Code-authenticated clients now hold their own session tokens (see #2), so a single phone can be cut off by resetting rather than being stuck with the master credential forever.
  • Comparison is constant-time via MessageDigest.isEqual.

Deliberately unchanged: usesCleartextTraffic. Restricting it would need a network security config, and Android's config cannot express "any private address range" — only concrete hosts. Since the whole point of the generic source is pointing the TV at an arbitrary LAN server without TLS (ErsatzTV, Threadfin, a local Dispatcharr), scoping it would break legitimate setups without a workable allow-list. The token exposure that mattered is now bounded instead: it can be revoked, and it no longer travels to Google's backup servers.

Fixed in dd612fc, shipped in v0.9.0 — with one deliberate exception noted below. - **Backup:** `res/xml/backup_rules.xml` and `res/xml/data_extraction_rules.xml` exclude `pairing.xml` and `auth.xml` from both cloud backup and device transfer, wired up in the manifest. The pairing token and the OIDC tokens no longer leave the device. - **Rotation and revocation:** `Pairing.reset()` drops the QR token, the four-digit code and every session token at once; it is reachable as "Kopplung zurücksetzen" in the settings, and the QR/code redraw immediately afterwards. Code-authenticated clients now hold their own session tokens (see #2), so a single phone can be cut off by resetting rather than being stuck with the master credential forever. - **Comparison** is constant-time via `MessageDigest.isEqual`. **Deliberately unchanged: `usesCleartextTraffic`.** Restricting it would need a network security config, and Android's config cannot express "any private address range" — only concrete hosts. Since the whole point of the generic source is pointing the TV at an arbitrary LAN server without TLS (ErsatzTV, Threadfin, a local Dispatcharr), scoping it would break legitimate setups without a workable allow-list. The token exposure that mattered is now bounded instead: it can be revoked, and it no longer travels to Google's backup servers.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: be-nj/castarr#3