Harden reconnection, fix resource leaks, improve security and UX - #1
Merged
Conversation
Connection & reconnection: - Broadcast reconnect offers to each interface's directed broadcast address (/24) in addition to 255.255.255.255, which many routers/APs silently drop — the main cause of LAN reconnect never finding a peer. - Deduplicate incoming offer datagrams (same offer now arrives via several broadcast addresses) so a duplicate no longer spawns a second answer connection that breaks the one already completing. - Add a 10s timeout to the reconnect auth handshake; a peer that opens a channel but never answers the challenge no longer leaves a dangling half-trusted connection. - Close (not just drop) peer connections on pre-registration failure on both the initiator and answerer paths — these previously leaked. - Rebind the reconnect UDP socket lazily: a failed bind at startup now self-heals on the next supervisor sweep instead of disabling reconnect until app restart. - Use LAN-only ICE (no STUN) for reconnects so candidate gathering completes in milliseconds instead of waiting up to the 5s STUN deadline on every attempt. - Add fallback STUN servers for pairing. - Guard the connection supervisor against overlapping sweeps (pool-change events firing mid-sweep double-fired reconnects). - Real backpressure in the chunked sender: wait for the data channel buffer to drain below 1MB with a stall timeout, and fail fast with a typed ConnectionError when the channel is no longer open. Security: - The loopback view-once video server now serves only an unguessable random token path and binds unshared; previously any app on the device could port-scan 127.0.0.1 and download the decrypted video. - Zero-fill pending decrypted images/videos on unpair instead of just dropping the references. UX/UI: - Swipe-to-unpair now notifies the remote peer (parity with the in-conversation unpair) so they don't keep a dead pairing. - Image viewer no longer consumes the view on transient `inactive` lifecycle events (same fix the video viewer already had), and can no longer double-pop. - Tapping a message notification opens that conversation, including when the tap cold-launches the app. - Settings: configurable device name (announced during pairing instead of the random "Me_xxxx"), and working max-dimension / JPEG-quality controls that previously did nothing. - Friendlier "Paired" date format on peer tiles. Tests: fake data channels now report an open state; new test covers fail-fast send on a closed channel. 48/48 passing, analyzer clean. https://claude.ai/code/session_016PVQaBGjqmrWPg5zFfpBaB
Fonts (the "weird letter spacing / gradient numbers" bug): - Root cause: the theme pulled Inter at runtime via google_fonts. When the device was offline (or before the first-launch download finished) the primary font was missing and ALL text fell through to the NotoColorEmoji fallback, which renders digits as gradient emoji and Latin letters with broken metrics. - Bundle Inter (Regular/Medium/SemiBold/Bold) locally and drop the google_fonts dependency, removing the network dependency entirely. NotoColorEmoji stays as a fallback but, because Inter covers Latin + digits, it is now only consulted for actual emoji. UI / contrast: - New central theme (lib/core/theme.dart) with a Material 3 dark scheme and shared AppColors tokens, replacing scattered color literals and low-contrast Colors.grey/green/orange usages. - Brightened accent (#8C74FF) and accessible status colors (green/amber) that clear WCAG AA on the near-black background. - Fixed the received chat bubble: it was #12121A, all but invisible against the #0A0A0F background. Now a distinct raised surface with asymmetric corners and proper timestamp contrast on both bubble types. - Polished the empty-peers state, encryption banner (added a lock icon), offline/reconnecting banners, and setup screen to the new tokens. Reconnect auth — wire format v2 (breaks pairings with v1 peers): - Bind the proof-of-key-possession to the DTLS connection: each side now signs `nonce || channelBinding`, where channelBinding is a SHA-256 of the two DTLS certificate fingerprints (sorted) from the local/remote SDP. A direct connection yields the same binding on both ends; a LAN relay/MITM must terminate DTLS with its own certificate per leg, so the fingerprints — and the binding — differ and auth fails. Closes the previously-documented relay gap. - v2 peers refuse the v1 handshake; documented in CLAUDE.md. 48/48 tests pass, analyzer clean. https://claude.ai/code/session_016PVQaBGjqmrWPg5zFfpBaB
- .github/workflows/build-apk.yml: on each push (and manual dispatch), sets up Java 17 + Flutter 3.41.1, runs flutter pub get, and builds with --obfuscate --split-debug-info. Uploads two artifacts: the release APK and the de-obfuscation symbol maps (needed to symbolize crash traces from obfuscated builds). - Untrack android/build/ output that had been committed and ignore the directory so it cannot come back. The release build type signs with the debug keystore (Flutter template default), so CI needs no signing secrets; swap in a real keystore via key.properties + repository secrets when a store-ready build is needed. https://claude.ai/code/session_016PVQaBGjqmrWPg5zFfpBaB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Connection & reconnection:
address (/24) in addition to 255.255.255.255, which many routers/APs
silently drop — the main cause of LAN reconnect never finding a peer.
several broadcast addresses) so a duplicate no longer spawns a second
answer connection that breaks the one already completing.
a channel but never answers the challenge no longer leaves a dangling
half-trusted connection.
both the initiator and answerer paths — these previously leaked.
self-heals on the next supervisor sweep instead of disabling
reconnect until app restart.
completes in milliseconds instead of waiting up to the 5s STUN
deadline on every attempt.
(pool-change events firing mid-sweep double-fired reconnects).
buffer to drain below 1MB with a stall timeout, and fail fast with a
typed ConnectionError when the channel is no longer open.
Security:
random token path and binds unshared; previously any app on the
device could port-scan 127.0.0.1 and download the decrypted video.
dropping the references.
UX/UI:
in-conversation unpair) so they don't keep a dead pairing.
inactivelifecycle events (same fix the video viewer already had), and can no
longer double-pop.
when the tap cold-launches the app.
of the random "Me_xxxx"), and working max-dimension / JPEG-quality
controls that previously did nothing.
Tests: fake data channels now report an open state; new test covers
fail-fast send on a closed channel. 48/48 passing, analyzer clean.
https://claude.ai/code/session_016PVQaBGjqmrWPg5zFfpBaB