Skip to content

Instantly share code, notes, and snippets.

@stek29
Last active June 13, 2026 16:11
Show Gist options
  • Select an option

  • Save stek29/4b15b02cf420223cc4c5b11e4870f5e0 to your computer and use it in GitHub Desktop.

Select an option

Save stek29/4b15b02cf420223cc4c5b11e4870f5e0 to your computer and use it in GitHub Desktop.
log of work done to get export-findmy working, see https://github.com/stek29/export-findmy.git

Worklog

Scope

Made the Find My export CLI buildable against published fork dependencies, fixed export filename collisions, improved Apple ID authentication and reuse, investigated the OpenBubbles upstream state, fixed the iCloud Keychain and PCS failures observed during live testing, fixed the naming-record association, split the resulting dependency changes into publishable fork commits, added interactive escrow bottle deletion, replaced the hardcoded device identity with a persistent profile-centered workspace, and added dual-format export (plist + JSON) for direct FindMy.py compatibility.

Build and dependency setup

  • Installed the protobuf compiler required by the Rust dependency graph.
  • Used local checkouts under vendor/ while diagnosing and separating changes.
  • Fixed the nested clearadi submodule URL to use HTTPS instead of an inaccessible SSH checkout.
  • Published the dependency changes to dedicated forks.
  • Changed the root Cargo.toml back from local paths to Git dependencies.
  • Built the release binary at target/release/export-findmy.
  • Dependency crates emit numerous pre-existing warnings, but builds and tests complete successfully.

The vendor/ directory is now only a local development checkout. It is about 655 MB, contains nested Git repositories and build artifacts, and must not be committed to export-findmy.

Export filename collision fix

AirTags and other items can share the same display name, which previously made later exports overwrite earlier plist files.

The exporter now:

  • Sanitizes the display name for use as a filename.
  • Includes the sanitized hardware model after the display name so exported accessories are easier to identify.
  • Appends a stable item identifier.
  • Adds a deterministic collision suffix if the resulting name is still in use.
  • Includes tests for duplicate names and filename sanitization.

The filename format is now <name>_<model>_<stable-identifier>.<plist|json>. Missing model values use the unknown-model fallback.

Apple ID authentication fixes

Trusted-device and SMS 2FA

The initial AuthSrp failure occurred immediately after Apple displayed the 2FA prompt because the authentication state machine treated trusted-device 2FA and SMS 2FA as the same transition. It requested a trusted-device prompt and then immediately attempted SMS verification with a hardcoded phone identifier, before the CLI could ask for the verification code.

The vendored icloud-auth client now:

  • Handles NeedsDevice2FA separately and prompts for the trusted-device code.
  • Handles NeedsSMS2FA through the SMS flow.
  • Retrieves the actual trusted phone identifier from Apple's auth metadata instead of assuming identifier 1.
  • Reports HTTP failures with operation-specific context.

The automatic 2FA flow handles trusted-device and SMS modes but does not yet expose a choice to the user. A staged login-state-machine API in icloud-auth would let the CLI present available methods (trusted device, multiple phone numbers) and accept the user's selection before requesting a challenge. This is implemented — see the "Interactive Apple 2FA method selection" section below.

Useful authentication logging:

RUST_LOG=icloud_auth=debug,omnisette=debug ./target/release/export-findmy

trace logging should be avoided for normal debugging because authentication responses can contain sensitive account metadata.

Authentication cache

The CLI now supports a plaintext plist cache so successful Apple account authentication can be reused instead of requiring password and 2FA on every run.

The cache contains:

  • Apple ID username.
  • SPD account metadata.
  • GSA, HB, and PET tokens with expiration timestamps.
  • The SHA-256 password hash required by the existing token refresh path.

It does not store the raw password or device passcode.

