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
17 changes: 17 additions & 0 deletions app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,20 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/stubs/AppleStubs.cmake")

add_apple_stub(CoreFoundation)
add_apple_stub(mediaplatform)

# --------------------------------------------------------------------------------------------
# The emulated Mac.
#
# Since 2026-10-12 Apple gives the Find My token only to a Mac or iPhone identity, and a machine
# provisioned by the Android libraries above cannot present as one. So this runs Apple's *macOS*
# CoreADI instead, under an x86_64 emulator - see macadi/MacAdi.cmake and MacCoreAdi.java.
#
# Its own library, not part of libopentagviewer.so: the emulator is GPL-2.0 and several MB, and
# keeping it separate means it is only loaded when the emulated Mac is actually used.
# --------------------------------------------------------------------------------------------

include("${CMAKE_CURRENT_SOURCE_DIR}/macadi/MacAdi.cmake")

add_library(macadi SHARED macadi/jni.cpp)
target_compile_options(macadi PRIVATE -fexceptions)
target_link_libraries(macadi PRIVATE macadi_core log)
40 changes: 40 additions & 0 deletions app/src/main/cpp/macadi/MacAdi.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# The emulated Mac's Anisette core: Apple's macOS CoreADI, x86_64, run under Unicorn.
#
# Defines the static library `macadi_core`. Included by app/src/main/cpp/CMakeLists.txt for the
# app, and by app/src/test/macadi-host/ for the host check - so both build exactly these sources
# against exactly this Unicorn, rather than two copies of how they are built.
#
# **Unicorn is GPL-2.0.** It is fetched at build time, pinned by hash, and linked only into
# libmacadi.so, never into libopentagviewer.so. An APK built with this carries GPL-2.0 code.
#
# Apple's library is never part of the build or the repository. The app downloads it from
# Apple at runtime - see MacCoreAdiFetcher.

include_guard(GLOBAL)
include(FetchContent)

# Only the x86 target: CoreADI is x86_64 code, and every other architecture is megabytes of
# QEMU that would never run.
set(UNICORN_ARCH "x86" CACHE STRING "Enabled unicorn architectures" FORCE)
set(UNICORN_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(UNICORN_INSTALL OFF CACHE BOOL "" FORCE)
set(UNICORN_LEGACY_STATIC_ARCHIVE OFF CACHE BOOL "" FORCE)

FetchContent_Declare(
unicorn
URL https://github.com/unicorn-engine/unicorn/archive/refs/tags/2.1.4.tar.gz
URL_HASH SHA256=ea8863f095a0136388694e5a6063afd9bb7650e30243dd6251af59c5ce5601f4)
FetchContent_MakeAvailable(unicorn)

add_library(macadi_core STATIC
${CMAKE_CURRENT_LIST_DIR}/macho.cpp
${CMAKE_CURRENT_LIST_DIR}/vm.cpp
${CMAKE_CURRENT_LIST_DIR}/core_adi.cpp)
target_include_directories(macadi_core PUBLIC
${CMAKE_CURRENT_LIST_DIR}
${unicorn_SOURCE_DIR}/include)
target_compile_features(macadi_core PUBLIC cxx_std_17)
# Errors are C++ exceptions up to the JNI boundary, where they become Java ones.
target_compile_options(macadi_core PRIVATE -fexceptions)
set_target_properties(macadi_core PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_link_libraries(macadi_core PUBLIC unicorn)
43 changes: 43 additions & 0 deletions app/src/main/cpp/macadi/NOTICE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Third-party notices for the emulated Mac

## sacca97/os-findmy-e

`macho.*`, `vm.*` and `core_adi.*` are a C++ port of
[sacca97/os-findmy-e](https://github.com/sacca97/os-findmy-e) `native/adi-core/src/{macho,vm,adi}.rs`,
and the provisioning headers in `MacProvisioning.java` and the identity shapes in `MacIdentity.java`
follow its `tools/machoadi/{gsa,mac_anisette}.py`. Its licence:

```
MIT License

Copyright (c) 2026 sacca

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```

## Unicorn

[Unicorn](https://github.com/unicorn-engine/unicorn) 2.1.4 is fetched from source at build time
(`MacAdi.cmake`, pinned by SHA-256) and linked into `libmacadi.so`. It is licensed under
**GPL-2.0**, so an APK containing `libmacadi.so` contains GPL-2.0 code.

## Apple's CoreADI

Not part of this repository or of any build. The app downloads it at runtime from Apple's own
server - see `MacCoreAdiFetcher.java` and `scripts/locate_mac_coreadi.py`.
Loading