PONGo_android/CHANGELOG.md
Jon Vanvik 41098831ae Security hardening + code-review fixes
Outcome of a full multi-agent code review. Touches TLS, credential storage,
API versioning, networking correctness, concurrency/lifecycle, and the release
build. See CHANGELOG.md for the full list and the items to port to the iOS app.

Security
- Remove untrusted-TLS support: delete ServerTrustPolicy.AllowSelfSignedForHost
  and its trust-all X509TrustManager; drop the self-signed Settings toggle.
- Enforce HTTPS in normalizedBaseUrl (reject http + embedded credentials);
  add network_security_config (no cleartext, system CAs only) + allowBackup=false.
- Never persist passwords at rest: remember-me stores email only; no password
  pre-fill; clear creds on sign-out / when remember-me is off.
- Match CSRF host lookup to OkHttp's host parsing; stop leaking raw exception text.

Correctness
- Fix API versioning: hardcode v1/v3 per endpoint (docs Sec.4) instead of one
  broken global apiVersion; remove the version plumbing + Settings field.
- Treat "warning" status as success (committed write); fail-fast on a missing
  pagination cursor; close Response on cancellation; tolerant JSON parse; User-Agent.
- Firmware isCompatible fails closed + extra acknowledgment for a non-compatible image.

Concurrency / lifecycle
- Run network I/O on Dispatchers.IO; stale-result generation guards + rethrow
  CancellationException in all loads; host AppState in a ViewModel (survive config
  change); thread-safe cookie jar; reset/upgrade re-entry guard; wire list Retry.

Build
- Enable R8 minify+shrink with kotlinx.serialization keep rules; drop unused
  navigation-compose; add the missing .gitignore.

NOTE: built/verified by static review only (no Android SDK on the dev machine) —
run :app:assembleDebug and a release build before shipping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 17:37:48 +02:00

6.9 KiB

Changelog — PON Go (Android)

All notable changes to this app. The ★ Port to iOS section in each entry calls out changes that affect shared REST behaviour / security / device-safety and most likely have an equivalent in the sibling iOS app (../mcms_ios) — the two share the same MCMS field paths and request semantics (see docs/MCMS_API.md).


[Unreleased] — Security hardening & code-review fixes (2026-05-31)

Outcome of a full code review. No versionCode bump yet — bump before release. Release build now uses R8; run ./gradlew :app:assembleDebug and a release build to verify (the dev machine for this change had no Android SDK).

Security

  • Removed untrusted-TLS support entirely. Deleted ServerTrustPolicy.AllowSelfSignedForHost and its all-accepting X509TrustManager (it accepted any certificate for the configured host — a full MITM hole on a tool that can reset gear and push firmware). Removed the "Allow self-signed certificate" toggle from Settings. Only system-CA trust remains, plus the still-available ServerTrustPolicy.PinPublicKeySha256 for pinning a known box's key. (core/Networking.kt, core/ApiConfiguration.kt, ui/AuthScreens.kt)
  • Enforced HTTPS. normalizedBaseUrl now defaults a missing scheme to https, rejects any non-https URL, and rejects embedded credentials. Added res/xml/network_security_config.xml (cleartext forbidden, system CAs only — blocks the user-installed-CA MITM vector) + usesCleartextTraffic="false". (core/ApiConfiguration.kt, AndroidManifest.xml)
  • Hardened stored credentials. Passwords are no longer persisted at rest: "Remember me" stores the email only; the password field is never pre-filled and is kept out of the saved-instance Bundle (plain remember, not rememberSaveable). Credentials are cleared on sign-out and when "Remember me" is unchecked; any legacy stored password is purged. allowBackup="false". (ui/AppState.kt, ui/AuthScreens.kt, AndroidManifest.xml)
  • CSRF cookie lookup now resolves the host via OkHttp's parser (matching the cookie jar) so the X-CSRFToken lookup can't silently miss on host-normalisation edge cases.
  • Unexpected (non-ApiError) throwables no longer leak raw library text to the UI. (ui/Types.kt)