Behavior:

  • Default path: auth_cache.plist.
  • Cache files are created with Unix mode 0600.
  • The default cache file lives under the active profile directory and is covered by .local/.gitignore.
  • Cached authentication is validated using the MobileMe delegate request.
  • Rejected or revoked cached authentication is deleted immediately, before falling back to interactive login. This prevents the rejected cache from being retried if the fresh login fails or is interrupted.
  • A successful fresh login writes a replacement cache.
  • A cache belonging to a different Apple ID is ignored.

Options:

--auth-cache <path>  Use a custom plaintext cache path
--no-auth-cache      Disable cache reads and writes
--clear-auth-cache   Delete the cache before authentication

Upstream OpenBubbles investigation

Reviewed OpenBubbles/rustpush issue #18 and compared the vendored thisiscam/rustpush findmy-export-support branch with current upstream OpenBubbles/rustpush.

Findings:

  • Issue #18 confirms that owned-item Find My export needs GSA/2FA, Cuttlefish/Octagon trust, PCS, and CloudKit.
  • Remote anisette is sufficient for this path.
  • Albert activation is not required for exporting the account owner's items. APS activation is relevant to sharing and iMessage-related paths.
  • The fork commit inspected was three upstream commits behind, but both trees referenced the same apple-private-apis commit for icloud-auth.
  • Therefore, upstream did not already contain a fix for the observed 2FA state machine defect.
  • Upstream had a relevant MobileMe delegate parsing fix. That narrow fix was ported.
  • Broader upstream authentication changes switched to V2 authentication and required validation data. They were not ported because this CLI's headless device configuration currently supplies no validation data and the change could break authentication.

The MobileMe parser now:

  • Accepts a missing config dictionary with #[serde(default)].
  • Preserves the full service-data dictionary as the MobileMe configuration.
  • Searches recursively for escrowProxyUrl to tolerate Apple response layout differences.

This removes the previous accidental empty configuration and reduces reliance on the hardcoded escrow proxy fallback.

iCloud Keychain escrow diagnosis and fix

Diagnostics

The original failure was:

No escrow bottles found. Make sure you have another trusted device.

Diagnostics were added to distinguish these cases:

  • Apple returned no escrow metadata.
  • Apple returned no viable Cuttlefish bottles.
  • A bottle had no matching metadata record.
  • Matching metadata existed but failed schema validation.

Information-level logging reports record and bottle counts. Debug logging reports the schema error and top-level plist key/type shape without logging plist values or secrets.

Useful escrow logging:

RUST_LOG=rustpush::icloud::keychain=info ./target/release/export-findmy
RUST_LOG=rustpush::icloud::keychain=debug ./target/release/export-findmy

Live result and root cause

The account returned:

Escrow lookup returned 27 metadata record(s) and 4 viable Cuttlefish bottle(s)
Discarded 0 bottle(s) without matching escrow metadata and 4 bottle(s) with invalid metadata

Debug output identified the exact incompatibility:

Serde("missing field `passcodeGeneration`")

All four otherwise matching records omitted passcodeGeneration. The records contained the expected bottle, client metadata, SPKI, build, and timestamp fields, so this was an Apple schema variation rather than an absence of trusted devices or escrow bottles.

Final parser behavior

passcodeGeneration now uses #[serde(default)], producing 0 when Apple omits it.

A broader schema-compatible fallback was briefly implemented during diagnosis. After the exact variation was identified, it was removed. The final parser is strict for the known escrow schema, with only passcodeGeneration treated as optional. Future schema mismatches remain visible in debug logs and are rejected instead of being silently accepted.

The user-facing empty-bottle error now also points to the relevant RUST_LOG target.

Escrow bottle identification

The bottle chooser originally displayed only EscrowMetadata.serial, which made accounts with several viable bottles difficult to use. Those identifiers are device hardware serial numbers.

The chooser now also reads the following optional fields from ClientMetadata:

  • device_name
  • device_model_class
  • device_model

Each choice displays the available device name/model followed by its serial, OS build, and escrow timestamp. If Apple omits the client metadata fields, the serial, build, and timestamp remain available for cross-referencing against the trusted devices listed in Apple Account settings.

