Repository navigation
Fix HTML escaping in map view - #182
Merged
jleinenbach merged 1 commit intoOct 23, 2025
Merged
Conversation
jleinenbach
added a commit
that referenced
this pull request
Jun 18, 2026
…alize GCM identity whitespace (#1116) * fix(coordinator): only reset decrypt-failure counter on a real decrypt; normalize GCM identity whitespace Two Codex review findings on PR #182. 1. polling.py: the consecutive-decrypt-failure counter was reset on any cycle without an account-wide decrypt error, including idle cycles that only saw empty/no-reporter responses. For intermittently reporting BLE trackers, idle cycles between two failing decrypt cycles perpetually cleared the budget, so the reauth threshold was never reached and the user stayed stuck with stale location data and no reauth prompt. Gate the reset on positive proof (cycle_had_successful_decrypt), mirroring the CryptoStatus.OK gate. Unlike that gate it intentionally does NOT also require not cycle_had_stale_key: this counter tracks the account-wide shared key, while a per-tracker outdated owner key never advances it, so proof the account key works must clear it even when one tracker still carries an outdated key. 2. shared_helpers.py: the all-whitespace credential whitelist omitted the GCM identity material android_id and security_token. Both are parsed via int() on the FCM login path, so an interior space from a line-wrapped secrets.json paste broke push registration despite the normalization fix. Add both keys. Tests: a new mutation-sharp idle-cycle test proves the counter is preserved; the existing reset test now returns a genuinely decryptable location (positive proof) instead of an empty dict; a new GCM-identity normalization test covers both keys plus a negative control (app_id stays untouched). * fix(coordinator): clear shared decrypt-failure budget on background-push decrypt success The account-wide _consecutive_decrypt_failures counter is fed from three sources (poll cycle, FCM background-push decode, manual locate) but was only reset-on-success by the poll cycle. A successful background decrypt never cleared it, so for push-driven accounts with idle scheduled polls a couple of non-consecutive background decrypt failures could accumulate to the reauth threshold and trip a spurious re-authentication for a perfectly healthy key. Introduce a shared note_decrypt_success() entry point on the coordinator that mirrors note_decrypt_failure(): it clears the counter (and the stale last-error) on proven decrypt success. The poll-cycle inline reset is refactored to call it, and the FCM receiver gains _note_decrypt_success_for_entry(), invoked when a background decode yields usable locations. Manual locate stays upward-only by documented design. note_decrypt_success() deliberately does not touch CryptoStatus, which the poll loop owns with its stale-key-aware gating. Addresses the Codex review on this PR. * docs(fcm): clarify source-keyed decrypt-success reset Add an inline rationale at the background decrypt-success reset explaining why the shared reauth budget is cleared for the source entry_id only, not the routed fan-out target set. The background decrypt validates exactly the source entry's cache/owner_key and is symmetric with the failure paths, which also key on the source. Routed targets share the source's FCM registration token (same account-wide owner_key), so the source success already proves their key. Clearing the routed target set instead would break the source entry's own reset invariant and could mask a genuinely stale key on an unrelated entry. No behavior change (comment only). * fix(coordinator): keep stale-key diagnostic when clearing decrypt budget note_decrypt_success() is the shared success entry point for both the poll cycle and the FCM background-push decode. It used to clear _last_decrypt_error unconditionally, but a per-tracker StaleOwnerKeyError records that field for the encryption-key status sensor while deliberately leaving the account-wide counter at zero and keeping CryptoStatus at tracker_key_outdated. On a mixed cycle (one stale tracker, one healthy sibling) the success path then wiped the stale-key diagnostic while the coupled status intentionally stayed outdated, so the sensor reported an outdated tracker without the last_decrypt_error_class needed to triage it. Couple the diagnostic to the CryptoStatus.OK transition instead: the poll OK gate (already gated on `cycle_had_successful_decrypt and not cycle_had_stale_key`) now clears _last_decrypt_error together with the OK status, so both move as one sensor surface. note_decrypt_success() now touches only the account-wide counter, which both the poll and background paths legitimately reset on proven decrypt. This also removes a pre-existing push-path divergence (status invalid + error class cleared). Add a mixed-cycle regression test asserting the StaleOwnerKeyError class survives a sibling's successful decrypt, extend the recovery test to assert the OK gate clears the class when no stale key is present, and align the shared-entry-point tests with the narrowed contract. Mutation-checked both ways. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jleinenbach
added a commit
that referenced
this pull request
Jun 18, 2026
…ecrypt budget (#1117) * fix(coordinator): require an authenticated location before clearing decrypt budget The shared consecutive-decrypt-failure budget (which gates re-authentication) was cleared whenever a decode produced a non-empty result. The decode layer, however, returns a single metadata_only sentinel row when no encrypted report decrypts but the secrets bundle still yields key material (e.g. a phone without a fresh fix). That row is non-empty but carries no authenticated decrypt, so: - background push (fcm_receiver_ha): _note_decrypt_success_for_entry fired on it - poll cycle (polling): cycle_had_successful_decrypt was set on it Report-less metadata_only pushes/polls interleaved with real failures could keep a stale E2EE key below the reauth threshold, leaving the account stuck with no location updates and no reauth prompt. Introduce a shared predicate is_real_location_record() in decrypt_locations.py (the sentinel's producer, the single source of truth for the metadata_only contract) and gate both success signals on it. A genuine decrypted record still clears the budget; the metadata still flows downstream. Manual locate stays upward-only (unchanged). Addresses the Codex review on PR #182. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(coordinator): require decrypted coordinates to clear the decrypt budget SEMANTIC rows skip the crypto path (decrypted_location=b"", continue before authentication) and carry no metadata_only flag, so the previous `metadata_only is not True` denylist wrongly treated them as proof that the account-wide shared key still decrypts. A semantic-only push/poll interleaved with real DecryptionError cycles could clear the reauth budget and strand a stale E2EE key below the threshold. Replace the denylist with an allowlist on positive proof: a record clears the budget only if it carries authenticated coordinates (latitude and longitude not None), the sole output of the crypto path. This rejects metadata_only sentinels, SEMANTIC rows, and any future report-less shape across both consumer paths (poll cycle + background push) without changing own/foreign coordinate-report handling. manual locate stays upward-only. Tests: predicate unit matrix extended (SEMANTIC, partial-coordinate, 0.0), plus poll-cycle and background-push regression tests proving a SEMANTIC-only result does not reset the counter. Mutation-checked (denylist revert and single-axis predicate both turn the new tests red). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(coordinator): prove decrypt over the full response, not the selected record The poll path cleared the shared decrypt-failure (reauth) budget only when the single display-selected location record was an authenticated coordinate report. api.async_get_device_location collapses a device's response to one record via _select_best_location, which ranks by newest last_seen. A newer report-less SEMANTIC/metadata row can therefore outrank and hide a sibling coordinate report that decrypted successfully in the same response, so the poll loop missed the proof, left the counter intact, and a later transient failure could trip a spurious reauth. The decrypt proof is a property of the whole response, not of one record. Add a shared any_real_location_record(records) predicate in decrypt_locations.py and evaluate it over the full candidate list. async_get_device_location carries the full-list verdict to the poll loop via an internal _decrypt_proven hint (popped before caching, like _report_hint); the gate prefers the hint and falls back to the single record only when it is absent (legacy/mocks). The background push path now uses the same shared helper instead of an inline any(...), keeping both automatic paths on one full-list contract. Manual locate stays upward-only and strips the hint at the source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(api): replace asyncio.run with async tests; drop file from guard allowlist tests/AGENTS.md forbids asyncio.run() inside tests because Home Assistant's pytest-asyncio harness already manages the event loop (asyncio_mode = auto); a competing loop breaks fixtures. Codex flagged the two location-selection tests added for the decrypt-proof fix. Convert all four asyncio.run() call sites in tests/test_api_location_selection.py to async def + direct await (the two newly added decrypt-proof tests plus two pre-existing same-file tests). test_sync_wrappers_guard_when_loop_running now runs in the managed loop, which is exactly its guarded condition, so its assertions are unchanged. The file is now free of asyncio.run(), so remove it from LEGACY_ALLOWLIST in tests/test_guard_asyncio_run_antipattern.py; the guard now enforces the no-asyncio.run rule on this file going forward. The remaining ad-hoc loop in test_sync_wrappers_use_provided_session_loop uses asyncio.new_event_loop()/run_until_complete to capture the session-bound loop and is intentionally left as-is (the AST guard only matches asyncio.run). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 18, 2026
jleinenbach
added a commit
that referenced
this pull request
Jun 18, 2026
… push (#1119) Codex flagged on #182 that the "Encryption Key Status" diagnostic sensor can stick on a stale shared_key_invalid / shared_key_missing status on push-only setups whose scheduled polls stay idle, even though successful FCM background pushes keep decrypting real locations. Previously the OK transition was owned solely by the poll cycle, so the reauth budget healed from a push but the sensor did not. Add a push-specific coordinator entry point note_background_decrypt_success() that first clears the shared reauth budget (note_decrypt_success, unchanged) and then lifts the sensor out of the account-wide failure states (SHARED_KEY_INVALID / SHARED_KEY_MISSING) that a proven decrypt refutes, moving the error class with it through the single _set_crypto_status writer. The push path (_note_decrypt_success_for_entry) now calls it, gated as before on any_real_location_record (no metadata-only / SEMANTIC heal). It deliberately leaves TRACKER_KEY_OUTDATED and its error class intact: a single background decode has no cross-tracker view and cannot prove a per-tracker outdated owner key was re-paired, so that state stays owned by the poll cycle (preserves the round-5 diagnostic invariant). UNKNOWN is left untouched too, so OK is never fabricated before the first real signal. The shared note_decrypt_success keeps its contract (account-wide counter only), so the poll path is unchanged. Tests: parametrized heal of both account-wide failure states, an anti-widening guard proving TRACKER_KEY_OUTDATED and its error class survive, and an UNKNOWN no-op; fcm-receiver tests updated to the new push entry point. Mutation-checked (neutralizing the heal gate turns the two heal tests red). ruff + mypy --strict clean, full suite green. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
jleinenbach
added a commit
that referenced
this pull request
Jul 2, 2026
feat: integrate 1.7.2 (E2EE stale-key reauth, encryption-key status sensor, quieter logs)
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.
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68fa35ed5f208329b146b6c06b3d26cc