init
This commit is contained in:
89
example/anisette.rs
Normal file
89
example/anisette.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anisette_rs::{Adi, AdiInit, Device, ProvisioningSession, init_idbfs_for_path, sync_idbfs};
|
||||
use anyhow::{Context, Result};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD};
|
||||
use serde_json::json;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Usage:
|
||||
// cargo run --example anisette -- <libstoreservicescore.so> <libCoreADI.so> <library_path> [dsid] [apple_root_pem]
|
||||
let storeservices_path = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "libstoreservicescore.so".to_string());
|
||||
let coreadi_path = std::env::args()
|
||||
.nth(2)
|
||||
.unwrap_or_else(|| "libCoreADI.so".to_string());
|
||||
let library_path = std::env::args()
|
||||
.nth(3)
|
||||
.unwrap_or_else(|| "./anisette/".to_string());
|
||||
let dsid_raw = std::env::args().nth(4).unwrap_or_else(|| "-2".to_string());
|
||||
let apple_root_pem = std::env::args().nth(5).map(PathBuf::from);
|
||||
|
||||
let _mount_path = init_idbfs_for_path(&library_path)
|
||||
.map_err(|e| anyhow::anyhow!("failed to initialize IDBFS: {e}"))?;
|
||||
|
||||
fs::create_dir_all(&library_path)
|
||||
.with_context(|| format!("failed to create library path: {library_path}"))?;
|
||||
|
||||
let device_path = Path::new(&library_path).join("device.json");
|
||||
let mut device = Device::load(&device_path)?;
|
||||
|
||||
let storeservicescore = fs::read(&storeservices_path)
|
||||
.with_context(|| format!("failed to read {storeservices_path}"))?;
|
||||
let coreadi =
|
||||
fs::read(&coreadi_path).with_context(|| format!("failed to read {coreadi_path}"))?;
|
||||
|
||||
let mut adi = Adi::new(AdiInit {
|
||||
storeservicescore,
|
||||
coreadi,
|
||||
library_path: library_path.clone(),
|
||||
provisioning_path: Some(library_path.clone()),
|
||||
identifier: None,
|
||||
})?;
|
||||
|
||||
if !device.initialized {
|
||||
println!("Initializing device");
|
||||
device.initialize_defaults();
|
||||
device.persist()?;
|
||||
} else {
|
||||
println!(
|
||||
"(Device initialized: server-description='{}' device-uid='{}' adi='{}' user-uid='{}')",
|
||||
device.data.server_friendly_description,
|
||||
device.data.unique_device_identifier,
|
||||
device.data.adi_identifier,
|
||||
device.data.local_user_uuid
|
||||
);
|
||||
}
|
||||
|
||||
adi.set_identifier(&device.data.adi_identifier)?;
|
||||
|
||||
let dsid = if let Some(hex) = dsid_raw.strip_prefix("0x") {
|
||||
u64::from_str_radix(hex, 16)?
|
||||
} else {
|
||||
let signed: i64 = dsid_raw.parse()?;
|
||||
signed as u64
|
||||
};
|
||||
|
||||
let is_provisioned = adi.is_machine_provisioned(dsid)?;
|
||||
|
||||
if !is_provisioned {
|
||||
println!("Provisioning...");
|
||||
let mut provisioning_session =
|
||||
ProvisioningSession::new(&mut adi, &device.data, apple_root_pem)?;
|
||||
provisioning_session.provision(dsid)?;
|
||||
} else {
|
||||
println!("(Already provisioned)");
|
||||
}
|
||||
|
||||
let otp = adi.request_otp(dsid)?;
|
||||
let headers = json!({
|
||||
"X-Apple-I-MD": STANDARD.encode(otp.otp),
|
||||
"X-Apple-I-MD-M": STANDARD.encode(otp.machine_id),
|
||||
});
|
||||
|
||||
let _ = sync_idbfs(false);
|
||||
println!("{}", serde_json::to_string_pretty(&headers)?);
|
||||
Ok(())
|
||||
}
|
||||
790
example/browser-run.js
Normal file
790
example/browser-run.js
Normal file
@@ -0,0 +1,790 @@
|
||||
|
||||
|
||||
// Set up Module configuration before loading libcurl
|
||||
if (typeof window !== 'undefined') {
|
||||
window.Module = window.Module || {};
|
||||
window.Module.preRun = window.Module.preRun || [];
|
||||
window.Module.preRun.push(function() {
|
||||
// Get the default CA certs from libcurl (will be available after WASM loads)
|
||||
// We'll create the extended CA file here
|
||||
console.log('[preRun] Setting up extended CA certificates');
|
||||
});
|
||||
}
|
||||
|
||||
import { libcurl } from './libcurl.mjs';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
glueUrl: './anisette/anisette_rs.js',
|
||||
storeservicesUrl: './arm64-v8a/libstoreservicescore.so',
|
||||
coreadiUrl: './arm64-v8a/libCoreADI.so',
|
||||
libraryPath: './anisette/',
|
||||
provisioningPath: './anisette/',
|
||||
identifier: '',
|
||||
dsid: '-2',
|
||||
assetVersion: '',
|
||||
rustBacktrace: 'full',
|
||||
rustLibBacktrace: '1',
|
||||
};
|
||||
|
||||
const state = {
|
||||
module: null,
|
||||
storeservicesBytes: null,
|
||||
coreadiBytes: null,
|
||||
};
|
||||
|
||||
const CONFIG = loadConfig();
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
function log(message) {
|
||||
const line = `${message}`;
|
||||
console.log(line);
|
||||
logEl.textContent += `${line}\n`;
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
const cfg = { ...DEFAULT_CONFIG };
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
for (const key of Object.keys(cfg)) {
|
||||
const value = params.get(key);
|
||||
if (value !== null) {
|
||||
cfg[key] = value;
|
||||
}
|
||||
}
|
||||
if (!cfg.assetVersion) {
|
||||
cfg.assetVersion = String(Date.now());
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMountPath(path) {
|
||||
const trimmed = (path || '').trim();
|
||||
if (!trimmed || trimmed === '/' || trimmed === './' || trimmed === '.') {
|
||||
return '/';
|
||||
}
|
||||
|
||||
const noTrailing = trimmed.replace(/\/+$/, '');
|
||||
const noDot = noTrailing.startsWith('./') ? noTrailing.slice(1) : noTrailing;
|
||||
if (noDot.startsWith('/')) {
|
||||
return noDot;
|
||||
}
|
||||
return `/${noDot}`;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
let s = '';
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
s += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(s);
|
||||
}
|
||||
|
||||
function base64ToBytes(text) {
|
||||
const clean = (text || '').trim();
|
||||
if (!clean) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
const s = atob(clean);
|
||||
const out = new Uint8Array(s.length);
|
||||
for (let i = 0; i < s.length; i += 1) {
|
||||
out[i] = s.charCodeAt(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function dsidToU64(value) {
|
||||
return BigInt.asUintN(64, BigInt(value.trim()));
|
||||
}
|
||||
|
||||
async function fetchBytes(url, label) {
|
||||
const res = await fetch(url);
|
||||
assert(res.ok, `${label} fetch failed: HTTP ${res.status} (${url})`);
|
||||
return new Uint8Array(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
function ensureExport(name) {
|
||||
const fn = state.module[name];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`missing export ${name}`);
|
||||
}
|
||||
return fn;
|
||||
}
|
||||
|
||||
function allocBytes(bytes) {
|
||||
if (bytes.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const malloc = ensureExport('_malloc');
|
||||
const ptr = Number(malloc(bytes.length));
|
||||
state.module.HEAPU8.set(bytes, ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function allocCString(text) {
|
||||
const value = text || '';
|
||||
const size = state.module.lengthBytesUTF8(value) + 1;
|
||||
const malloc = ensureExport('_malloc');
|
||||
const ptr = Number(malloc(size));
|
||||
state.module.stringToUTF8(value, ptr, size);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function readBytes(ptr, len) {
|
||||
return state.module.HEAPU8.slice(Number(ptr), Number(ptr) + Number(len));
|
||||
}
|
||||
|
||||
function readRustError() {
|
||||
const getPtr = ensureExport('_anisette_last_error_ptr');
|
||||
const getLen = ensureExport('_anisette_last_error_len');
|
||||
const ptr = Number(getPtr());
|
||||
const len = Number(getLen());
|
||||
if (len === 0) {
|
||||
return '';
|
||||
}
|
||||
return new TextDecoder().decode(readBytes(ptr, len));
|
||||
}
|
||||
|
||||
function call(name, fn) {
|
||||
let ret;
|
||||
try {
|
||||
ret = fn();
|
||||
} catch (e) {
|
||||
log(`${name}: trap=${String(e)}`);
|
||||
throw e;
|
||||
}
|
||||
const err = readRustError();
|
||||
log(`${name}: ret=${ret}${err ? ` err=${err}` : ''}`);
|
||||
if (ret < 0) {
|
||||
throw new Error(`${name} failed: ${err || `ret=${ret}`}`);
|
||||
}
|
||||
return { ret, err };
|
||||
}
|
||||
|
||||
function resolveWasmUrl(jsUrl) {
|
||||
const url = new URL(jsUrl, window.location.origin);
|
||||
if (!url.pathname.endsWith('.js')) {
|
||||
throw new Error(`invalid glue path (expect .js): ${url.href}`);
|
||||
}
|
||||
url.pathname = url.pathname.slice(0, -3) + '.wasm';
|
||||
return url.href;
|
||||
}
|
||||
|
||||
async function initModule() {
|
||||
log(`config: ${JSON.stringify(CONFIG)}`);
|
||||
|
||||
state.storeservicesBytes = await fetchBytes(CONFIG.storeservicesUrl, 'libstoreservicescore.so');
|
||||
state.coreadiBytes = await fetchBytes(CONFIG.coreadiUrl, 'libCoreADI.so');
|
||||
|
||||
const moduleUrl = new URL(CONFIG.glueUrl, window.location.origin);
|
||||
moduleUrl.searchParams.set('v', CONFIG.assetVersion);
|
||||
const createModule = (await import(moduleUrl.href)).default;
|
||||
const wasmUrl = resolveWasmUrl(moduleUrl.href);
|
||||
log(`glue_url=${moduleUrl.href}`);
|
||||
log(`wasm_url=${wasmUrl}`);
|
||||
|
||||
const wasmRes = await fetch(wasmUrl, { cache: 'no-store' });
|
||||
assert(wasmRes.ok, `wasm fetch failed: HTTP ${wasmRes.status} (${wasmUrl})`);
|
||||
const wasmBinary = new Uint8Array(await wasmRes.arrayBuffer());
|
||||
assert(wasmBinary.length >= 8, `wasm too small: ${wasmBinary.length} bytes`);
|
||||
const magicOk =
|
||||
wasmBinary[0] === 0x00 &&
|
||||
wasmBinary[1] === 0x61 &&
|
||||
wasmBinary[2] === 0x73 &&
|
||||
wasmBinary[3] === 0x6d;
|
||||
if (!magicOk) {
|
||||
const head = Array.from(wasmBinary.slice(0, 8))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join(' ');
|
||||
throw new Error(`invalid wasm magic at ${wasmUrl}, first8=${head}`);
|
||||
}
|
||||
|
||||
state.module = await createModule({
|
||||
noInitialRun: true,
|
||||
wasmBinary,
|
||||
ENV: {
|
||||
RUST_BACKTRACE: CONFIG.rustBacktrace,
|
||||
RUST_LIB_BACKTRACE: CONFIG.rustLibBacktrace,
|
||||
},
|
||||
print: (msg) => log(`${msg}`),
|
||||
printErr: (msg) => log(`${msg}`),
|
||||
locateFile: (path) => {
|
||||
if (path.includes('.wasm')) {
|
||||
return wasmUrl;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
});
|
||||
|
||||
log('emscripten module instantiated');
|
||||
}
|
||||
|
||||
async function syncIdbfs(populate) {
|
||||
const FS = state.module.FS;
|
||||
await new Promise((resolve, reject) => {
|
||||
FS.syncfs(populate, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function initIdbfs() {
|
||||
const FS = state.module.FS;
|
||||
const IDBFS = FS.filesystems?.IDBFS;
|
||||
const mountPath = normalizeMountPath(CONFIG.libraryPath);
|
||||
|
||||
if (!IDBFS) {
|
||||
throw new Error('IDBFS unavailable on FS.filesystems');
|
||||
}
|
||||
|
||||
if (mountPath !== '/') {
|
||||
try {
|
||||
FS.mkdirTree(mountPath);
|
||||
} catch (_) {
|
||||
// ignore existing path
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
FS.mount(IDBFS, {}, mountPath);
|
||||
} catch (_) {
|
||||
// ignore already mounted
|
||||
}
|
||||
|
||||
await syncIdbfs(true);
|
||||
log(`idbfs mounted: ${mountPath}`);
|
||||
}
|
||||
|
||||
async function persistIdbfs() {
|
||||
await syncIdbfs(false);
|
||||
log('idbfs sync: flushed');
|
||||
}
|
||||
|
||||
// ===== HTTP Request helpers using libcurl =====
|
||||
|
||||
const USER_AGENT = 'akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0';
|
||||
|
||||
async function httpGet(url, extraHeaders = {}) {
|
||||
log(`GET ${url}`);
|
||||
const headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
...extraHeaders,
|
||||
};
|
||||
const resp = await libcurl.fetch(url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
redirect: 'manual',
|
||||
_libcurl_http_version: 1.1,
|
||||
insecure: true,
|
||||
});
|
||||
const body = await resp.text();
|
||||
log(`GET ${url} -> ${resp.status}`);
|
||||
return { status: resp.status, body };
|
||||
}
|
||||
|
||||
async function httpPost(url, data, extraHeaders = {}) {
|
||||
log(`POST ${url}`);
|
||||
const headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Connection': 'keep-alive',
|
||||
...extraHeaders,
|
||||
};
|
||||
const resp = await libcurl.fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: data,
|
||||
redirect: 'manual',
|
||||
insecure: true,
|
||||
_libcurl_http_version: 1.1,
|
||||
});
|
||||
const body = await resp.text();
|
||||
log(`POST ${url} -> ${resp.status}`);
|
||||
return { status: resp.status, body };
|
||||
}
|
||||
|
||||
// Simple plist parsing for the specific format we need
|
||||
function parsePlist(xmlText) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(xmlText, 'text/xml');
|
||||
|
||||
function parseNode(node) {
|
||||
if (!node) return null;
|
||||
|
||||
const tag = node.tagName;
|
||||
if (tag === 'dict') {
|
||||
const result = {};
|
||||
let key = null;
|
||||
for (const child of node.children) {
|
||||
if (child.tagName === 'key') {
|
||||
key = child.textContent;
|
||||
} else if (key !== null) {
|
||||
result[key] = parseNode(child);
|
||||
key = null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else if (tag === 'array') {
|
||||
return Array.from(node.children).map(parseNode);
|
||||
} else if (tag === 'string') {
|
||||
return node.textContent || '';
|
||||
} else if (tag === 'integer') {
|
||||
return parseInt(node.textContent, 10);
|
||||
} else if (tag === 'true') {
|
||||
return true;
|
||||
} else if (tag === 'false') {
|
||||
return false;
|
||||
} else if (tag === 'data') {
|
||||
// base64 encoded data
|
||||
const text = node.textContent || '';
|
||||
return text.replace(/\s/g, '');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const plist = doc.querySelector('plist > dict, plist > array');
|
||||
return parseNode(plist);
|
||||
}
|
||||
|
||||
// ===== Device Management =====
|
||||
|
||||
class Device {
|
||||
constructor() {
|
||||
this.uniqueDeviceIdentifier = '';
|
||||
this.serverFriendlyDescription = '';
|
||||
this.adiIdentifier = '';
|
||||
this.localUserUuid = '';
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
generate() {
|
||||
// Generate UUID
|
||||
this.uniqueDeviceIdentifier = crypto.randomUUID().toUpperCase();
|
||||
|
||||
// Pretend to be a MacBook Pro like in index.py
|
||||
this.serverFriendlyDescription = '<MacBookPro13,2> <macOS;13.1;22C65> <com.apple.AuthKit/1 (com.apple.dt.Xcode/3594.4.19)>';
|
||||
|
||||
// Generate 16 hex chars (8 bytes) for ADI identifier
|
||||
const bytes = new Uint8Array(8);
|
||||
crypto.getRandomValues(bytes);
|
||||
this.adiIdentifier = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
// Generate 64 hex chars (32 bytes) for local user UUID
|
||||
const luBytes = new Uint8Array(32);
|
||||
crypto.getRandomValues(luBytes);
|
||||
this.localUserUuid = Array.from(luBytes, b => b.toString(16).toUpperCase().padStart(2, '0')).join('');
|
||||
|
||||
this.initialized = true;
|
||||
log(`Device generated: UDID=${this.uniqueDeviceIdentifier}, ADI=${this.adiIdentifier}`);
|
||||
}
|
||||
}
|
||||
|
||||
function deviceFilePath() {
|
||||
const mountPath = normalizeMountPath(CONFIG.libraryPath);
|
||||
if (mountPath === '/') {
|
||||
return '/device.json';
|
||||
}
|
||||
return `${mountPath}/device.json`;
|
||||
}
|
||||
|
||||
function parseDeviceRecord(record) {
|
||||
if (!record || typeof record !== 'object') {
|
||||
return null;
|
||||
}
|
||||
const device = new Device();
|
||||
device.uniqueDeviceIdentifier = String(record.UUID || '');
|
||||
device.serverFriendlyDescription = String(record.clientInfo || '');
|
||||
device.adiIdentifier = String(record.identifier || '');
|
||||
device.localUserUuid = String(record.localUUID || '');
|
||||
device.initialized = Boolean(
|
||||
device.uniqueDeviceIdentifier &&
|
||||
device.serverFriendlyDescription &&
|
||||
device.adiIdentifier &&
|
||||
device.localUserUuid,
|
||||
);
|
||||
return device;
|
||||
}
|
||||
|
||||
function serializeDevice(device) {
|
||||
return {
|
||||
UUID: device.uniqueDeviceIdentifier,
|
||||
clientInfo: device.serverFriendlyDescription,
|
||||
identifier: device.adiIdentifier,
|
||||
localUUID: device.localUserUuid,
|
||||
};
|
||||
}
|
||||
|
||||
function readDeviceFromFs() {
|
||||
const FS = state.module.FS;
|
||||
const path = deviceFilePath();
|
||||
try {
|
||||
const text = FS.readFile(path, { encoding: 'utf8' });
|
||||
const parsed = JSON.parse(text);
|
||||
const device = parseDeviceRecord(parsed);
|
||||
if (!device || !device.initialized) {
|
||||
return null;
|
||||
}
|
||||
log(`Device loaded: UDID=${device.uniqueDeviceIdentifier}, ADI=${device.adiIdentifier}`);
|
||||
return device;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persistDevice(device) {
|
||||
const FS = state.module.FS;
|
||||
const path = deviceFilePath();
|
||||
const text = JSON.stringify(serializeDevice(device), null, 2);
|
||||
FS.writeFile(path, text);
|
||||
log(`Device persisted: ${path}`);
|
||||
}
|
||||
|
||||
function loadOrCreateDevice() {
|
||||
let device = readDeviceFromFs();
|
||||
let shouldPersist = false;
|
||||
|
||||
if (!device) {
|
||||
device = new Device();
|
||||
device.generate();
|
||||
shouldPersist = true;
|
||||
}
|
||||
|
||||
const override = (CONFIG.identifier || '').trim();
|
||||
if (override && override !== device.adiIdentifier) {
|
||||
device.adiIdentifier = override;
|
||||
device.initialized = true;
|
||||
shouldPersist = true;
|
||||
log(`Device identifier overridden: ${override}`);
|
||||
}
|
||||
|
||||
if (shouldPersist) {
|
||||
persistDevice(device);
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
// ===== Provisioning Session =====
|
||||
|
||||
class ProvisioningSession {
|
||||
constructor(device) {
|
||||
this.device = device;
|
||||
this.urlBag = {};
|
||||
}
|
||||
|
||||
getBaseHeaders() {
|
||||
return {
|
||||
'X-Mme-Device-Id': this.device.uniqueDeviceIdentifier,
|
||||
'X-MMe-Client-Info': this.device.serverFriendlyDescription,
|
||||
'X-Apple-I-MD-LU': this.device.localUserUuid,
|
||||
'X-Apple-Client-App-Name': 'Setup',
|
||||
};
|
||||
}
|
||||
|
||||
getClientTime() {
|
||||
// ISO format without milliseconds, like Python's isoformat()
|
||||
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
async loadUrlBag() {
|
||||
if (Object.keys(this.urlBag).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = 'https://gsa.apple.com/grandslam/GsService2/lookup';
|
||||
const { body } = await httpGet(url, this.getBaseHeaders());
|
||||
|
||||
const plist = parsePlist(body);
|
||||
if (plist && plist.urls) {
|
||||
this.urlBag = plist.urls;
|
||||
log(`URL bag loaded: ${Object.keys(this.urlBag).join(', ')}`);
|
||||
} else {
|
||||
throw new Error('Failed to parse URL bag');
|
||||
}
|
||||
}
|
||||
|
||||
async provision(dsId) {
|
||||
log('Starting provisioning...');
|
||||
|
||||
await this.loadUrlBag();
|
||||
|
||||
// Step 1: Start provisioning - get spim from Apple
|
||||
const startProvisioningPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Header</key>
|
||||
<dict/>
|
||||
<key>Request</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>`;
|
||||
|
||||
const extraHeadersStart = {
|
||||
...this.getBaseHeaders(),
|
||||
'X-Apple-I-Client-Time': this.getClientTime(),
|
||||
};
|
||||
|
||||
const { body: startBody } = await httpPost(
|
||||
this.urlBag.midStartProvisioning,
|
||||
startProvisioningPlist,
|
||||
extraHeadersStart
|
||||
);
|
||||
|
||||
const spimPlist = parsePlist(startBody);
|
||||
const spimStr = spimPlist?.Response?.spim;
|
||||
if (!spimStr) {
|
||||
throw new Error('Failed to get spim from start provisioning');
|
||||
}
|
||||
|
||||
const spim = base64ToBytes(spimStr);
|
||||
log(`Got spim: ${spim.length} bytes`);
|
||||
|
||||
// Step 2: Call ADI start_provisioning
|
||||
const cpim = await this.adiStartProvisioning(dsId, spim);
|
||||
log(`Got cpim: ${cpim.length} bytes`);
|
||||
|
||||
// Step 3: End provisioning - send cpim to Apple
|
||||
const endProvisioningPlist = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Header</key>
|
||||
<dict/>
|
||||
<key>Request</key>
|
||||
<dict>
|
||||
<key>cpim</key>
|
||||
<string>${bytesToBase64(cpim)}</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>`;
|
||||
|
||||
const extraHeadersEnd = {
|
||||
...this.getBaseHeaders(),
|
||||
'X-Apple-I-Client-Time': this.getClientTime(),
|
||||
};
|
||||
|
||||
const { body: endBody } = await httpPost(
|
||||
this.urlBag.midFinishProvisioning,
|
||||
endProvisioningPlist,
|
||||
extraHeadersEnd
|
||||
);
|
||||
|
||||
const endPlist = parsePlist(endBody);
|
||||
const response = endPlist?.Response;
|
||||
if (!response) {
|
||||
throw new Error('Failed to get response from end provisioning');
|
||||
}
|
||||
|
||||
const ptm = base64ToBytes(response.ptm);
|
||||
const tk = base64ToBytes(response.tk);
|
||||
log(`Got ptm: ${ptm.length} bytes, tk: ${tk.length} bytes`);
|
||||
|
||||
// Step 4: Call ADI end_provisioning
|
||||
await this.adiEndProvisioning(ptm, tk);
|
||||
log('Provisioning completed successfully');
|
||||
}
|
||||
|
||||
async adiStartProvisioning(dsId, spim) {
|
||||
const pSpim = allocBytes(spim);
|
||||
const startFn = ensureExport('_anisette_start_provisioning');
|
||||
const start = call('anisette_start_provisioning', () =>
|
||||
Number(startFn(dsId, pSpim, spim.length)),
|
||||
);
|
||||
|
||||
if (start.ret !== 0) {
|
||||
throw new Error(`start_provisioning failed: ${start.err || 'unknown error'}`);
|
||||
}
|
||||
|
||||
const getCpimPtr = ensureExport('_anisette_get_cpim_ptr');
|
||||
const getCpimLen = ensureExport('_anisette_get_cpim_len');
|
||||
const getSession = ensureExport('_anisette_get_session');
|
||||
|
||||
const cpimPtr = Number(getCpimPtr());
|
||||
const cpimLen = Number(getCpimLen());
|
||||
state.session = Number(getSession());
|
||||
|
||||
return readBytes(cpimPtr, cpimLen);
|
||||
}
|
||||
|
||||
async adiEndProvisioning(ptm, tk) {
|
||||
const pPtm = allocBytes(ptm);
|
||||
const pTk = allocBytes(tk);
|
||||
const endFn = ensureExport('_anisette_end_provisioning');
|
||||
const end = call('anisette_end_provisioning', () =>
|
||||
Number(endFn(state.session, pPtm, ptm.length, pTk, tk.length)),
|
||||
);
|
||||
|
||||
if (end.ret !== 0) {
|
||||
throw new Error(`end_provisioning failed: ${end.err || 'unknown error'}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Main Flow =====
|
||||
|
||||
async function initAnisette(identifier) {
|
||||
const pStores = allocBytes(state.storeservicesBytes);
|
||||
const pCore = allocBytes(state.coreadiBytes);
|
||||
const pLibrary = allocCString(CONFIG.libraryPath);
|
||||
const pProvisioning = allocCString(CONFIG.provisioningPath);
|
||||
const pIdentifier = allocCString(identifier);
|
||||
|
||||
const initFromBlobs = ensureExport('_anisette_init_from_blobs');
|
||||
const init = call('anisette_init_from_blobs', () =>
|
||||
Number(
|
||||
initFromBlobs(
|
||||
pStores,
|
||||
state.storeservicesBytes.length,
|
||||
pCore,
|
||||
state.coreadiBytes.length,
|
||||
pLibrary,
|
||||
pProvisioning,
|
||||
pIdentifier,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (init.ret !== 0) {
|
||||
throw new Error('init failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function isMachineProvisioned(dsId) {
|
||||
const isProvisionedFn = ensureExport('_anisette_is_machine_provisioned');
|
||||
const provisioned = call('anisette_is_machine_provisioned', () =>
|
||||
Number(isProvisionedFn(dsId)),
|
||||
);
|
||||
return provisioned.ret !== 0; // ret === 0 means provisioned (no error)
|
||||
}
|
||||
|
||||
async function requestOtp(dsId) {
|
||||
const otpFn = ensureExport('_anisette_request_otp');
|
||||
const otp = call('anisette_request_otp', () => Number(otpFn(dsId)));
|
||||
if (otp.ret !== 0) {
|
||||
throw new Error(`request_otp failed: ${otp.err || 'unknown error'}`);
|
||||
}
|
||||
|
||||
const getOtpPtr = ensureExport('_anisette_get_otp_ptr');
|
||||
const getOtpLen = ensureExport('_anisette_get_otp_len');
|
||||
const getMidPtr = ensureExport('_anisette_get_mid_ptr');
|
||||
const getMidLen = ensureExport('_anisette_get_mid_len');
|
||||
|
||||
const otpPtr = Number(getOtpPtr());
|
||||
const otpLen = Number(getOtpLen());
|
||||
const midPtr = Number(getMidPtr());
|
||||
const midLen = Number(getMidLen());
|
||||
|
||||
const otpBytes = readBytes(otpPtr, otpLen);
|
||||
const midBytes = readBytes(midPtr, midLen);
|
||||
|
||||
return {
|
||||
oneTimePassword: otpBytes,
|
||||
machineIdentifier: midBytes,
|
||||
};
|
||||
}
|
||||
|
||||
async function initLibcurl() {
|
||||
log('initializing libcurl...');
|
||||
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
libcurl.set_websocket(`${wsProto}//${location.host}/wisp/`);
|
||||
|
||||
// Capture libcurl verbose output
|
||||
libcurl.stderr = (text) => {
|
||||
log(`[libcurl] ${text}`);
|
||||
};
|
||||
|
||||
await libcurl.load_wasm('./libcurl.wasm');
|
||||
|
||||
// // Get default CA certs and append Apple CA certs
|
||||
// const defaultCacert = libcurl.get_cacert();
|
||||
// const extendedCacert = defaultCacert + '\n' + APPLE_CA_CERTS;
|
||||
|
||||
// Create a file with extended CA certs in the Emscripten FS
|
||||
|
||||
log('libcurl initialized');
|
||||
}
|
||||
|
||||
|
||||
function dumpFs(path = '/') {
|
||||
const FS = state.module.FS;
|
||||
const entries = FS.readdir(path).filter((name) => name !== '.' && name !== '..');
|
||||
for (const name of entries) {
|
||||
const full = path === '/' ? `/${name}` : `${path}/${name}`;
|
||||
const stat = FS.stat(full);
|
||||
if (FS.isDir(stat.mode)) {
|
||||
console.log(`dir ${full}`);
|
||||
dumpFs(full);
|
||||
} else {
|
||||
const data = FS.readFile(full);
|
||||
console.log(`file ${full} size=${data.length}`);
|
||||
// 如果要看内容(文本)
|
||||
// console.log(new TextDecoder().decode(data));
|
||||
// base64
|
||||
function bytesToHex(bytes) {
|
||||
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(' ');
|
||||
}
|
||||
console.log(bytesToBase64(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Initialize libcurl for HTTP requests
|
||||
await initLibcurl();
|
||||
|
||||
// Load WASM module
|
||||
await initModule();
|
||||
|
||||
// Initialize IDBFS
|
||||
await initIdbfs();
|
||||
|
||||
// Load device info or generate new one
|
||||
const device = loadOrCreateDevice();
|
||||
|
||||
// Initialize anisette with device identifier
|
||||
await initAnisette(device.adiIdentifier);
|
||||
|
||||
const dsid = dsidToU64(CONFIG.dsid);
|
||||
|
||||
// Check if machine is provisioned
|
||||
const isProvisioned = await isMachineProvisioned(dsid);
|
||||
|
||||
if (!isProvisioned) {
|
||||
log('Machine not provisioned, starting provisioning...');
|
||||
const session = new ProvisioningSession(device);
|
||||
await session.provision(dsid);
|
||||
} else {
|
||||
log('Machine already provisioned');
|
||||
}
|
||||
|
||||
// Request OTP
|
||||
log('Requesting OTP...');
|
||||
const otp = await requestOtp(dsid);
|
||||
|
||||
// Output the result
|
||||
const result = {
|
||||
'X-Apple-I-MD': bytesToBase64(otp.oneTimePassword),
|
||||
'X-Apple-I-MD-M': bytesToBase64(otp.machineIdentifier),
|
||||
};
|
||||
log(`OTP result: ${JSON.stringify(result, null, 2)}`);
|
||||
|
||||
// Persist IDBFS
|
||||
await persistIdbfs();
|
||||
// dumpFs("/anisette/");
|
||||
log('done');
|
||||
} catch (e) {
|
||||
log(`fatal: ${String(e)}`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
260
example/run-node.mjs
Normal file
260
example/run-node.mjs
Normal file
@@ -0,0 +1,260 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const distDir = path.join(__dirname, 'dist');
|
||||
const modulePath = path.join(distDir, 'anisette_rs.node.js');
|
||||
const wasmPath = path.join(distDir, 'anisette_rs.node.wasm');
|
||||
|
||||
function usage() {
|
||||
console.log('usage: bun test/run-node.mjs <libstoreservicescore.so> <libCoreADI.so> [library_path] [dsid] [identifier] [trace_window_start]');
|
||||
console.log('note: library_path should contain adi.pb/device.json when available');
|
||||
}
|
||||
|
||||
function allocBytes(module, bytes) {
|
||||
const ptr = module._malloc(bytes.length);
|
||||
module.HEAPU8.set(bytes, ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function allocCString(module, value) {
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
const size = module.lengthBytesUTF8(value) + 1;
|
||||
const ptr = module._malloc(size);
|
||||
module.stringToUTF8(value, ptr, size);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function readLastError(module) {
|
||||
const ptr = module._anisette_last_error_ptr();
|
||||
const len = module._anisette_last_error_len();
|
||||
if (!ptr || !len) {
|
||||
return '';
|
||||
}
|
||||
const bytes = module.HEAPU8.subarray(ptr, ptr + len);
|
||||
return new TextDecoder('utf-8').decode(bytes);
|
||||
}
|
||||
|
||||
function normalizeLibraryRoot(input) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return '.';
|
||||
}
|
||||
const normalized = trimmed.replace(/\/+$/, '');
|
||||
return normalized || '.';
|
||||
}
|
||||
|
||||
function ensureTrailingSlash(input) {
|
||||
if (!input) {
|
||||
return './';
|
||||
}
|
||||
return input.endsWith('/') ? input : `${input}/`;
|
||||
}
|
||||
|
||||
function joinLibraryFile(root, fileName) {
|
||||
if (root === '/') {
|
||||
return `/${fileName}`;
|
||||
}
|
||||
if (root.endsWith('/')) {
|
||||
return `${root}${fileName}`;
|
||||
}
|
||||
return `${root}/${fileName}`;
|
||||
}
|
||||
|
||||
function writeVirtualFile(module, filePath, buffer) {
|
||||
const pathPtr = allocCString(module, filePath);
|
||||
const dataPtr = allocBytes(module, buffer);
|
||||
const result = module._anisette_fs_write_file(pathPtr, dataPtr, buffer.length);
|
||||
module._free(pathPtr);
|
||||
module._free(dataPtr);
|
||||
if (result !== 0) {
|
||||
const message = readLastError(module);
|
||||
throw new Error(message || 'virtual fs write failed');
|
||||
}
|
||||
}
|
||||
|
||||
function readBytes(module, ptr, len) {
|
||||
if (!ptr || !len) {
|
||||
return new Uint8Array();
|
||||
}
|
||||
return module.HEAPU8.slice(ptr, ptr + len);
|
||||
}
|
||||
|
||||
function toBase64(bytes) {
|
||||
if (!bytes.length) {
|
||||
return '';
|
||||
}
|
||||
return Buffer.from(bytes).toString('base64');
|
||||
}
|
||||
|
||||
function toAppleClientTime(date = new Date()) {
|
||||
return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
function detectAppleLocale() {
|
||||
const locale = Intl.DateTimeFormat().resolvedOptions().locale || 'en-US';
|
||||
return locale.replace('-', '_');
|
||||
}
|
||||
|
||||
async function fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const defaultStoreservicesPath = path.join(__dirname, 'arm64-v8a', 'libstoreservicescore.so');
|
||||
const defaultCoreadiPath = path.join(__dirname, 'arm64-v8a', 'libCoreADI.so');
|
||||
|
||||
const storeservicesPath = args[0] ?? defaultStoreservicesPath;
|
||||
const coreadiPath = args[1] ?? defaultCoreadiPath;
|
||||
const libraryPath = args[2] ?? './anisette/';
|
||||
const dsidRaw = args[3] ?? '-2';
|
||||
let identifier = args[4] ?? '';
|
||||
const traceWindowStartRaw = args[5] ?? '0';
|
||||
const silent = process.env.ANISETTE_SILENT === '1';
|
||||
|
||||
if (!(await fileExists(storeservicesPath)) || !(await fileExists(coreadiPath))) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const libraryRoot = normalizeLibraryRoot(libraryPath);
|
||||
const libraryArg = ensureTrailingSlash(libraryRoot);
|
||||
const resolvedLibraryPath = path.resolve(libraryRoot);
|
||||
const devicePath = path.join(resolvedLibraryPath, 'device.json');
|
||||
const adiPath = path.join(resolvedLibraryPath, 'adi.pb');
|
||||
let deviceData = null;
|
||||
|
||||
if (await fileExists(devicePath)) {
|
||||
try {
|
||||
deviceData = JSON.parse(await fs.readFile(devicePath, 'utf8'));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!identifier && deviceData) {
|
||||
try {
|
||||
if (deviceData && typeof deviceData.identifier === 'string' && deviceData.identifier) {
|
||||
identifier = deviceData.identifier;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const moduleFactory = (await import(pathToFileURL(modulePath).href)).default;
|
||||
const module = await moduleFactory({
|
||||
locateFile(file) {
|
||||
if (file.endsWith('.wasm')) {
|
||||
return wasmPath;
|
||||
}
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
const storeservices = await fs.readFile(storeservicesPath);
|
||||
const coreadi = await fs.readFile(coreadiPath);
|
||||
if (await fileExists(adiPath)) {
|
||||
const adiData = await fs.readFile(adiPath);
|
||||
try {
|
||||
writeVirtualFile(module, joinLibraryFile(libraryRoot, 'adi.pb'), adiData);
|
||||
} catch (err) {
|
||||
console.error('anisette_fs_write_file failed:', err.message || err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (await fileExists(devicePath)) {
|
||||
const deviceData = await fs.readFile(devicePath);
|
||||
try {
|
||||
writeVirtualFile(module, joinLibraryFile(libraryRoot, 'device.json'), deviceData);
|
||||
} catch (err) {
|
||||
console.error('anisette_fs_write_file failed:', err.message || err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const storeservicesPtr = allocBytes(module, storeservices);
|
||||
const coreadiPtr = allocBytes(module, coreadi);
|
||||
const libraryPtr = allocCString(module, libraryArg);
|
||||
const provisioningPtr = allocCString(module, libraryArg);
|
||||
const identifierPtr = allocCString(module, identifier);
|
||||
|
||||
const initResult = module._anisette_init_from_blobs(
|
||||
storeservicesPtr,
|
||||
storeservices.length,
|
||||
coreadiPtr,
|
||||
coreadi.length,
|
||||
libraryPtr,
|
||||
provisioningPtr,
|
||||
identifierPtr
|
||||
);
|
||||
|
||||
module._free(storeservicesPtr);
|
||||
module._free(coreadiPtr);
|
||||
if (libraryPtr) {
|
||||
module._free(libraryPtr);
|
||||
}
|
||||
if (provisioningPtr) {
|
||||
module._free(provisioningPtr);
|
||||
}
|
||||
if (identifierPtr) {
|
||||
module._free(identifierPtr);
|
||||
}
|
||||
|
||||
if (initResult !== 0) {
|
||||
console.error('anisette_init_from_blobs failed:', readLastError(module));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const traceWindowStart = BigInt(traceWindowStartRaw);
|
||||
if (traceWindowStart > 0n && typeof module._anisette_set_trace_window_start === 'function') {
|
||||
const traceResult = module._anisette_set_trace_window_start(traceWindowStart);
|
||||
if (traceResult !== 0) {
|
||||
console.error('anisette_set_trace_window_start failed:', readLastError(module));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const dsid = BigInt(dsidRaw);
|
||||
const provisioned = module._anisette_is_machine_provisioned(dsid);
|
||||
if (provisioned < 0) {
|
||||
console.error('anisette_is_machine_provisioned failed:', readLastError(module));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (provisioned !== 1 && !silent) {
|
||||
console.warn('device not provisioned, request_otp may fail');
|
||||
}
|
||||
|
||||
const otpResult = module._anisette_request_otp(dsid);
|
||||
if (otpResult !== 0) {
|
||||
console.error('anisette_request_otp failed:', readLastError(module));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const otpBytes = readBytes(module, module._anisette_get_otp_ptr(), module._anisette_get_otp_len());
|
||||
const midBytes = readBytes(module, module._anisette_get_mid_ptr(), module._anisette_get_mid_len());
|
||||
const localUserUuid = (deviceData && typeof deviceData.localUUID === 'string') ? deviceData.localUUID : '';
|
||||
const mdLu = process.env.ANISETTE_MD_LU_BASE64 === '1'
|
||||
? Buffer.from(localUserUuid, 'utf8').toString('base64')
|
||||
: localUserUuid;
|
||||
const headers = {
|
||||
'X-Apple-I-Client-Time': toAppleClientTime(),
|
||||
'X-Apple-I-MD': toBase64(otpBytes),
|
||||
'X-Apple-I-MD-LU': mdLu,
|
||||
'X-Apple-I-MD-M': toBase64(midBytes),
|
||||
'X-Apple-I-MD-RINFO': process.env.ANISETTE_MD_RINFO ?? '17106176',
|
||||
'X-Apple-I-SRL-NO': process.env.ANISETTE_SRL_NO ?? '0',
|
||||
'X-Apple-I-TimeZone': process.env.ANISETTE_TIMEZONE ?? 'UTC',
|
||||
'X-Apple-Locale': process.env.ANISETTE_LOCALE ?? detectAppleLocale(),
|
||||
'X-MMe-Client-Info': (deviceData && typeof deviceData.clientInfo === 'string') ? deviceData.clientInfo : '',
|
||||
'X-Mme-Device-Id': (deviceData && typeof deviceData.UUID === 'string') ? deviceData.UUID : ''
|
||||
};
|
||||
|
||||
if (!silent) {
|
||||
console.log(JSON.stringify(headers, null, 2));
|
||||
}
|
||||
Reference in New Issue
Block a user