Live output confirmed that device_model can contain Apple's human-readable marketing name, for example iPad Pro (11-inch) (2nd generation), rather than a hardware identifier such as iPad8,9.

Post-join CloudKit panic

After the escrow parser fix, a live run successfully joined the trust circle and then panicked while syncing keychain data for the Find My CloudKit fetch. The repeated AAD v2 / decrypted data messages came from CuttlefishEncItem::decrypt, not from the final BeaconStore record decoder.

The vendored upstream implementation used panic-prone operations in that path:

  • authenticated SIV decryption used .unwrap()
  • malformed ISO/IEC 7816-4 padding called panic!("Bad padding!")
  • short ciphertext could panic while slicing the IV
  • decrypted keychain payloads were logged in hexadecimal at INFO

The upstream OpenBubbles revision checked during diagnosis still has the same panic-prone implementation, so there was no newer fix to port.

The local implementation now:

  • validates ciphertext and record-key lengths
  • converts authenticated-decryption and padding failures into a contextual KeychainItemDecryptError
  • logs skipped item identifiers and errors instead of terminating the process
  • moves AAD construction messages to DEBUG
  • no longer logs decrypted keychain payload bytes

Trusted-peer state persistence

The successful join was committed by Apple before the later panic, which explains why the next escrow lookup contained a fifth bottle. The old CLI kept KeychainClientState only in memory, so every process restart had to create a new peer and escrow bottle even after a successful join.

The CLI now stores trusted-peer/keychain state in keychain_state.plist with mode 0600 on Unix. On later runs it loads the state, verifies that the saved identity is still in the clique, and skips escrow recovery when valid.

New options:

  • --keychain-state <path>
  • --clear-keychain-state

The previous run did not save its private peer identity, so its newly visible bottle cannot fully reconstruct that local state. One additional successful join may be required with the fixed build; later runs should print Reusing saved keychain trust identity and should not add bottles.

The live post-join failures were reproduced and diagnosed with:

RUST_BACKTRACE=1 \
RUST_LOG=icloud_auth=debug,omnisette=debug,rustpush::icloud::keychain=debug \
./target/debug/export-findmy --anisette-url "https://ani.neoarz.com"

Post-fix verification:

  • cargo check
  • cargo test --bin export-findmy with all four tests passing
  • cargo build
  • debug binary help output includes both keychain-state options

RFC 6637 AES key-wrap panic

The next live run supplied a backtrace for a separate panic during Find My PCS zone-key decoding:

openssl::symm::Crypter::new: an IV is required for this cipher
rustpush::util::rfc6637_unwrap_key
rustpush::icloud::pcs::PCSKey::new
rustpush::icloud::pcs::PCSShareProtection::decode

rfc6637_unwrap_key passed the ID_AES128_WRAP cipher to OpenSSL's generic openssl::symm::decrypt helper with None for the IV. With OpenSSL crate 0.10.80, the generic Crypter path reports an IV length for that cipher and panics before initialization.

The RFC 6637 implementation now uses OpenSSL's dedicated RFC 3394 APIs:

  • openssl::aes::wrap_key
  • openssl::aes::unwrap_key
  • AesKey::new_encrypt / AesKey::new_decrypt

These APIs correctly apply the RFC 3394 default IV. Both wrapping and unwrapping were switched so locally generated PCS data uses the same code path.

The surrounding decoder now validates:

  • plaintext key size before wrapping
  • compact ephemeral public-key size
  • wrapped ciphertext length and block alignment
  • RFC 6637 plaintext framing byte
  • padding length and bytes
  • checksum

Invalid or tampered inputs return RFC6637Error rather than panicking.

Two focused tests were added:

  • RFC 6637 wrap/unwrap round trip
  • tampered wrapped-key rejection

