Skip to content

iCloud route: RecoveredPeer.peer_id uses the escrow label, not the Cuttlefish peer hash — every share comes back unreadable #140

Description

@jamorenom

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 stripped

EscrowRecord.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_id

Falls 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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    @exporter-toolIssues regarding the desktop export tool (wizard and CLI)bugSomething isn't workinghelp wantedExtra attention is needed

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions