Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/macadi-host.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Emulated Mac (host)

# The emulated Mac's native core, built for the host and run against Apple's real macOS CoreADI.
#
# Builds app/src/main/cpp/macadi/ exactly as the app does (MacAdi.cmake is shared), fetches the
# library with the same range request the app makes, checks both pinned hashes, and has the
# emulator load it and answer "not provisioned". Nothing is sent to Apple but that one download:
# provisioning, which would register a machine, is not run.

on:
pull_request:
branches: [ "main" ]
paths:
- "app/src/main/cpp/macadi/**"
- "app/src/test/macadi-host/**"
- "app/src/main/java/dev/wander/android/opentagviewer/anisette/MacCoreAdi*.java"
- "scripts/locate_mac_coreadi.py"
- ".github/workflows/macadi-host.yml"
push:
branches: [ "main" ]
paths:
- "app/src/main/cpp/macadi/**"
- "app/src/test/macadi-host/**"
- "app/src/main/java/dev/wander/android/opentagviewer/anisette/MacCoreAdi*.java"
- "scripts/locate_mac_coreadi.py"
- ".github/workflows/macadi-host.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
host:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Fetch Apple's macOS CoreADI the way the app does, and check both hashes
run: python scripts/locate_mac_coreadi.py --remote --out "$RUNNER_TEMP/CoreADI.x86_64"

- name: Build the native core for the host
run: |
cmake -S app/src/test/macadi-host -B "$RUNNER_TEMP/macadi" -DCMAKE_BUILD_TYPE=Release
cmake --build "$RUNNER_TEMP/macadi" --parallel

- name: Load the library and ask whether it is provisioned
run: |
out=$("$RUNNER_TEMP/macadi/macadi_cli" "$RUNNER_TEMP/CoreADI.x86_64" 979a0 /nonexistent \
00112233445566778899aabbccddeeff -1 --load-only)
echo "$out"
test "$out" = '{"loaded": true, "provisioned": false}'
46 changes: 45 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ python -m venv .venv && .venv/bin/pip install "FindMy==<pinned version>"
- **FindMy's own local provider still does not work.** `anisette` needs `unicorn`, a CPU
emulator Chaquopy cannot build for Android. `app/stubs/unicorn/` is a stand-in that makes
the dependency tree resolve; every method raises. The stub is not a step toward enabling
`aniLocal`, and never will be.
`aniLocal`, and never will be. Unicorn *does* run in the app, but as native code built by
CMake for the emulated Mac (rule 21), never as the Python package.
- **The app does run ADI locally, by a different route.** `app/src/main/java/.../anisette/`
loads Apple's real Android ADI libraries in-process and produces Anisette here, falling
back to a remote server when it cannot. That is unrelated to `aniLocal` and needs no
Expand Down Expand Up @@ -694,6 +695,49 @@ already sent you, and then reasoning from a guess while you wait.
had a phone number blacked out by hand, and the pixels either side of it were not. Quote the
error text, never the surrounding screen, and never re-upload the image.

### 21. Anisette comes from an emulated Mac, and it is one machine for the life of the install