The adjacent INFO log that printed the unwrapped PCS master key was removed. Focused tests, the application tests, and cargo check all pass after this change.

Find My naming-record association

The first successful export produced 13 decrypted MasterBeacon records, but every output used the Unknown-* fallback and an empty emoji.

The CLI indexed BeaconNamingRecord by its decrypted associatedBeacon field and KeyAlignmentRecord by beaconIdentifier, but then incorrectly looked both up using MasterBeaconRecord.stableIdentifier. The live stable identifiers were values such as l:/..., a:/..., and 2006~#..., so they could not match the naming/alignment references.

Those reference fields point to the MasterBeacon CloudKit record identifier, which is the id key used by upstream rustpush's FindMyClient::sync_items. The assembly code now matches naming and alignment records using that CloudKit record ID. stableIdentifier remains the exported accessory identifier and filename disambiguator.

The CLI now reports how many accessories received matching naming and alignment records, plus counts for any orphaned related records. This makes association failures visible without logging decrypted record contents.

These counts are exact CloudKit-reference lookups, not fuzzy or heuristic matching:

  • each MasterBeaconRecord is indexed by its CloudKit record ID
  • BeaconNamingRecord.associatedBeacon references that ID
  • KeyAlignmentRecord.beaconIdentifier independently references that ID

Naming and alignment are separate side records, so Apple can return different counts for each. The successful live run had 13 MasterBeacon records, 12 matching naming records, and 11 matching alignment records. The nameless MasterBeacon can be identified from the existing output as stable identifier me:/deadbeef-10-dead-beef-deadbeefdead, model iPhone11,6. It may be a stale or system-owned device record rather than a currently visible named Find My accessory.

Upstream rustpush already treats alignment as optional and falls back when it is absent. The current exporter does not serialize alignment fields into the output plist, so missing alignment does not change the exported key material. A missing naming record only causes the Unknown-* name and empty emoji fallback.

The summary now also prints the stable identifier and model for every accessory missing a naming or alignment record. This will identify the two records without alignment on the next run without exposing decrypted keys or secrets.

The exported MasterBeacon private/public keys and secrets were unaffected by the naming bug; only the attached name, emoji, naming ID, and alignment lookup used the wrong key.

Verification after the association fix:

  • cargo check
  • cargo test --bin export-findmy

Dependency repository split and publication

The dependency work was separated from the application so each repository has a reviewable history.

apple-private-apis

Fork:

https://github.com/stek29/apple-private-apis.git

Branch:

local/export-findmy-changes

Commits:

80461a4 Fix trusted-device and SMS 2FA transitions
d051b77 Add reusable Apple account cache serialization
b96e584 Use HTTPS for clearadi submodule

The branch is pushed and clean. The clearadi submodule now uses https://github.com/OpenBubbles/clearadi-stub.git.

rustpush

Fork:

https://github.com/stek29/rustpush.git

Branch:

local/export-findmy-changes

The work was rebased onto OpenBubbles rustpush master before publication. Commits on top of that upstream revision:

7a67df2 Expose APIs needed for headless FindMy key export
2ea7154 Handle nested MobileMe delegate configuration
96c1228 Accept and diagnose escrow metadata variations
119d57e Avoid panics while decrypting keychain items
a0a9257 Use OpenSSL AES key-wrap APIs for RFC 6637
be556fd Use forked apple-private-apis dependency
eaa297f Update apple-private-apis submodule

The branch is pushed and clean. Its apple-private-apis submodule points to commit b96e584 in the fork above.

Headless delegate-login regression

After the root application was switched to the rebased RustPush fork, fresh debug builds failed during the MobileMe delegate request with:

AuthError({"status-message": "We can not process your request, please try again later.", "status": 1})

The older release binary still worked. Comparing the two dependency trees showed that the OpenBubbles rebase had brought in the V2 delegate-login request, even though the earlier investigation had concluded that it should not be used by this headless configuration. The V2 request is intended to carry X-Mme-Nas-Qualify validation data, while FakeIOSConfig returns no validation data.

