feat: fix

This commit is contained in:
2026-02-28 18:50:57 +08:00
parent f1054e6476
commit d0bb6f357e
8 changed files with 11474 additions and 85 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ anisette/*
js/node_modules/*
*.so
*.wasm
frontend/.vscode/extensions.json

103
AGENTS.md
View File

@@ -1,91 +1,62 @@
# AGENTS.md
# CLAUDE.md
This file provides guidance to Every Agents when working with code in this repository.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Overview
This crate provides Apple Anisette authentication in browser via WebAssembly. It uses a custom Unicorn Engine fork (https://github.com/lbr77/unicorn/tree/tci-emscripten) to emulate ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) for generating Anisette headers.
Rust/WASM project that emulates ARM64 Android binaries (`libstoreservicescore.so` + `libCoreADI.so`) via a custom Unicorn Engine fork to generate Apple Anisette headers in the browser or Node.js.
**Note**: The Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included in this repository. Extract them from an APK or obtain separately.
The Android library blobs are not included — extract from an APK or obtain separately.
## Build Commands
### Prerequisites
- Rust nightly (edition 2024)
- Emscripten SDK (for WASM builds)
### Setup Unicorn Engine
Clone the custom Unicorn repository and checkout to the `tci-emscripten` branch:
Must source Emscripten before building WASM:
```bash
git clone https://github.com/lbr77/unicorn.git
cd unicorn && git checkout tci-emscripten
source "/Users/libr/Desktop/Life/emsdk/emsdk_env.sh"
```
Then build Unicorn for Emscripten:
```bash
bash script/rebuild-unicorn.sh
bun run build # WASM (debug) + JS bundle
bun run release # WASM (release) + JS bundle
bun run build:js # JS bundle only (no WASM rebuild)
bun run build:glue # WASM only
bun run build:unicorn # Rebuild Unicorn (rarely needed)
```
The rebuild script handles:
- Running `emcmake cmake` with appropriate flags
- Building only `arm` and `aarch64` architectures
- Using static archives (`-DUNICORN_LEGACY_STATIC_ARCHIVE=ON`)
JS bundle outputs to `dist/anisette.js`. WASM glue outputs to `dist/anisette_rs.node.{js,wasm}` and `dist/anisette_rs.{js,wasm}`.
### Build WASM Glue
```bash
bash script/build-glue.sh # Debug build
bash script/build-glue.sh --release # Release build
```
Outputs:
- `test/dist/anisette_rs.js` / `.wasm` (web)
- `test/dist/anisette_rs.node.js` / `.wasm` (Node.js)
- Copies to `../../frontend/public/anisette/`
### Run Native Example
```bash
cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [apple_root_pem]
```
### Run Node.js Example
```bash
node example/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier]
```
The `js/package.json` build script also outputs to `../dist/anisette.js` directly.
## Architecture
### Core Modules
### Rust → WASM layer (`src/`)
- **`adi.rs`**: ADI (Apple Device Identity) wrapper — provisioning, OTP requests
- **`emu.rs`**: Unicorn-based ARM64 emulator core — library loading, symbol resolution, function calls
- **`exports.rs`**: C FFI exports for WASM — `anisette_*` functions
- **`device.rs`**: Device identity management — UUIDs, identifiers, persistence
- **`idbfs.rs`**: IndexedDB filesystem integration for Emscripten
- **`provisioning.rs`** / **`provisioning_wasm.rs`**: Apple provisioning protocol
- `exports.rs` — all `#[no_mangle]` C FFI exports. Every new public function must also be added to `EXPORTED_FUNCTIONS` in `script/build-glue.sh` (both `WEB_EXPORTED_FUNCTIONS` and `NODE_EXPORTED_FUNCTIONS` as appropriate).
- `adi.rs` — wraps the emulated ADI library calls
- `emu.rs` — Unicorn ARM64 emulator core
- `idbfs.rs` — Emscripten IndexedDB FS integration (browser only)
### Emulator Memory Layout
### JS/TS layer (`js/src/`)
- Libraries mapped to import address space with stub hooks
- Stack, heap, and return addresses pre-allocated
- Import hooks dispatch to runtime stubs
- `anisette.ts` — main `Anisette` class. **Each `getData()` call fully reinits the WASM state** (new `WasmBridge`, re-writes VFS files, re-calls `initFromBlobs`) to work around a Unicorn emulator bug that causes illegal writes on repeated use.
- `wasm-bridge.ts` — raw pointer/length marshalling to WASM exports
- `wasm-loader.ts` — thin wrapper around `ModuleFactory`; caller must pass `locateFile` via `moduleOverrides` to resolve the `.wasm` path
- `provisioning.ts` — Apple provisioning HTTP flow (fetches SPIM, sends CPIM)
- `device.ts` — loads or generates `device.json`
### Public API (exports.rs)
### Key design decisions
- `anisette_init_from_blobs` — Initialize from library blobs
- `anisette_is_machine_provisioned` — Check provisioning state
- `anisette_start_provisioning` / `anisette_end_provisioning` — Provisioning flow
- `anisette_request_otp` — Generate OTP + machine ID headers
- `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
- Browser FS/path gotcha:
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
### Data Flow
### Example usage
1. Load Android `libstoreservicescore.so` + `libCoreADI.so`
2. Initialize device identity (`device.json`)
3. Provision with Apple (if needed)
4. Request OTP → `X-Apple-I-MD` + `X-Apple-I-MD-M` headers
```bash
NODE_TLS_REJECT_UNAUTHORIZED=0 bun example/anisette-api.mjs \
<libstoreservicescore.so> <libCoreADI.so> [library_path]
```

View File

@@ -49,6 +49,10 @@ The `js/package.json` build script also outputs to `../dist/anisette.js` directl
- `adi.pb` (provisioning state) lives in the WASM VFS. After provisioning, call `anisette.getAdiPb()` and persist it yourself — it is **not** automatically written to disk.
- `fromSo()` accepts `init.adiPb` and `init.deviceJsonBytes` to restore a previous session into the VFS before init.
- `loadWasm()` is environment-agnostic — no `node:` imports. Pass `locateFile` in `moduleOverrides`.
- Browser FS/path gotcha:
- Prefer `./anisette/` (not `/anisette`) for `libraryPath` / `provisioningPath`.
- The Rust emulation stubs currently hardcode `./anisette` and `./anisette/adi.pb` checks in `src/stub.rs` (`mkdir`/`open`), so absolute paths can break provisioning (e.g. `ADIProvisioningEnd -45054`).
- In browser flow, mount IDBFS and run `syncfs(true)` before ADI init to avoid VFS state being overwritten later.
### Example usage

View File

@@ -32,12 +32,13 @@ async function runDemo() {
status.value = 'Initializing Anisette...'
const anisette = await Anisette.fromSo(ssBytes, caBytes, wasmModule, {
httpClient,
init: { libraryPath: '/anisette' }
init: { libraryPath: './anisette/' }
})
console.log(anisette.getDevice())
if (!anisette.isProvisioned) {
status.value = 'Provisioning...'
await anisette.provision()
} else {
status.vaule = 'Already provisioned, skipping provisioning step'
}
status.value = 'Getting headers...'
@@ -52,7 +53,7 @@ async function runDemo() {
<template>
<div>
<h1>Anisette JS Demo</h1>
<h1>Anisette Vue Demo</h1>
<div>
<button @click="runDemo">Run</button>

View File

@@ -11,6 +11,7 @@ import {
toAppleClientTime,
detectLocale,
encodeUtf8,
decodeUtf8,
} from "./utils.js";
const DEFAULT_DSID = BigInt(-2);
@@ -91,31 +92,41 @@ export class Anisette {
): Promise<Anisette> {
const bridge = new WasmBridge(wasmModule);
const initOpts = options.init ?? {};
const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH;
const provisioningPath = initOpts.provisioningPath ?? libraryPath;
const libraryPath = normalizeAdiPath(initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH);
const provisioningPath = normalizeAdiPath(initOpts.provisioningPath ?? libraryPath);
const dsid = options.dsid ?? DEFAULT_DSID;
// Load or generate device config
const savedDeviceJson = initOpts.deviceJsonBytes
? (() => { try { return JSON.parse(new TextDecoder().decode(initOpts.deviceJsonBytes)) as import("./types.js").DeviceJson; } catch { return null; } })()
: null;
const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig);
// Mount + load persisted IDBFS first so file state is stable before init.
mountIdbfsPaths(bridge, libraryPath, provisioningPath);
try {
await bridge.syncIdbfsFromStorage();
} catch {
// Ignore errors - might be first run with no existing data
}
// Restore adi.pb into VFS if provided
// Load device config from explicit bytes first, then from persisted VFS.
const savedDeviceJson =
parseDeviceJsonBytes(initOpts.deviceJsonBytes) ??
readDeviceJsonFromVfs(bridge, joinPath(libraryPath, "device.json"));
const device = Device.fromJson(savedDeviceJson, initOpts.deviceConfig);
const identifier = initOpts.identifier ?? device.adiIdentifier;
// Restore explicit adi.pb into VFS if provided.
if (initOpts.adiPb) {
bridge.writeVirtualFile(joinPath(provisioningPath, "adi.pb"), initOpts.adiPb);
}
// Write device.json into WASM VFS
// Keep VFS device.json consistent with the active in-memory device.
const deviceJsonBytes = initOpts.deviceJsonBytes ?? encodeUtf8(JSON.stringify(device.toJson(), null, 2));
bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes);
// Initialize WASM ADI
bridge.initFromBlobs(
libs.storeservicescore,
libs.coreadi,
libraryPath,
provisioningPath,
initOpts.identifier ?? device.adiIdentifier
identifier
);
const provisioning = new ProvisioningSession(
@@ -124,8 +135,6 @@ export class Anisette {
options.httpClient
);
const identifier = initOpts.identifier ?? device.adiIdentifier;
return new Anisette(bridge, device, provisioning, dsid, provisioningPath, libraryPath, libs, wasmModule, identifier, options.httpClient);
}
@@ -139,6 +148,12 @@ export class Anisette {
/** Run the provisioning flow against Apple servers. */
async provision(): Promise<void> {
await this.provisioning.provision(this.dsid);
// Sync provisioning state to IndexedDB (browser only)
try {
await this.bridge.syncIdbfsToStorage();
} catch {
// Ignore errors in Node.js or if IDBFS unavailable
}
}
/** Read adi.pb from the WASM VFS for persistence. */
@@ -149,12 +164,27 @@ export class Anisette {
/** Generate Anisette headers. Throws if not provisioned. */
async getData(): Promise<AnisetteHeaders> {
// Reinit WASM state before each call to avoid emulator corruption on repeated use
const adiPb = this.bridge.readVirtualFile(joinPath(this.provisioningPath, "adi.pb"));
const adiPb = readOptionalFile(
this.bridge,
joinPath(this.provisioningPath, "adi.pb")
);
const deviceJsonBytes = encodeUtf8(JSON.stringify(this.device.toJson(), null, 2));
this.bridge = new WasmBridge(this.wasmModule);
this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb);
mountIdbfsPaths(this.bridge, this.libraryPath, this.provisioningPath);
try {
await this.bridge.syncIdbfsFromStorage();
} catch {
// Ignore errors - might be first run or Node.js
}
if (adiPb) {
this.bridge.writeVirtualFile(joinPath(this.provisioningPath, "adi.pb"), adiPb);
}
this.bridge.writeVirtualFile(joinPath(this.libraryPath, "device.json"), deviceJsonBytes);
this.bridge.initFromBlobs(this.libs.storeservicescore, this.libs.coreadi, this.libraryPath, this.provisioningPath, this.identifier);
this.provisioning = new ProvisioningSession(this.bridge, this.device, this.httpClient);
const { otp, machineId } = this.bridge.requestOtp(this.dsid);
@@ -195,3 +225,66 @@ function joinPath(base: string, file: string): string {
const b = base.endsWith("/") ? base : `${base}/`;
return `${b}${file}`;
}
function normalizeAdiPath(path: string): string {
const trimmed = path.trim().replace(/\\/g, "/");
if (!trimmed || trimmed === "." || trimmed === "./" || trimmed === "/") {
return "./";
}
const noTrail = trimmed.replace(/\/+$/, "");
if (!noTrail || noTrail === ".") {
return "./";
}
if (noTrail.startsWith("./") || noTrail.startsWith("../")) {
return `${noTrail}/`;
}
if (noTrail.startsWith("/")) {
return `.${noTrail}/`;
}
return `./${noTrail}/`;
}
function mountIdbfsPaths(
bridge: WasmBridge,
libraryPath: string,
provisioningPath: string
): void {
const paths = new Set([libraryPath, provisioningPath]);
for (const path of paths) {
bridge.initIdbfs(path);
}
}
function readOptionalFile(bridge: WasmBridge, path: string): Uint8Array | null {
try {
return bridge.readVirtualFile(path);
} catch {
return null;
}
}
function parseDeviceJsonBytes(
bytes: Uint8Array | undefined
): import("./types.js").DeviceJson | null {
if (!bytes) {
return null;
}
try {
return JSON.parse(decodeUtf8(bytes)) as import("./types.js").DeviceJson;
} catch {
return null;
}
}
function readDeviceJsonFromVfs(
bridge: WasmBridge,
path: string
): import("./types.js").DeviceJson | null {
const bytes = readOptionalFile(bridge, path);
if (!bytes) {
return null;
}
return parseDeviceJsonBytes(bytes);
}

5782
js/src/anisette_rs.js Normal file

File diff suppressed because one or more lines are too long

5452
js/src/anisette_rs.node.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -215,4 +215,89 @@ export class WasmBridge {
machineId: this.readBytes(midPtr, midLen),
};
}
/**
* Initialize IDBFS for browser persistence.
* Only works in browser environments with IDBFS available.
*/
initIdbfs(path: string): void {
// Check if FS and IDBFS are available (browser only)
if (!this.m.FS || !this.m.FS.filesystems?.IDBFS) {
return; // Node.js or environment without IDBFS
}
const normalizedPath = this.normalizeMountPath(path);
// Create directory structure
if (normalizedPath !== "/") {
try {
this.m.FS.mkdirTree(normalizedPath);
} catch {
// Directory already exists, ignore
}
}
// Mount IDBFS
try {
this.m.FS.mount(this.m.FS.filesystems.IDBFS, {}, normalizedPath);
} catch {
// Already mounted, ignore
}
}
/**
* Sync IDBFS from IndexedDB to memory (async).
* Must be called after initIdbfs to load existing data from IndexedDB.
*/
async syncIdbfsFromStorage(): Promise<void> {
if (!this.m.FS) {
return; // FS not available
}
return new Promise((resolve, reject) => {
this.m.FS.syncfs(true, (err: Error | null) => {
if (err) {
console.error("[anisette] IDBFS sync from storage failed:", err);
reject(err);
} else {
resolve();
}
});
});
}
/**
* Sync IDBFS from memory to IndexedDB (async).
* Must be called after modifying files to persist them.
*/
async syncIdbfsToStorage(): Promise<void> {
if (!this.m.FS) {
return; // FS not available
}
return new Promise((resolve, reject) => {
this.m.FS.syncfs(false, (err: Error | null) => {
if (err) {
console.error("[anisette] IDBFS sync to storage failed:", err);
reject(err);
} else {
resolve();
}
});
});
}
private normalizeMountPath(path: string): string {
const trimmed = path.trim();
const noSlash = trimmed.replace(/\/+$/, "");
const noDot = noSlash.startsWith("./") ? noSlash.slice(2) : noSlash;
if (!noDot || noDot === ".") {
return "/";
} else if (noDot.startsWith("/")) {
return noDot;
} else {
return "/" + noDot;
}
}
}