Since 2026-10-12 Apple issues the Find My token (`searchPartyToken`) only to a sign-in whose
Anisette machine was minted by a Mac or an iPhone; an Android-minted one gets `mmeAuthToken` alone
and every report fetch answers 401 ([malmeloo/FindMy.py#278](https://github.com/malmeloo/FindMy.py/issues/278)).
From about 2026-10-20 the edge also answers 503 to any `X-MMe-Client-Info` whose OS token is
`Android`, whatever else is sent ([#261](https://github.com/parawanderer/OpenTagViewer/pull/261)).
So `LocalAnisette`'s Android libraries can no longer reach Find My by any combination of headers.

`EmulatedMacAnisette` runs Apple's **macOS** CoreADI - the x86_64 build in iTunes 12.8.3 - under
Unicorn, in `libmacadi.so` (`app/src/main/cpp/macadi/`, ported from sacca97/os-findmy-e; see its
`NOTICE.md`). It is what `AppDependencies` hands out.

- **The library is fetched from Apple, never shipped.** One HTTP range request: in that exact disk
image the package payload is stored uncompressed and contiguous, so its offset is a constant.
`scripts/locate_mac_coreadi.py` re-derives and checks every constant, reading them from the Java
sources. Both the payload and the library are pinned by SHA-256, and the dispatcher offset
(`0x979A0`) is only valid for that one build.
- **One identity, kept forever** - rule 11, with more at stake. `files/anisette/mac/identity.json`
and `store/` belong together: the library hashes the host UUID into the machine id, Apple binds
the provisioning to the device id, and the Find My token only works alongside Anisette from the
machine that obtained it. Apple will provision a device id **once** (a second attempt answers
HTTP 433 / -29002), so a lost store means a new machine and a new device-list entry.
- **Never leave `adi.pb` in the store.** Provisioning writes the legacy whole-machine blob beside
the per-user one, and while it exists the library says "not provisioned". A process that
provisioned and then asked for headers sent Apple an empty machine id, which surfaces as
**`ec -80009`, "MID is invalid"** - and looked like an emulation fault until a restart, which
never reloads the file, made it go away. `CoreAdi::beginSession` drops it before every session.
- **A Mac's device id is not a UUID**, and FindMy.py's CloudKit client parses it as one. The
keychain and CloudKit clients get `icloud_bridge._forCloudKit`'s view of the account instead,
which answers `device_uuid` with the host UUID and forwards everything else. Anything new that
hands the account to CloudKit has to go through it.
- **Never freeze the emulated clock outside a byte-for-byte comparison.** A constant OTP is
accepted for about a minute and then refused.
- **Grand Slam now asks for a second factor with `hsc` 409 and no `au`**, which the pinned
FindMy.py reads as "no 2FA needed". `main._treatHsc409AsSecondFactor` fills `au` in.
- **The APK carries GPL-2.0 code** (Unicorn). Keep it in `libmacadi.so`, not in
`libopentagviewer.so`.

The emulator suite cannot cover any of this - it swaps the Anisette source for a fake. What does is
`macadi-host.yml` (the library loads in the ported emulator), the JVM tests on the fetcher, the
Python tests in `test_emulated_mac.py`, and a real sign-in on a phone.

---

## Building and testing
Expand Down
21 changes: 21 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,26 @@ static constructor running. No provisioning and nothing sent to Apple beyond dow
libraries. See [`app/src/test/adi-on-bionic/README.md`](app/src/test/adi-on-bionic/README.md) for how
it works and what it measured.

### The emulated Mac, on the host

```bash
python scripts/locate_mac_coreadi.py --remote --out /tmp/CoreADI.x86_64
cmake -S app/src/test/macadi-host -B build/macadi && cmake --build build/macadi --parallel
build/macadi/macadi_cli /tmp/CoreADI.x86_64 979a0 /nonexistent 00112233445566778899aabbccddeeff -1 --load-only
```

Since 2026-10-12 Apple gives the Find My token only to a sign-in whose Anisette machine was
minted by a Mac or an iPhone, so the app runs Apple's **macOS** CoreADI under an x86_64 emulator
(`app/src/main/cpp/macadi/`, see `EmulatedMacAnisette`). This builds the same native core for the
machine you are on, through the same `MacAdi.cmake`, and loads Apple's library into it. The first
line downloads 3.5 MB from Apple with exactly the range request the app makes and checks it
against the pinned hashes; `--dmg iTunes12.8.3.dmg` does the same against a full copy of the
installer and shows which of its chunks the range spans.

Given a provisioned store directory and its host UUID instead of `--load-only`, `macadi_cli`
prints one header set. Pass a fixed Unix time as the fifth argument to compare its output byte
for byte against another implementation; never use a fixed clock for anything sent to Apple.

### Which Python each tree targets

There are three, and they are not the same:
Expand Down Expand Up @@ -886,6 +906,7 @@ run regardless: each AES entry carries a fresh random salt.
| `native-stubs.yml` | `app/src/main/cpp/**`, `app/src/test/cpp/**` changes | Builds the stand-ins for Apple's two stubbed libraries on x86_64 and arm64 Linux and checks what they export and return. Seconds, no Android |
| `adi-on-bionic.yml` | `app/src/main/cpp/**`, `adi-libraries.json`, `AdiFunction.java`, its own directory | Loads and initialises Apple's real ADI libraries on x86_64 and arm64 Linux under Android 9 and Android 14 bionic, with no emulator, and checks the pre-#232 stub still fails. About a minute cold, seconds warm |
| `check-gsa-edge.yml` | daily, and every PR to `main` | Asks Apple's edge whether it still lets the app's and the exporter's sign-in and provisioning requests through, with their exact headers and no account. A scheduled failure opens an issue. Seconds |
| `macadi-host.yml` | `app/src/main/cpp/macadi/**`, `app/src/test/macadi-host/**`, `MacCoreAdi*.java`, `scripts/locate_mac_coreadi.py` | Fetches Apple's macOS CoreADI with the app's own range request, checks both pinned hashes, builds the emulated Mac's native core for the host and has it load the library. Nothing sent to Apple beyond the download. A few minutes, mostly building Unicorn |
| `waiting-on-reporter.yml` | a comment on an issue, daily | Removes the `waiting on reporter` label when the issue's author replies. Daily, comments once on any labelled issue quiet for 14 days, and again only after somebody else has commented. Never closes anything. Both triggers run the copy on `main`, so a PR cannot exercise it; `workflow_dispatch` defaults to a dry run that prints what it would post |

The instrumented job needs KVM on the runner; the workflow enables it first. It runs the same
Expand Down
Loading
Loading