RustPush now selects the request format based on that capability:

  • non-empty validation data uses the upstream V2 request
  • missing or empty validation data uses the legacy delegate request used by the working release

The compatibility fix was committed and pushed as:

0f5bb1f Support delegate login without validation data

The root ignored lockfile was refreshed to 0f5bb1f, cargo check -p rustpush completed successfully, all four exporter tests passed, and the debug binary was rebuilt. A live Apple-account run is still required to confirm the remote delegate response.

Root dependency configuration

All five internal crates now resolve from the published rustpush branch:

  • rustpush
  • omnisette
  • icloud_auth
  • keystore
  • cloudkit-proto

This was verified without relying on the local vendor/ tree. Cargo fetched the fork, its apple-private-apis submodule, the HTTPS clearadi submodule, and OpenAbsinthe, then passed all four root tests.

Root repository cleanup

The application fork is:

https://github.com/stek29/export-findmy.git

The intended remote layout is:

origin   https://github.com/stek29/export-findmy.git
upstream https://github.com/thisiscam/export-findmy.git

The root application commit should contain:

  • .gitignore
  • Cargo.toml
  • README.md
  • src/main.rs
  • worklog.md

Cargo.lock remains ignored. This project intentionally follows the published RustPush development branch rather than pinning the complete resolved dependency graph in the application repository.

The following local or sensitive material must not be committed:

  • keys/, which contains exported Find My private keys
  • vendor/, which contains the dependency development checkouts
  • target/
  • .local/, which contains private device profiles, auth caches, keychain state, and other per-workspace sensitive state

The scripts under scripts/ are a separate FindMy.py conversion and location fetching workflow. They should be committed separately only after documenting their Python dependencies and ignoring their sensitive account.json state.

Interactive escrow bottle deletion

Added the --delete-own-escrow-bottle maintenance mode. It authenticates, loads the escrow bottle list, displays each bottle's device description, serial, build, and escrow timestamp, and exits without joining the keychain or exporting accessories.

Deletion uses the selected viable bottle's escrow label with the existing targeted KeychainClient::delete API. It does not call reset_clique, which would attempt to remove every viable bottle.

Because Apple's viable bottle list can include records belonging to real devices, deletion requires four deliberate actions:

  1. Select the bottle index.
  2. Successfully unlock the selected bottle with its device passcode/password.
  3. Enter DELETE <index>.
  4. Re-enter the selected device serial immediately before deletion.

Pressing Enter at selection or failing either confirmation cancels without making a deletion request.

The deletion path now branches immediately after Apple authentication and bypasses the MobileMe delegate request. Escrow listing and deletion use the GSA token directly; requiring MobileMe made this maintenance operation fail when Apple temporarily rejected the unrelated iosbuddy/loginDelegates request. Saved keychain state supplies the escrow host when available, otherwise the existing default escrow proxy host is used.

The README now makes escrow cleanup part of the recommended post-export flow. It tells users to delete the synthetic exporter bottle once it is no longer needed, identifies it using the selected device profile, and warns against deleting records belonging to real Apple devices.

A hidden --delete-own-escrow-bottle-without-password escape hatch enters the same interactive deletion flow but skips recovery of the selected bottle. It is intentionally omitted from --help and the README. The index-based DELETE <index> confirmation and exact serial confirmation remain mandatory, and the CLI warns when password verification has been bypassed. This exists for cleanup if the configured bottle password is unavailable.

Persistent device profile and escrow password

Replaced the process-local synthetic identity and fixed findmy-export password with a persistent profile-centered workspace.

The committed device-profile.template.toml has the same [device], [software], and [escrow] schema as the private profile. The README tells users to copy it to .local/device-profile.toml, edit the copy, and pass it with --device-profile. The CLI refuses to run directly from a *.template.toml file. The .local/ directory is ignored by Git. Empty UUID and UDID fields are generated on first use and written back to the copied profile; all other identity fields are required.

