Skip to content

Fix decrypt failures, hanging waits, and a few listener crashes - #114

Open
P6g9YHK6 wants to merge 4 commits into
leonboe1:mainfrom
P6g9YHK6:upstrem-major-fixes
Open

Fix decrypt failures, hanging waits, and a few listener crashes#114
P6g9YHK6 wants to merge 4 commits into
leonboe1:mainfrom
P6g9YHK6:upstrem-major-fixes

Conversation

@P6g9YHK6

@P6g9YHK6 P6g9YHK6 commented Aug 11, 2026

Copy link
Copy Markdown

Fixes #22 & #67.

Raise on failed Nova API requests instead of returning None
A failed request used to just print an error and return None. Every caller either discards that return value or waits on some other side effect of the request arriving (like an FCM push for a locate action), so a rejected request was indistinguishable from one that was accepted but never got a response - callers had to sit through a full timeout before finding out something had already failed immediately.

Time out location requests instead of polling forever
get_location_data_for_device() used to spin in a tight sleep loop with no way out if Google's push never arrived (device offline, no fresh fix available, etc.), hanging the script until it got killed by hand. Gives it a 60s default timeout instead, and unregisters the callback afterwards so repeated calls don't leak entries into FcmReceiver's callback list forever.

Fix FCM push listener crashes and hanging sign-in waits

  • Per-message crypto-key/salt headers sometimes come without base64url padding, crashing the listener with a padding error.
  • Those same headers can carry more than one ;-separated parameter, which used to decode into a garbage blob instead of raising, failing later with a confusing EC key error deep inside http_ece.
  • A data message meant for a different app on the same Android ID used to be decrypted anyway (guaranteed to fail) and crash the whole listener - now it's skipped.
  • Selenium's TimeoutException has no message, so a sign-in timeout showed up as a blank error - both sign-in waits now raise a descriptive TimeoutError.
  • Overlapping sign-in flows would kill each other's Chrome process (create_driver() unconditionally pkills chrome before launching) - serialized with a lock.
  • The encryption-confirmation alert-polling loop was unconditional (while True) with a bare except that swallowed everything, so it could hang forever if neither JS callback ever fired - bounded to the same sign-in timeout.

Fix decrypt failures after an E2EE owner key rotation
This is the cryptography.exceptions.InvalidTag / "Failed to decrypt identity key encrypted with owner key version X, current owner key version is Y" failure. Two separate bugs combined to make trackers permanently undecryptable after an account's FMDN owner key was ever rotated (or on any account with more than one owner key generation in play), even when a working key was actually available:

  • The vault can hold more than one key epoch for the finder_hw domain, but the parser always returned whichever entry the array happened to list first instead of comparing epochs.
  • On a decrypt failure, decrypt_locations.py went straight to telling the user to delete their credentials and sign in again, without ever retrying against the tracker's own required owner key version, or against any other version the account might have.

Now it retries with the tracker's own required version, then as a last resort against every owner key blob pulled from the real Find My Device web app's own internal API during sign-in (KeyBackup/vault_web_api.py, new - GetEidInfoForE2eeDevices always hands back only its own idea of "current" no matter what version is actually requested, so this is the only way found so far to reach the others). Only falls through to the delete-and-resignin message if all of those fail too.

A failed request used to just print an error and return None. Every
caller either discards that return value or waits on some other side
effect of the request arriving (like an FCM push for a locate action),
so a rejected request was indistinguishable from one that was accepted
but never got a response - callers had to sit through a full timeout
before finding out something had already failed immediately.
get_location_data_for_device() used to spin in a tight sleep loop with
no way out if Google's push never arrived (device offline, no fresh
fix available, etc.), hanging the script until it got killed by hand.
Give it a 60s default timeout via threading.Event instead, and
unregister the callback afterwards so repeated calls don't leak
entries into FcmReceiver's callback list forever.
- fcmpushclient.py: per-message crypto-key/salt headers sometimes come
  without base64url padding, crashing the listener with a padding
  error; add the same padding fallback already used for der_data/secret.
- fcmpushclient.py: those same headers can carry more than one
  ;-separated parameter, which used to decode into a garbage blob
  instead of raising, failing later with a confusing EC key error deep
  inside http_ece - split off the extra parameter instead.
- fcmpushclient.py: a data message meant for a different app on the
  same Android ID used to be decrypted anyway (guaranteed to fail) and
  crash the whole listener - skip it instead.
- auth_flow.py / shared_key_flow.py: Selenium's TimeoutException has no
  message, so a sign-in timeout showed up as a blank error with no
  indication of what happened - raise a descriptive TimeoutError
  instead.
- auth_flow.py: overlapping sign-in flows would kill each other's
  Chrome process (create_driver() unconditionally pkills chrome before
  launching), so serialize them with a lock.
- shared_key_flow.py: the alert-polling loop for the encryption
  confirmation step was unconditional (while True) with a bare except
  that swallowed everything, so it could hang forever with no way out
  if neither JS callback ever fired - bound it to the same sign-in
  timeout.
Two separate bugs combined to make trackers permanently undecryptable
after an account's FMDN owner key was ever rotated (or on any account
with more than one owner key generation in play), even when a working
key was actually available:

- response_parser.py: the vault can hold more than one key epoch for
  the finder_hw domain, but this always returned whichever entry the
  array happened to list first instead of comparing epochs - a
  stale/rotated-out epoch could win over the current one.
- decrypt_locations.py: on a decrypt failure it went straight to
  telling the user to delete their credentials and sign in again,
  without ever retrying against the tracker's own required owner key
  version, or against any other key version the account might have.

get_eid_info()/get_owner_key() now take an explicit owner_key_version.
decrypt_locations.py retries with the tracker's own required version,
then as a last resort against every owner key blob pulled from the
real Find My Device web app's own internal API during sign-in
(KeyBackup/vault_web_api.py, new - GetEidInfoForE2eeDevices always
hands back only its own idea of 'current' no matter what version is
actually requested, so this is the only way found so far to reach the
others). Only falls through to the delete-and-resignin message if all
of those fail too.
@muddymind

Copy link
Copy Markdown

Thanks! this PR solved the issues I was having.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

getting cryptography.exceptions.InvalidTag

2 participants