Repository navigation
iCloud route: RecoveredPeer.peer_id uses the escrow label, not the Cuttlefish peer hash — every share comes back unreadable #140
Description
Summary
RecoveredPeer.peer_id returns the escrow record's label suffix, but Cuttlefish addresses peers by the SHA256:-prefixed hash carried in the sealed bottle. On my account those are different strings, so FetchRecoverableTlkShares is asked for shares belonging to a peer that does not exist. Apple answers with the view key sets and zero shares, and the export dies at zone_keys().
The failure reads as an account/permissions problem — "carries no readable share record" for every view — and is not one. Every share is present and readable once the right peer id is used.
Fixing it needs the id corrected in both places it is used, not just the fetch. See "Two uses" below.
Environment
- Exporter 1.3.0,
--source icloud - FindMy.py fork,
feat/icloud-keychain-export@337381de(current branch tip) - Linux, Python 3.13, local anisette
- Account: personal, 6 escrow records / 5 recoverable, trust circle of 5 peers. Reproduced identically on a second, unrelated account.
Symptom
INFO findmy.keychain.escrow: Account holds 6 escrow record(s), 5 of them recoverable
INFO findmy.keychain.recovery: Recovered 764 bytes of sealed material from com.apple.icdp.record.<REDACTED>
INFO findmy.keychain.peers: Trust circle holds 5 peer(s)
INFO findmy.keychain.shares: Peer <REDACTED> is entitled to 22 share(s)
INFO findmy.keychain.shares: Entry for DevicePairing carries no readable share record. ...
WARN findmy.keychain.shares: Entry for ProtectedCloudStorage carries no readable share record, and that view is one this needs ...
WARN findmy.keychain.shares: Entry for Manatee carries no readable share record, and that view is one this needs ...
INFO findmy.keychain.shares: Read 0 share(s) across views:
INFO findmy.keychain.session: 0 elliptic-curve key(s) across Manatee, ProtectedCloudStorage
findmy.keychain.session.KeychainSessionError: No keychain keys are held, so nothing can be decrypted.
Escrow recovery itself succeeds — the passcode is correct and the bottle opens. It fails one step later.
Why it is not "this peer has no shares"
Each of the 22 entries carries ~1.4 KB, so I dumped one and decoded it:
RecoverableTlkShare
1: service .......... "Groups" <- parses fine
2: ViewKeySet ....... 1409 B
1: {2: 397B} tlk <- matches the .proto exactly
2: {2: 497B} classA
3: {2: 497B} classB
3: share ............ ABSENT
describe_wire at depth 0 for all 22: 1:bytes 6B, 2:bytes 1409B — field 3 is never present. So the schema is right and the data is real; Apple is returning view keys and declining to return shares, because the peer named in the request is not one it knows.
Root cause
RecoveredPeer.peer_id (findmy/keychain/session.py):
@property
def peer_id(self) -> str:
return self.record.peer_id # com.apple.icdp.record.<suffix>, prefix strippedEscrowRecord.peer_id's own docstring says the value is "a SHA256:-prefixed base64 digest — the same value a peer carries as its hash". On my account it is not: it is a 26-character string with no SHA256: prefix, and it is not in the peer directory. The real id is on the sealed bottle, which recover() already has in scope and already uses for the sponsor lookup:
sponsor = directory.get(sealed.peer_id) or directory.get(inner.peer_id)Proof
Instrumented key_shares to fetch with each candidate id in the same run:
| id source | in peer directory | readable shares |
|---|---|---|
record.peer_id (label suffix) |
no | 0 / 22 |
sealed.peer_id (SHA256:…) |
yes | 21 / 22, Manatee included |
Two uses — fixing only the fetch is not enough
With just the fetch corrected, all 21 shares come back and then every one is rejected:
Shares for <label-suffix-id>: 0/21 unwrapped
because key_shares also passes expected_receiver=peer.peer_id to unwrap_share. Correcting the property fixes both call sites at once.
Fix
Stash the bottle's id at recovery time and prefer it:
# in recover(), replacing the bare `return RecoveredPeer(...)`
recovered = RecoveredPeer(record=record, fields=fields, salt=salt, keys=keys,
bottle=open_bottle(sealed, keys, sponsor=sponsor))
real_id = sealed.peer_id or inner.peer_id
if real_id and real_id != record.peer_id:
logger.info("Addressing the recovered peer as %s (the escrow label says %s)",
real_id, record.peer_id)
object.__setattr__(recovered, "_cuttlefish_peer_id", real_id)
return recovered
# and on RecoveredPeer
@property
def peer_id(self) -> str:
return getattr(self, "_cuttlefish_peer_id", None) or self.record.peer_idFalls back to the previous behaviour when the two agree, so it cannot regress accounts where the label suffix already is the peer hash. (A cleaner version would make it a real dataclass field — I kept the diff minimal to show the point.)
After the fix
INFO findmy.keychain.session: Addressing the recovered peer as SHA256:<REDACTED> (the escrow label says <REDACTED>)
INFO findmy.keychain.session: Shares for SHA256:<REDACTED>: 21/21 unwrapped
INFO findmy.keychain.session: The Manatee view holds 106 elliptic-curve key(s)
INFO findmy.cloudkit.pcs: The zone yields 3 key(s) for its records
Exporting 19
A full export, and on the second account an inventory that matches its known tag count exactly (32 of 32, third-party "Works with Find My" trackers included).
Possibly unrelated, seen on one account
Protection structure's signature did not verify under the master EC key.
The key id matched, so this is more likely a misreading of the signed data's
layout than a wrong key.
Logged once on the larger account; every beacon still decrypted (33 master + 33 naming + 32 alignment). Mentioning it only in case it is the same class of layout issue. Happy to open it separately with a wire dump if useful.
Thanks for building this route — being able to read the inventory without a Mac is the thing that made it worth chasing. Glad to test a patch on both accounts.
Activity
Address a recovered peer by the bottle's id, not the escrow label
parawanderer commentedon Aug 23, 2026
Thanks — fix is in, plus a few things your report turned up on review. Would you re-run against both accounts?
Looking for
Addressing the recovered peer as SHA256:…and a non-zero share count.Details: #144, and
feat/icloud-keychain-export@4f94015in the FindMy.py fork.Your commit is credited on the pin bump, so you'll show up in this repo's contributor history too.
Yes please on the separate signature-verification issue.
3996ec8splits that warning into the four cases it was covering, so a fresh log will say which one you have.parawanderer commentedon Aug 23, 2026
Fixed in #144 per this comment: #144 (comment)
Say where to report something the CLI cannot fix
Take the FindMy.py fix for the peer id the shares are fetched for
Pin the merged FindMy.py, and use the key index it now hands back
Stop the exporter's locking default holding back everything else
Say where to report something the CLI cannot fix
Take the FindMy.py fix for the peer id the shares are fetched for
Stop the exporter's locking default holding back everything else
Pin the merged FindMy.py, and use the key index it now hands back