The profile controls the device name, serial, hardware model, escrow model class, UUID, UDID, OS version, build, CFNetwork version, and Darwin version. The default escrow model class remains iMac. RustPush commit bdd6399 added a backward-compatible OSConfig::get_model_class() method so escrow metadata no longer hardcodes this value inside the keychain client.

Unless an existing explicit path option overrides them, local artifacts are now derived from the selected profile directory:

  • keys/
  • auth_cache.plist
  • keychain_state.plist
  • keystore.plist
  • anisette_state/

When a synthetic bottle must be created and no password is configured, the CLI offers to generate a 16-character random password or accept a user-entered password with confirmation. No minimum length is enforced; an empty password requires an explicit warning confirmation. The password is stored in the private profile alongside the persistent identity. Profile updates use an atomic temporary-file replacement and mode 0600 on Unix. The password is never printed or serialized into plist state.

EXPORT_FINDMY_ESCROW_PASSWORD remains an ephemeral override and is not written into the profile. A separate password-file override is no longer needed because the private profile is the single configuration artifact.

Bottle deletion uses the saved profile password when the user presses Enter, but still accepts any manually entered password, including the former findmy-export value for an older bottle. The hidden password-verification bypass remains available when no password is known.

Dual-format export (plist + JSON)

Motivation

The original exporter wrote only Apple .plist files. The downstream FindMy.py project also accepts a native JSON format with pre-derived fields such as master_key, skn, sks, alignment_date, and alignment_index. The Python script .local/old/scripts/plist_to_json.py performed this conversion offline.

This session moved the conversion into the exporter itself so both formats are produced in a single run, eliminating the separate Python dependency for users who only need the JSON.

Changes

Cargo.toml

Added:

  • serde_json = "1.0" for JSON serialization
  • hex = "0.4" for hex encoding of binary key material
  • chrono = "0.4" for ISO 8601 datetime formatting

src/main.rs

Serial number parsing (serial_from_identifier):

Ported the Python ~#¶§ parsing logic from plist_to_json.py to Rust. Supports three observed identifier layouts:

  1. AirTag: 2006~#<hwid>~#<serial> — serial is the tail after the last ~#
  2. AirPods: a:/<uuid>~#¶<model>§<hwid>§<hex-ascii-serial>§<position> — serial is sections[2] hex-decoded to ASCII
  3. 3rd-party: a:/<uuid>~#<serial> — serial is the tail after ~#

Returns None when the identifier contains no ~# delimiter.

JSON accessor generation (accessory_to_json):

Maps BeaconAccessory fields to the FindMy.py FindMyAccessoryMapping schema:

JSON key Source
type "accessory"
master_key Last 28 bytes of private_key, hex-encoded
skn shared_secret, hex-encoded
sks shared_secret_2 or secure_locations_shared_secret, hex-encoded
paired_at pairing_date as ISO 8601 / RFC 3339
name naming.name
model master_record.model
identifier master_record.stable_identifier
group_identifier master_record.group_identifier (from CloudKit groupIdentifier field)
serial_number Parsed from stable_identifier via serial_from_identifier
alignment_date alignment.last_index_observation_date as ISO 8601
alignment_index alignment.last_index_observed

Dual-format Step 7:

The export loop now:

  1. Computes a single accessory_basename (e.g. Keys_AirTag_ID-123)
  2. Writes {basename}.plist via plist::to_file_xml
  3. Writes {basename}.json via serde_json::to_string_pretty

The existing collision check (output_paths HashSet) guards both extensions.

Success message:

Updated from "Exported N accessory plist file(s)" to "Exported N accessory file pair(s) (plist + json)".

Tests added:

  • serial_from_airtag_identifier — tail extraction
  • serial_from_airpods_identifier — hex-decode path (¶§485830304141§HX00AA)
  • serial_from_third_party_identifier — direct tail
  • serial_from_identifier_without_tilde_hash — returns None
  • accessory_basename_produces_expected_format — basename consistency
  • json_output_has_expected_schema — round-trip field verification against a synthetic BeaconAccessory