Correctness

  • Per-endpoint API versioning. Endpoint paths are now fixed at v1/v3 per docs/MCMS_API.md §4 instead of one global apiVersion (which was wrong for every config: lists/states need v3, single-doc/login/logs need v1 — no single value worked). Removed the global apiVersion config field, the version override plumbing, and the API-version Settings input. Versions for undocumented endpoints (controllers, olts/configs, reset, ponmgr/version) are inferred and marked // inferred. (core/McmsEndpoint.kt, core/ApiClient.kt)
  • "warning" status treated as success. The {status,data} unwrap now accepts warning as well as success (a warning means the write committed; only a schema mismatch is reported) — previously a committed firmware/config write was reported as failed. (core/ApiClient.kt)
  • Pagination fails loud, not silent. getAll now throws instead of returning a truncated fleet when a full page yields no usable cursor (e.g. a projection drops _id). (core/ApiClient.kt)
  • Response no longer leaks on coroutine cancellation (resume-with-onCancellation closes a dropped response — prevents connection-pool exhaustion on navigate-away). (core/ApiClient.kt)
  • Firmware compatibility now fails closed. FirmwareFile.isCompatible returns compatible only when vendor + equipment id are known and the file positively matches (was: unknown/missing metadata ⇒ "compatible"). Staging a non-compatible image now requires an explicit extra acknowledgment in the dialog. Mis-flashing can brick an ONU. (core/Firmware.kt, ui/OnuScreens.kt)
  • JSON parse is tolerant. A non-JSON 2xx body yields a typed ApiError.Decoding instead of crashing. A required non-empty User-Agent header is now sent (§3.1). (core/ApiClient.kt)

Concurrency / lifecycle

  • Network I/O off the main thread (Dispatchers.IO) — fixes ANR/jank on large fleets; the onAuthExpired Compose-state write stays on the main thread. (core/ApiClient.kt)
  • Stale-result generation guards in every screen load()/reload(): a slow earlier load can no longer overwrite newer data, and CancellationException is rethrown (a cancelled load is no longer shown as an error). Fixes the ONU-detail tab-switch race. (ui/*Screens.kt)
  • AppState hosted in a ViewModel so rotation / dark-mode no longer rebuilds the connection and bounces the user to login. (MainActivity.kt)
  • Thread-safe SessionCookieJar (synchronised; OkHttp calls it from background threads). (core/Networking.kt)
  • Re-entry guard on reset()/upgrade(); list-screen "Retry" buttons wired up; the dashboard keeps its last-good data when a background refresh fails. (ui/*Screens.kt)

Build / Android

  • Release build enables R8 isMinifyEnabled + isShrinkResources with correct kotlinx.serialization keep rules (so config (de)serialization survives shrinking). (app/build.gradle.kts, app/proguard-rules.pro)
  • Dropped the unused navigation-compose dependency. Added the missing .gitignore. (app/build.gradle.kts, .gitignore)

★ Port to iOS (mcms_ios)

These touch shared REST/security/safety behaviour and very likely have an iOS analogue — review and port:

  1. Per-endpoint v1/v3 versioning — if iOS uses a single global API version it has the same "no value works for everything" defect. Pin versions per endpoint per docs/MCMS_API.md §4.
  2. "warning" = success in the {status,data} unwrap — a committed write must not be reported as a failure.
  3. Firmware isCompatible fail-closed (+ extra acknowledgment for a non-compatible image) — this is a device-brick safety fix, not cosmetic.
  4. Remove the self-signed "escape hatch." HANDOFF notes the Android trust-all path "matches the iOS self-signed escape hatch," so iOS almost certainly has the same MITM hole. Remove it; rely on ATS + system trust, or pin the leaf key for a known box.
  5. Credential hardening — store email only (never the password) in the Keychain; clear on sign-out / when remember-me is off; don't pre-fill the password field.
  6. Reject http:// at config time even though iOS ATS blocks cleartext at the transport layer — fail early with a clear message rather than a confusing transport error.
  7. Pagination fail-fast when a full page has no cursor, rather than silently truncating.
  8. Send a non-empty User-Agent (an empty UA is rejected by MCMS, §3.1).

iOS-specific equivalents worth a glance but not 1:1 ports: cancel-safe request teardown (don't leak the response on task cancellation), and not surfacing stale results when a newer load is in flight.