README.md

  • Updated the project description to mention both .plist and .json
  • Updated the --output-dir option description
  • Updated the example run to show both files being written
  • Added a detailed "Output format" section with separate plist and JSON tables
  • Updated the security warning to reference both file extensions
  • Updated the "How it works" step 6 to mention "matched plist and json file pairs"

Verification

Completed successfully across the development session:

  • cargo check
  • cargo test --bin export-findmy — all 14 tests pass, including the 6 new ones for serial parsing and JSON output
  • cargo build / cargo build --release

The Apple authentication and escrow paths require account-owner runtime testing. The live logs supplied during the session confirmed the 2FA fix progressed through login, isolated the escrow failure to the missing optional passcodeGeneration field, and produced a successful trust-circle join.


Add group_identifier to exported accessory data

The group_identifier field (maps multi-part accessories like AirPods to their parent group) was hardcoded to null. FindMy.py requires it to resolve group names.

The MasterBeaconRecord struct in vendored rustpush lacked the field; the CloudKitRecord derive macro silently discards unknown CloudKit fields.

Changes:

  • vendor/rustpush/src/findmy.rs — added group_identifier: Option<String>
  • src/main.rs — wired into accessory_to_json(), accessory_to_plist(), and test fixture
  • README.md — added groupIdentifier to plist table, removed "always null"
  • worklog.md — updated field mapping table

Pushed to stek29/rustpush branch local/export-findmy-changes, then cargo update -p rustpush pulled the new commit. Builds and all 14 tests pass.

Note: Confirmed via FindMySyncPlus (Swift, reads fmipcore cache) that groupIdentifier is a string UUID on Items.data entries matching a Devices.data parent's baUUID. Whether CloudKit's MasterBeaconRecord carries the field directly or it lives in a separate record type remains to be verified — the derive macro logs unmapped fields at info! level.

Interactive Apple 2FA method selection

Replaced the automatic 2FA flow (which followed Apple's au field deterministically) with a staged state-machine API, matching how FindMy.py handles 2FA: always fetch the phone list and present all methods regardless of au, then let the user choose.

New types in icloud-auth: SecondFactorMethod, LoginStep, PendingSecondFactor. New methods on AppleAccount: login_step_start, login_choose_method, login_submit_code. Existing login() and login_with_anisette() remain unchanged.

CLI (login_interactive) now shows a numbered menu of available methods (trusted device + SMS per phone number) and prompts for the code.

Verified: cargo check, cargo test --lib (10 icloud-auth tests), all 14 export-findmy tests. Trusted device flow live-tested. SMS flow implemented but not yet live-tested.

FindMy.py plist compatibility fix

Changed plist generation to match the Apple plist structure consumed by FindMyAccessory.from_plist:

  • Truncate plist dates to whole UTC seconds because Python's plistlib rejects fractional plist dates.
  • Store cryptographic blobs under the Apple keychain-style key.data dictionaries instead of as raw plist data.
  • Store stableIdentifier as a one-element array so FindMy.py can extract the serial number.
  • Keep the scalar identifier field used as the accessory identifier.

End-to-end FindMy.py location verification

FindMy.py successfully fetched and decrypted location reports using both the native Rust JSON and JSON produced from the corrected plist. The native JSON required no changes.

Known FindMy.py conversion behavior:

  • FindMyAccessory.from_plist ignores the plist name, so converted JSON has name: null and the example displays the stable identifier.
  • Without a separate --alignment-plist, converted JSON starts with the pairing date and alignment index 0; report fetching subsequently realigns it.
  • Plist dates have whole-second precision. Native JSON preserves the original RFC 3339 fractional seconds; Python accepts the nanosecond timestamp and truncates it to microseconds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment