From d05cc41660c975edcebd2cda7ddb7a85d5287cc5 Mon Sep 17 00:00:00 2001 From: libr Date: Sat, 28 Feb 2026 00:36:15 +0800 Subject: [PATCH] feat: Implement Anisette JS/TS API with WASM support - Added main Anisette class for high-level API. - Introduced device management with Device class. - Created HTTP client abstraction for network requests. - Implemented provisioning session handling with ProvisioningSession class. - Added utility functions for encoding, decoding, and random generation. - Established library management with LibraryStore class. - Integrated WASM loading and bridging with WasmBridge. - Defined core types and interfaces for the API. - Set up TypeScript configuration and build scripts. - Updated package.json for new build and run commands. - Added bun.lock and package.json for JS dependencies. - Enhanced error handling and memory management in Rust code. --- .gitignore | 4 + Cargo.lock | 1916 ++++++++++++++++++++++++++++++++++++++ README.md | 138 ++- example/anisette-api.mjs | 52 ++ example/index.py | 1454 +++++++++++++++++++++++++++++ example/run-node.mjs | 2 +- js/bun.lock | 15 + js/package.json | 22 + js/src/anisette.ts | 213 +++++ js/src/device.ts | 64 ++ js/src/http.ts | 32 + js/src/index.ts | 17 + js/src/library.ts | 40 + js/src/provisioning.ts | 159 ++++ js/src/types.ts | 44 + js/src/utils.ts | 91 ++ js/src/wasm-bridge.ts | 202 ++++ js/src/wasm-loader.ts | 27 + js/tsconfig.json | 18 + package.json | 5 +- script/build-glue.sh | 12 + src/debug.rs | 12 +- 22 files changed, 4520 insertions(+), 19 deletions(-) create mode 100644 Cargo.lock create mode 100644 example/anisette-api.mjs create mode 100644 example/index.py create mode 100644 js/bun.lock create mode 100644 js/package.json create mode 100644 js/src/anisette.ts create mode 100644 js/src/device.ts create mode 100644 js/src/http.ts create mode 100644 js/src/index.ts create mode 100644 js/src/library.ts create mode 100644 js/src/provisioning.ts create mode 100644 js/src/types.ts create mode 100644 js/src/utils.ts create mode 100644 js/src/wasm-bridge.ts create mode 100644 js/src/wasm-loader.ts create mode 100644 js/tsconfig.json diff --git a/.gitignore b/.gitignore index 1d4f0da..eafce3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ target/ test/ dist/ +cache/ +*.apk +anisette/* +js/node_modules/* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..28d14cb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1916 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anisette-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "chrono", + "goblin", + "plist", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "thiserror", + "unicorn-engine", + "uuid", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "goblin" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scroll" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unicorn-engine" +version = "2.1.3" +dependencies = [ + "bitflags", + "cc", + "cmake", + "libc", + "pkg-config", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.113" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/README.md b/README.md index 37af0d5..01d8298 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,134 @@ -# anisette.js +# anisette-js -use anisette on browser locally! no more third-party server worries. +Apple Anisette authentication in browser via WebAssembly. Emulates ARM64 Android binaries to generate Anisette headers locally — no third-party servers required. -## usage +## Features -see examples/ +- **Local execution**: All computation happens in your browser or Node.js process +- **WASM-based**: Uses Unicorn Engine compiled to WebAssembly for ARM64 emulation +- **High-level JS/TS API**: Simple async interface, handles provisioning automatically +- **Single-file bundle**: Distribute as one `.js` + one `.wasm` file -should download apple music apk from [here](https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk) and unzip to get arm64 abi. +## Prerequisites + +- Rust nightly (for building the WASM module) +- Emscripten SDK +- Bun (for bundling the TypeScript API) + +Android library blobs (`libstoreservicescore.so`, `libCoreADI.so`) are not included. Extract them from an Apple Music APK or obtain separately. + +## Build + +```bash +# Clone and build custom Unicorn fork +git clone https://github.com/lbr77/unicorn.git +cd unicorn && git checkout tci-emscripten + +# Build everything (WASM + TS API bundle) +bash script/build-glue.sh + +# Or build just the JS bundle (WASM already built) +npm run build:js +``` + +Output files in `dist/`: +- `anisette.js` — bundled TS API + glue (single file) +- `anisette_rs.node.wasm` — WASM binary (required alongside `.js`) + +## Usage + +### Node.js + +```javascript +import { Anisette, loadWasm } from "./dist/anisette.js"; +import fs from "node:fs/promises"; + +const wasmModule = await loadWasm(); + +const storeservices = new Uint8Array(await fs.readFile("libstoreservicescore.so")); +const coreadi = new Uint8Array(await fs.readFile("libCoreADI.so")); + +const anisette = await Anisette.fromSo(storeservices, coreadi, wasmModule); + +if (!anisette.isProvisioned) { + await anisette.provision(); +} + +const headers = await anisette.getData(); +console.log(headers["X-Apple-I-MD"]); +``` + +Run the example: + +```bash +node example/anisette-api.mjs libstoreservicescore.so libCoreADI.so ./anisette/ +``` + +### Browser + +For browser usage, use the web-targeted WASM build (`anisette_rs.js` / `.wasm`) and import directly: + +```javascript +import ModuleFactory from "./anisette_rs.js"; + +const wasmModule = await ModuleFactory({ + locateFile: (f) => f.endsWith(".wasm") ? "./anisette_rs.wasm" : f +}); + +// Use WasmBridge for low-level access, or wrap with the TS API +``` + +## API Reference + +### `Anisette` + +Main class for generating Anisette headers. + +**Static methods:** + +- `Anisette.fromSo(storeservicescore, coreadi, wasmModule, options?)` — Initialize from library blobs +- `Anisette.fromSaved(ss, ca, deviceJson, adiPb, wasmModule, options?)` — Restore a saved session + +**Instance properties:** + +- `isProvisioned: boolean` — Whether the device is provisioned + +**Instance methods:** + +- `provision()` — Run Apple provisioning flow +- `getData(): Promise` — Generate Anisette headers +- `getDeviceJson(): Uint8Array` — Serialize device config for persistence + +### `loadWasm()` + +Loads the WASM module. In Node.js, resolves `.wasm` path relative to the bundle location. + +```javascript +import { loadWasm } from "./dist/anisette.js"; +const wasmModule = await loadWasm(); +``` + +## Architecture + +- **Rust/WASM core** (`src/`): Emulator, ADI wrapper, provisioning protocol +- **TypeScript API** (`js/src/`): High-level wrapper around WASM exports +- **Emscripten glue**: Bridges JS and WASM memory, handles VFS + +Key modules: +- `adi.rs` — ADI (Apple Device Identity) provisioning and OTP +- `emu.rs` — Unicorn-based ARM64 emulator +- `exports.rs` — C FFI exports for WASM +- `js/src/anisette.ts` — Main `Anisette` class +- `js/src/wasm-bridge.ts` — Low-level WASM memory management + +## Credits + +- [pyprovision-uc](https://github.com/JayFoxRox/pyprovision-uc) +- [Anisette.py](https://github.com/malmeloo/Anisette.py) +- [omnisette-server](https://github.com/SideStore/omnisette-server) +- [unicorn](https://github.com/petabyt/unicorn/tree/tci-emscripten) -## credits +## Known Issue: -[pyprovision-uc](https://github.com/JayFoxRox/pyprovision-uc) - -[Anisette.py](https://github.com/malmeloo/Anisette.py) - -[omnisette-server](https://github.com/SideStore/omnisette-server) +when requiring Otp for second time there will be a "WRITE UNMAPPED" error which could be avoided by initalizing onemoretime... \ No newline at end of file diff --git a/example/anisette-api.mjs b/example/anisette-api.mjs new file mode 100644 index 0000000..5e1d031 --- /dev/null +++ b/example/anisette-api.mjs @@ -0,0 +1,52 @@ +/** + * Example: using the high-level Anisette JS API (Node.js) + * + * Usage: + * node example/anisette-api.mjs [library_path] + */ + +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 bundlePath = path.join(__dirname, "..", "dist", "anisette.js"); + +const { Anisette, loadWasm } = await import( + pathToFileURL(bundlePath).href +).catch(() => { + console.error("Bundle not found. Run: npm run build:js"); + process.exit(1); +}); + +const args = process.argv.slice(2); +if (args.length < 2) { + console.error( + "usage: node example/anisette-api.mjs [library_path]" + ); + process.exit(1); +} + +const storeservicesPath = args[0]; +const coreadiPath = args[1]; +const libraryPath = args[2] ?? "./anisette/"; + +const wasmModule = await loadWasm(); + +const storeservices = new Uint8Array(await fs.readFile(storeservicesPath)); +const coreadi = new Uint8Array(await fs.readFile(coreadiPath)); + +const anisette = await Anisette.fromSo(storeservices, coreadi, wasmModule, { + init: { libraryPath }, +}); + +if (!anisette.isProvisioned) { + console.log("Device not provisioned — running provisioning..."); + await anisette.provision(); + console.log("Provisioning complete."); +} else { + console.log("Device already provisioned."); +} + +const headers = await anisette.getData(); +console.log(JSON.stringify(headers, null, 2)); diff --git a/example/index.py b/example/index.py new file mode 100644 index 0000000..27f03e8 --- /dev/null +++ b/example/index.py @@ -0,0 +1,1454 @@ +import math +import os +import time +from zipfile import ZipFile +import requests +import json +import sys +import plistlib +from io import BytesIO +import base64 +import datetime +from ctypes import * + +from elftools.elf.elffile import ELFFile +from elftools.elf.relocation import RelocationSection +from elftools.elf.sections import SymbolTableSection + +from unicorn import * +from unicorn.arm64_const import * + +enableCache = False + +returnAddress = 0xDEAD0000 +stackAddress = 0xF0000000 +stackSize = 0x100000 + +mallocAddress = 0x60000000 +mallocSize = 0x1000000 + +importAddress = 0xA0000000 +importSize = 0x1000 + +#FIXME: Define pageSize + +# def debugPrint(message): +# if False: +# print(message) +debugPrint = print + +def debugTrace(message): + if False: + print(message) + +def hook_mem_invalid(uc, access, address, size, value, user_data): + vm = user_data + assert(vm.uc == uc) + + if access == UC_MEM_WRITE_UNMAPPED: + debugPrint(">>> Missing memory is being WRITE at 0x%x, data size = %u, data value = 0x%x" \ + %(address, size, value)) + # return True to indicate we want to continue emulation + #return False + elif access == UC_MEM_FETCH_UNMAPPED: + debugPrint(">>> Missing memory is being FETCH at 0x%x, data size = %u, data value = 0x%x" \ + %(address, size, value)) + else: + # return False to indicate we want to stop emulation + #return False + pass + assert(False) + + +def hook_code(uc, address, size, user_data): + vm = user_data + assert(vm.uc == uc) + + debugPrint(">>> Tracing at 0x%X:" % (address), end="") + # read this instruction code from memory + tmp = uc.mem_read(address, size) + for i in tmp: + debugPrint(" %02X" %i, end="") + for i in [3, 8, 9, 10, 11, 20]: + value = uc.reg_read(UC_ARM64_REG_X0 + i) + debugPrint("; X%d: 0x%08X" % (i, value), end="") + debugPrint("; W13=0x%X" % uc.reg_read(UC_ARM64_REG_W13), end="") + debugPrint("; W14=0x%X" % uc.reg_read(UC_ARM64_REG_W14), end="") + debugPrint("; W15=0x%X" % uc.reg_read(UC_ARM64_REG_W15), end="") + debugPrint("; FP/X29=0x%X" % uc.reg_read(UC_ARM64_REG_FP), end="") + #print("; *347c40=0x%08X" % int.from_bytes(uc.mem_read(0x347c40, 4), 'little'), end="") + debugPrint("") + +def hook_block(uc, address, size, user_data): + vm = user_data + assert(vm.uc == uc) + + pass #print(" >>> Tracing basic block at 0x%x, block size = 0x%x" %(address, size)) + +def hook_stub(uc, address, size, user_data): + vm = user_data + assert(vm.uc == uc) + + assert(address >= importAddress) + assert(address < importAddress + 0x01000000 * 10) + + offset = address - importAddress + libraryIndex = offset // 0x01000000 + symbolIndex = (offset % 0x01000000) // 4 + + #assert(libraryIndex == 0) + library = vm.loadedLibraries[libraryIndex] + + symbolName = symbolNameByIndex(library, symbolIndex) + + lr = uc.reg_read(UC_ARM64_REG_LR) + + #print("stub", "0x%X" % lr, uc, address, size, user_data, end=" :: ") + #print(libraryIndex, library.name, symbolIndex, symbolName) + + if symbolName in stubbedFunctions: + stubbedFunctions[symbolName](vm) + #assert(False) + else: + debugPrint(symbolName) + assert(False) + + #time.sleep(0.1) + return True + + +class Vm(): + def __init__(self, uc): + self.uc = uc + self.loadedLibraries = [] + self.tempAllocator = Allocator(0x800000000, 0x10000000) + self.libraryAllocator = Allocator(0x00100000, 0x90000000) + +#FIXME: Hide these functions or move them to a separate package; they are only used internally +def createVm(): + # Startup a unicorn-engine instance as VM backend + if arch == "x86": + uc = Uc(UC_ARCH_X86, UC_MODE_32) + elif arch == "x86_64": + uc = Uc(UC_ARCH_X86, UC_MODE_64) + elif arch == "armeabi-v7a": + uc = Uc(UC_ARCH_ARM, UC_MODE_ARM) + elif arch == "arm64-v8a": + uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM) + else: + assert(False) + + # Register a fake return address + uc.mem_map(returnAddress, 0x1000) + + # Register some memory for malloc + uc.mem_map(mallocAddress, mallocSize) + + # Register a fake stack + uc.mem_map(stackAddress, stackSize) + + vm = Vm(uc) + + # Debug hooks + uc.hook_add(UC_HOOK_BLOCK, hook_block, vm) + #uc.hook_add(UC_HOOK_CODE, hook_code, vm) + uc.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED | UC_HOOK_MEM_FETCH_UNMAPPED, hook_mem_invalid, vm) + + # Add a region for imports + importCount = importSize // 4 + for i in range(10): + libraryImportAddress = importAddress + i * 0x01000000 + uc.mem_map(libraryImportAddress, importSize) + uc.mem_write(libraryImportAddress, b'\xc0\x03\x5f\xd6' * importCount) # RET instruction + uc.hook_add(UC_HOOK_CODE, hook_stub, vm, libraryImportAddress, libraryImportAddress + importSize - 1) + + + return vm + + +class Allocator(): + + def __init__(self, base, size): + self.__base = base + self.__size = size + self.__offset = 0 + + def alloc(self, size): + address = self.__base + self.__offset + + # Align to pagesize bytes + length = size + length += 0xFFF + length &= ~0xFFF + + self.__offset += length + assert(self.__offset < self.__base + self.__size) + + return address + +def roundUp(size, pageSize): + alignedSize = size + alignedSize += pageSize - 1 + alignedSize &= ~(pageSize - 1) + paddingSize = alignedSize - size + return alignedSize, paddingSize + +def allocData(vm, data): + uc = vm.uc + + length, paddingSize = roundUp(len(data), 0x1000) + address = vm.tempAllocator.alloc(length) + + debugPrint("Allocating at 0x%X; bytes 0x%X/0x%X" % (address, len(data), length)) + uc.mem_map(address, length) + uc.mem_write(address, data + b'\xCC' * paddingSize) + + return address + +def allocTemporary(vm, length): + return allocData(vm, b'\xAA' * length) + +def invoke_cdecl(vm, address, args): + uc = vm.uc + lr = returnAddress + for i, value in enumerate(args): + assert(i <= 28) + uc.reg_write(UC_ARM64_REG_X0 + i, value) + debugPrint("X%d: 0x%08X" % (i, value)) + debugPrint("Calling 0x%X" % address) + uc.reg_write(UC_ARM64_REG_SP, stackAddress + stackSize) + uc.reg_write(UC_ARM64_REG_LR, lr) + #uc.reg_write(UC_ARM64_REG_FP, stackAddress + stackSize) + uc.emu_start(address, lr) + x0 = uc.reg_read(UC_ARM64_REG_X0) + return x0 + + + +#FIXME: Move into a separate function +#FIXME: Download this file +# Development was done on https://web.archive.org/web/20231226115856/https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk +#FIXME: I attempted to do partial downloads, but unfortunately we can't download just the ZIP footer from the file. +# While the server does range-requests, it only allows gzip encoding and then we are missing some data to decompress. +if False: + url = "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk" + res = requests.get(url, headers={ + "Accept-Encoding": "gzip", # "identity" here, will freeze the transfer + # "Cache-Control": "max-age=0", # We don't want a gzipped response + # "Range": "bytes=0-500", + }, stream=True) + data = res.raw.read() + open("tmp.apk", "wb").write(data) + #print(data.hex()) + #assert(False) + + +#arch = "armeabi-v7a" +arch = "arm64-v8a" +#arch = "x86_64" +#arch = "x86" + +files = {} +libraryNames = [ + "libstoreservicescore.so", + "libCoreADI.so", +] +with ZipFile('applemusic.apk') as apk: + for libraryName in libraryNames: + files[libraryName] = apk.read("lib/" + arch + "/" + libraryName) + + + + +class ClientProvisioningIntermediateMetadata(): + def __init__(self, adiInstance, cpim, session): + self.adi = adiInstance + self.client_provisioning_intermediate_metadata = cpim + self.session = session + +class OneTimePassword(): + def __init__(self, adiInstance, oneTimePassword, machineIdentifier): + self.adi = adiInstance + self.one_time_password = oneTimePassword + self.machine_identifier = machineIdentifier + +def write_u64(vm, address, value): + return write_data(vm, address, int.to_bytes(value, 8, 'little', signed=False)) +def write_u32(vm, address, value): + return write_data(vm, address, int.to_bytes(value, 4, 'little', signed=False)) +def read_u64(vm, address): + return int.from_bytes(read_data(vm, address, 8), 'little', signed=False) +def read_u32(vm, address): + return int.from_bytes(read_data(vm, address, 4), 'little', signed=False) + + + +def uTo_s32(value): + bytes = int.to_bytes(value, 4, 'little', signed=False) + return int.from_bytes(bytes, 'little', signed=True) + +def uTo_s64(value): + bytes = int.to_bytes(value, 8, 'little', signed=False) + return int.from_bytes(bytes, 'little', signed=True) + +def sTo_u32(value): + bytes = int.to_bytes(value, 4, 'little', signed=True) + return int.from_bytes(bytes, 'little', signed=False) + +def sTo_u64(value): + bytes = int.to_bytes(value, 8, 'little', signed=True) + return int.from_bytes(bytes, 'little', signed=False) + + + + +class ADI(): + def __init__(self, libraryPath): + debugPrint("Constructing ADI for '%s'" % libraryPath) + + self.__vm = createVm() + + storeservicecoreLibrary = loadLibrary(self.__vm, "libstoreservicescore.so") + + debugPrint("Loading Android-specific symbols...") + + self.__pADILoadLibraryWithPath = resolveSymbolByName(storeservicecoreLibrary, "kq56gsgHG6") + self.__pADISetAndroidID = resolveSymbolByName(storeservicecoreLibrary, "Sph98paBcz") + self.__pADISetProvisioningPath = resolveSymbolByName(storeservicecoreLibrary, "nf92ngaK92") + + debugPrint("Loading ADI symbols...") + + self.__pADIProvisioningErase = resolveSymbolByName(storeservicecoreLibrary, "p435tmhbla") + self.__pADISynchronize = resolveSymbolByName(storeservicecoreLibrary, "tn46gtiuhw") + self.__pADIProvisioningDestroy = resolveSymbolByName(storeservicecoreLibrary, "fy34trz2st") + self.__pADIProvisioningEnd = resolveSymbolByName(storeservicecoreLibrary, "uv5t6nhkui") + self.__pADIProvisioningStart = resolveSymbolByName(storeservicecoreLibrary, "rsegvyrt87") + self.__pADIGetLoginCode = resolveSymbolByName(storeservicecoreLibrary, "aslgmuibau") + self.__pADIDispose = resolveSymbolByName(storeservicecoreLibrary, "jk24uiwqrg") + self.__pADIOTPRequest = resolveSymbolByName(storeservicecoreLibrary, "qi864985u0") + + self.load_library(libraryPath) + + @property + def provisioning_path(self): + return self._provisioning_path + + @provisioning_path.setter + def provisioning_path(self, value): + pPath = allocData(self.__vm, value.encode('utf-8') + b'\x00') + invoke_cdecl(self.__vm, self.__pADISetProvisioningPath, [pPath]) + self._provisioning_path = value + + @property + def identifier(self): + return self._identifier + + @identifier.setter + def identifier(self, value): + self._identifier = value + debugPrint("Setting identifier %s" % value) + identifier = value.encode('utf-8') + pIdentifier = allocData(self.__vm, identifier) + invoke_cdecl(self.__vm, self.__pADISetAndroidID, [pIdentifier, len(identifier)]) + + def load_library(self, libraryPath): + pLibraryPath = allocData(self.__vm, libraryPath.encode('utf-8') + b'\x00') + invoke_cdecl(self.__vm, self.__pADILoadLibraryWithPath, [pLibraryPath]) + def erase_provisioning(self): + assert(False) + def synchronize(self): + assert(False) + def destroy_provisioning(self): + assert(False) + def end_provisioning(self, session, persistentTokenMetadata, trustKey): + + pPersistentTokenMetadata = allocData(self.__vm, persistentTokenMetadata) + pTrustKey = allocData(self.__vm, trustKey) + + + ret = invoke_cdecl(self.__vm, self.__pADIProvisioningEnd, [ + session, + pPersistentTokenMetadata, + len(persistentTokenMetadata), + pTrustKey, + len(trustKey) + ]) + + debugPrint("0x%X" % session) + debugPrint(persistentTokenMetadata.hex(), len(persistentTokenMetadata)) + debugPrint(trustKey.hex(), len(trustKey)) + + debugPrint("%s: %X=%d" % ("pADIProvisioningEnd", ret, uTo_s32(ret))) + assert(ret == 0) + + def start_provisioning(self, dsId, serverProvisioningIntermediateMetadata): + debugPrint("ADI.start_provisioning") + #FIXME: !!! + + pCpim = allocTemporary(self.__vm, 8) # ubyte* + pCpimLength = allocTemporary(self.__vm, 4) # uint + pSession = allocTemporary(self.__vm, 4) # uint + pServerProvisioningIntermediateMetadata = allocData(self.__vm, serverProvisioningIntermediateMetadata) + debugPrint("0x%X" % dsId) + debugPrint(serverProvisioningIntermediateMetadata.hex()) + + ret = invoke_cdecl(self.__vm, self.__pADIProvisioningStart, [ + dsId, + pServerProvisioningIntermediateMetadata, + len(serverProvisioningIntermediateMetadata), + pCpim, + pCpimLength, + pSession + ]) + debugPrint("%s: %X=%d" % ("pADIProvisioningStart", ret, uTo_s32(ret))) + assert(ret == 0) + + + # Readback output + cpim = read_u64(self.__vm, pCpim) + debugPrint("Wrote data to 0x%X" % cpim) + cpimLength = read_u32(self.__vm, pCpimLength) + cpimBytes = read_data(self.__vm, cpim, cpimLength) + session = read_u32(self.__vm, pSession) + + debugPrint(cpimLength, cpimBytes.hex(), session) + #assert(False) + return ClientProvisioningIntermediateMetadata(self, cpimBytes, session) + def is_machine_provisioned(self, dsId): + debugPrint("ADI.is_machine_provisioned") + + + errorCode = uTo_s32(invoke_cdecl(self.__vm, self.__pADIGetLoginCode, [dsId])) + + if (errorCode == 0): + return True + elif (errorCode == -45061): + return False + + debugPrint("Unknown errorCode in is_machine_provisioned: %d=0x%X" % (errorCode, errorCode)) + assert(False) + + def dispose(self): + assert(False) + def request_otp(self, dsId): + debugPrint("ADI.request_otp") + #FIXME: !!! + + pOtp = allocTemporary(self.__vm, 8) + pOtpLength = allocTemporary(self.__vm, 4) + pMid = allocTemporary(self.__vm, 8) + pMidLength = allocTemporary(self.__vm, 4) + + #ubyte* otp; + #uint otpLength; + #ubyte* mid; + #uint midLength; + + ret = invoke_cdecl(self.__vm, self.__pADIOTPRequest, [ + dsId, + pMid, + pMidLength, + pOtp, + pOtpLength + ]) + debugPrint("%s: %X=%d" % ("pADIOTPRequest", ret, uTo_s32(ret))) + assert(ret == 0) + + otp = read_u64(self.__vm, pOtp) + otpLength = read_u32(self.__vm, pOtpLength) + otpBytes = read_data(self.__vm, otp, otpLength) + + mid = read_u64(self.__vm, pMid) + midLength = read_u32(self.__vm, pMidLength) + midBytes = read_data(self.__vm, mid, midLength) + + return OneTimePassword(self, otpBytes, midBytes) + + +class ProvisioningSession(): + + def __get(self, url, extraHeaders, cacheKey=None): + if enableCache and cacheKey != None: + try: + return open(cacheKey, "rb").read() + except: + pass + headers = self.__headers | extraHeaders + response = requests.get(url, headers=headers, verify=False) + if cacheKey != None: + open(cacheKey + "-head", "wb").write(json.dumps(headers, indent=2).encode('utf-8')) + open(cacheKey, "wb").write(response.content) + return response.content + + def __post(self, url, data, extraHeaders, cacheKey=None): + if enableCache and cacheKey != None: + try: + return open(cacheKey, "rb").read() + except: + pass + headers = self.__headers | extraHeaders + response = requests.post(url, data=data, headers=headers, verify=False) + if cacheKey != None: + open(cacheKey + "-head", "wb").write(json.dumps(headers, indent=2).encode('utf-8')) + open(cacheKey + "-req", "wb").write(data.encode('utf-8')) + open(cacheKey, "wb").write(response.content) + return response.content + + def __init__(self, adi, device): + + self.adi = adi + self.device = device + + self.__urlBag = {} + + self.__headers = { + "User-Agent": "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0", + + # they are somehow not using the plist content-type in AuthKit + "Content-Type": "application/x-www-form-urlencoded", + "Connection": "keep-alive", + + "X-Mme-Device-Id": device.unique_device_identifier, + # on macOS, MMe for the Client-Info header is written with 2 caps, while on Windows it is Mme... + # and HTTP headers are supposed to be case-insensitive in the HTTP spec... + "X-MMe-Client-Info": device.server_friendly_description, + "X-Apple-I-MD-LU": device.local_user_uuid, + + # "X-Apple-I-MLB": device.logicBoardSerialNumber, // 17 letters, uppercase in Apple's base 34 + # "X-Apple-I-ROM": device.romAddress, // 6 bytes, lowercase hexadecimal + # "X-Apple-I-SRL-NO": device.machineSerialNumber, // 12 letters, uppercase + + # different apps can be used, I already saw fmfd and Setup here + # and Reprovision uses Xcode in some requests, so maybe it is possible here too. + "X-Apple-Client-App-Name": "Setup", + } + + return + def load_url_bag(self): + content = self.__get("https://gsa.apple.com/grandslam/GsService2/lookup", {}, "cache/lookup.xml") + plist = plistlib.loads(content) + urls = plist['urls'] + for urlName, url in urls.items(): + self.__urlBag[urlName] = url + + def __time(self): + # Replaces Clock.currTime().stripMilliseconds().toISOExtString() + return datetime.datetime.now().replace(microsecond=0).isoformat() + + def provision(self, dsId): + debugPrint("ProvisioningSession.provision") + #FIXME: !!! + + + if (len(self.__urlBag) == 0): + self.load_url_bag() + + extraHeaders = { + "X-Apple-I-Client-Time": self.__time() + } + startProvisioningPlist = self.__post(self.__urlBag["midStartProvisioning"], + """ + + + +\tHeader +\t +\tRequest +\t + +""", extraHeaders, "cache/midStartProvisioning.xml") + + spimPlist = plistlib.loads(startProvisioningPlist) + spimResponse = spimPlist['Response'] + spimStr = spimResponse["spim"] + debugPrint(spimStr) + + spim = base64.b64decode(spimStr) + + cpim = self.adi.start_provisioning(dsId, spim) + #FIXME: scope (failure) try { adi.destroyProvisioning(cpim.session); } catch(Throwable) {} + + debugPrint(cpim.client_provisioning_intermediate_metadata.hex()) + + extraHeaders = { + "X-Apple-I-Client-Time": self.__time() + } + endProvisioningPlist = self.__post(self.__urlBag["midFinishProvisioning"], """ + + + +\tHeader +\t +\tRequest +\t +\t\tcpim +\t\t%s +\t + +""" % (base64.b64encode(cpim.client_provisioning_intermediate_metadata).decode('utf-8')), extraHeaders, "cache/midFinishProvisioning.xml") + + + + plist = plistlib.loads(endProvisioningPlist) + spimResponse = plist["Response"] + + #scope ulong routingInformation; + #routingInformation = to!ulong(spimResponse["X-Apple-I-MD-RINFO"]) + persistentTokenMetadata = base64.b64decode(spimResponse["ptm"]) + trustKey = base64.b64decode(spimResponse["tk"]) + + self.adi.end_provisioning(cpim.session, persistentTokenMetadata, trustKey) + + return + +uniqueDeviceIdentifierJson = "UUID" +serverFriendlyDescriptionJson = "clientInfo" +adiIdentifierJson = "identifier" +localUserUUIDJson = "localUUID" + +class Device(): + def __init__(self, path): + + debugPrint("Constructing Device for '%s'" % path) + + self.__path = path + + # Attempt to load the JSON + try: + dataBytes = open(self.__path, "rb").read() + data = json.loads(dataBytes.decode('utf-8')) + self._unique_device_identifier = data[uniqueDeviceIdentifierJson] + self._server_friendly_description = data[serverFriendlyDescriptionJson] + self._adi_identifier = data[adiIdentifierJson] + self._local_user_uuid = data[localUserUUIDJson] + self._initialized = True + return + except FileNotFoundError: + pass + + self._unique_device_identifier = None + self._server_friendly_description = None + self._adi_identifier = None + self._local_user_uuid = None + + # This means we have not loaded data from `path` + self._initialized = False + + def write(self, path = None): + if path != None: + self.__path = path + + # Save to JSON + data = {} + data[uniqueDeviceIdentifierJson] = self._unique_device_identifier + data[serverFriendlyDescriptionJson] = self._server_friendly_description + data[adiIdentifierJson] = self._adi_identifier + data[localUserUUIDJson] = self._local_user_uuid + dataBytes = json.dumps(data, indent=2).encode('utf-8') + open(self.__path, "wb").write(dataBytes) + + #FIXME: setters for all properties and they auto-write in the original implementation + + @property + def initialized(self): + return self._initialized + + @property + def unique_device_identifier(self): + return self._unique_device_identifier + + @unique_device_identifier.setter + def unique_device_identifier(self, value): + self._unique_device_identifier = value + self.write() + + @property + def server_friendly_description(self): + return self._server_friendly_description + + @server_friendly_description.setter + def server_friendly_description(self, value): + self._server_friendly_description = value + self.write() + + @property + def adi_identifier(self): + return self._adi_identifier + + @adi_identifier.setter + def adi_identifier(self, value): + self._adi_identifier = value + self.write() + + @property + def local_user_uuid(self): + return self._local_user_uuid + + @local_user_uuid.setter + def local_user_uuid(self, value): + self._local_user_uuid = value + self.write() + + + +R_AARCH64_ABS64 = 257 +R_AARCH64_GLOB_DAT = 1025 +R_AARCH64_JUMP_SLOT = 1026 +R_AARCH64_RELATIVE = 1027 + + +def parseElf(data): + dataIO = BytesIO(data) + elffile = ELFFile(dataIO) + return elffile + +def resolveSymbolByName(library, symbolName): + section = library.elf.get_section_by_name('.dynsym') + assert(isinstance(section, SymbolTableSection)) + + num_symbols = section.num_symbols() + for i in range(num_symbols): + sym = section.get_symbol(i) + if sym.name == symbolName: + #print(sym.__dict__) + return resolveSymbolByIndex(library, i) + + assert(False) + + + +def write_data(vm, address, data): + vm.uc.mem_write(address, data) + +def read_data(vm, address, length): + data = vm.uc.mem_read(address, length) + return bytes(data) + +def read_cstr(vm, address): + maxLength = 0x1000 + s = read_data(vm, address, maxLength) + s, terminator, _ = s.partition(b'\x00') + assert(terminator == b'\x00') + return s + + +def hook_emptyStub(vm): + vm.uc.reg_write(UC_ARM64_REG_X0, 0) + +mallocAllocator = Allocator(mallocAddress, mallocSize) +def hook_malloc(vm): + uc = vm.uc + x0 = uc.reg_read(UC_ARM64_REG_X0) + debugTrace("malloc(0x%X)" % x0) + x0 = mallocAllocator.alloc(x0) + uc.reg_write(UC_ARM64_REG_X0, x0) + +hook_free = hook_emptyStub + +def hook_strncpy(vm): + uc = vm.uc + + x0 = uc.reg_read(UC_ARM64_REG_X0) + x1 = uc.reg_read(UC_ARM64_REG_X1) + x2 = uc.reg_read(UC_ARM64_REG_X2) + + pDst = x0 + pSrc = x1 + _len = x2 + + src = read_cstr(vm, pSrc) + if len(src) > _len: + data = src[0:_len] + assert(False) + else: + paddingSize = _len - len(src) + data = src + b'\x00' * paddingSize + + write_data(vm, pDst, data) + + uc.reg_write(UC_ARM64_REG_X0, pDst) + + +def hook_mkdir(vm): + uc = vm.uc + + x0 = uc.reg_read(UC_ARM64_REG_X0) + x1 = uc.reg_read(UC_ARM64_REG_X1) + + path = read_cstr(vm, x0).decode('utf-8') + mode = x1 + + debugTrace("mkdir('%s', %s)" % (path, oct(mode))) + + assert(path in [ + "./anisette" + ]) + assert(mode == 0o777) + os.mkdir(path) # FIXME: mode? + + uc.reg_write(UC_ARM64_REG_X0, 0) + +def hook_umask(vm): + uc = vm.uc + + x0 = uc.reg_read(UC_ARM64_REG_X0) + + cmask = x0 + + cmask = 0o777 + + uc.reg_write(UC_ARM64_REG_X0, cmask) + +def hook_chmod(vm): + uc = vm.uc + + x0 = uc.reg_read(UC_ARM64_REG_X0) + x1 = uc.reg_read(UC_ARM64_REG_X1) + + path = read_cstr(vm, x0).decode('utf-8') + mode = x1 + + debugTrace("chmod('%s', %s)" % (path, oct(mode))) + + uc.reg_write(UC_ARM64_REG_X0, 0) + + + +# Based on https://github.com/Dadoum/Provision/blob/main/lib/std_edit/linux_stat.d (aarch64) +#FIXME: These must be changed to fixed size types +c_dev_t = c_uint32 +c_off_t = c_size_t +c_ino_t = c_uint64 +c_mode_t = c_uint32 #c_ushort +c_nlink_t = c_uint32 +c_uid_t = c_uint32 +c_gid_t = c_uint32 +c_blksize_t = c_ulong +c_blkcnt_t = c_uint64 +c_time_t = c_uint64 +c_suseconds_t = c_long + +class c_timeval(Structure): + _fields_ = [ + ("tv_sec", c_time_t), # /* seconds since Jan. 1, 1970 */ + ("tv_usec", c_suseconds_t) # /* and microseconds */ + ] + +class c_stat(Structure): + _fields_ = [ + ("st_dev", c_dev_t), # /* ID of device containing file */ + ("st_ino", c_ino_t), # /* inode number */ + ("st_mode", c_mode_t), # /* protection */ + ("st_nlink", c_nlink_t), # /* number of hard links */ + ("st_uid", c_uid_t), # /* user ID of owner */ + ("st_gid", c_gid_t), # /* group ID of owner */ + ("st_rdev", c_dev_t), # /* device ID (if special file) */ + ("__pad1", c_dev_t), # ??? + ("st_size", c_off_t), # /* total size, in bytes */ + ("st_blksize", c_blksize_t), # /* blocksize for file system I/O */ + ("__pad2", c_int), # ??? + ("st_blocks", c_blkcnt_t), # /* number of 512B blocks allocated */ + ("st_atime", c_time_t), # /* time of last access */ + ("st_atimensec", c_ulong), # ?!?! + ("st_mtime", c_time_t), # /* time of last modification */ + ("st_mtimensec", c_ulong), # ?!?! + ("st_ctime", c_time_t), # /* time of last status change */ + ("st_ctimensec", c_ulong), # ?!?! + ("__unused_0", c_int), # ??? + ("__unused_1", c_int) # ??? + ] + +# From https://chromium.googlesource.com/android_tools/+/20ee6d20/ndk/platforms/android-21/arch-arm64/usr/include/sys/stat.h +comment = """ + unsigned long st_dev; \ + unsigned long st_ino; \ + unsigned int st_mode; \ + unsigned int st_nlink; \ + uid_t st_uid; \ + gid_t st_gid; \ + unsigned long st_rdev; \ + unsigned long __pad1; \ + long st_size; \ + int st_blksize; \ + int __pad2; \ + long st_blocks; \ + long st_atime; \ + unsigned long st_atime_nsec; \ + long st_mtime; \ + unsigned long st_mtime_nsec; \ + long st_ctime; \ + unsigned long st_ctime_nsec; \ + unsigned int __unused4; \ + unsigned int __unused5; \ +""" + +tmp = c_stat() +tmpLen = len(bytes(tmp)) +#print(tmpLen) +assert(tmpLen == 128) + +errnoAddress = None + +ENOENT = 2 + +def handle_stat(vm, path, buf): + + try: + statResult = os.stat(path) + #print(statResult) + except FileNotFoundError: + print("Unable to stat '%s'" % path) + vm.uc.reg_write(UC_ARM64_REG_X0, sTo_u64(-1)) + setErrno(vm, ENOENT) + return + + stat = c_stat( + st_dev=0, + st_ino=0, + st_mode=statResult.st_mode, + # ... + st_size=statResult.st_size, + st_blksize=512, + st_blocks=(statResult.st_size + 511) // 512, + # ...s + ) + stat.__byte = statResult.st_size + statBytes = bytes(stat) + #print(statBytes.hex(), len(statBytes)) + + debugPrint("%s %s %s" % (statResult.st_size, statResult.st_blksize, statResult.st_blocks)) + debugPrint("%s %s %s" % (stat.st_size, stat.st_blksize, stat.st_blocks)) + + debugPrint("0x%X = %d" % (statResult.st_mode, statResult.st_mode)) + statBytes = b"".join([ + bytes.fromhex("00000000" + "00000000" + # st_dev + "00000000" + "00000000") + # st_ino + int.to_bytes(statResult.st_mode, 4, 'little') + # st_mode + bytes.fromhex("00000000" + # st_nlink + "a4810000" + # st_uid + "00000000" + # st_gid + "00000000" + "00000000" + # st_rdev + "00000000" + "00000000"), # __pad1 + int.to_bytes(statResult.st_size, 8, 'little'), # st_size + bytes.fromhex("00000000" + # st_blksize + "00000000" + # __pad2 + "00000000" + "00000000" + # st_blocks + "00000000" + "00000000" + # st_atime + "00000000" + "00000000" + # st_atime_nsec + #"00" * 4 + + "00" * 2 + "01" * 2 + "00000000" # st_mtime [This must have a valid value] + + "00000000" + "00000000" + # st_mtime_nsec + "00000000" + "00000000" + # st_ctime + "00000000" + "00000000" + # st_ctime_nsec + "00000000" + # __unused4 + "00000000" # __unused5 + ) + ]) + #00000000000000000002000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + debugPrint(len(statBytes)) + assert(len(statBytes) in [104, 128]) + + + write_data(vm, buf, statBytes) + + # Return success + vm.uc.reg_write(UC_ARM64_REG_X0, 0) + +def hook_lstat(vm): + + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + + pPath = x0 + path = read_cstr(vm, pPath).decode('utf-8') + buf = x1 + + debugTrace("lstat(0x%X:'%s', [...])" % (pPath, path)) + + return handle_stat(vm, path, buf) + + + + +def hook_fstat(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + + fildes = x0 + buf = x1 + + fileIndex = fildes + fileHandle = fileHandles[fileIndex] + + return handle_stat(vm, fileHandle.fileno(), buf) + + +fileHandles = [] + +O_RDONLY = 0o0 +O_WRONLY = 0o1 +O_RDWR = 0o2 +O_CREAT = 0o100 +O_NOFOLLOW = 0o100000 + +def hook_open(vm): + global fileHandles + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + x2 = vm.uc.reg_read(UC_ARM64_REG_X2) + + path = read_cstr(vm, x0).decode('utf-8') + oflag = x1 + mode = x2 + + debugTrace("open('%s', %s, %s)" % (path, oct(oflag), oct(mode))) + #time.sleep(2.0) + #assert(False) + + assert(path in [ + './anisette/adi.pb' + ]) + + assert(oflag in [0o100000, 0o100101]) + + if oflag & O_WRONLY: + mode = "wb" + else: + mode = "rb" + + if oflag & O_CREAT: + mode += "+" + + fileIndex = len(fileHandles) + fileHandle = open(path, mode) + fileHandles += [fileHandle] + + # Return fildes + fildes = fileIndex + vm.uc.reg_write(UC_ARM64_REG_X0, fildes) + + +def hook_ftruncate(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + + fildes = x0 + length = x1 + + debugTrace("ftruncate(%d, %d)" % (fildes, length)) + + fileIndex = fildes + fileHandle = fileHandles[fileIndex] + + fileHandle.truncate(length) + + vm.uc.reg_write(UC_ARM64_REG_X0, 0) + + +def hook_read(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + x2 = vm.uc.reg_read(UC_ARM64_REG_X2) + + fildes = x0 + buf = x1 + nbyte = x2 + + debugTrace("read(%d, 0x%X, %d)" % (fildes, buf, nbyte)) + #assert(False) + + fileIndex = fildes + fileHandle = fileHandles[fileIndex] + + bufBytes = fileHandle.read(nbyte) + write_data(vm, buf, bufBytes) + + vm.uc.reg_write(UC_ARM64_REG_X0, nbyte) + + + +def hook_write(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + x2 = vm.uc.reg_read(UC_ARM64_REG_X2) + + fildes = x0 + buf = x1 + nbyte = x2 + + debugTrace("write(%d, 0x%X, %d)" % (fildes, buf, nbyte)) + + fileIndex = fildes + fileHandle = fileHandles[fileIndex] + + bufBytes = read_data(vm, buf, nbyte) + fileHandle.write(bufBytes) + + vm.uc.reg_write(UC_ARM64_REG_X0, nbyte) + + +def hook_close(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + + fildes = x0 + + fileIndex = fildes + fileHandle = fileHandles[fileIndex] + + fileHandle.close() + + vm.uc.reg_write(UC_ARM64_REG_X0, 0) + +def hook_dlopenWrapper(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + path = read_cstr(vm, x0).decode('utf-8') + libraryName = path.rpartition('/')[2] + + debugTrace("dlopen('%s' (%s))" % (path, libraryName)) + + assert(libraryName in [ + "libCoreADI.so" + ]) + + library = loadLibrary(vm, libraryName) + x0 = library.index + vm.uc.reg_write(UC_ARM64_REG_X0, 1 + x0) + + #assert(False) + +def hook_dlsymWrapper(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + handle = x0 + symbol = read_cstr(vm, x1).decode('utf-8') + + libraryIndex = handle - 1 + library = vm.loadedLibraries[libraryIndex] + + debugTrace("dlsym(%X (%s), '%s')" % (handle, library.name, symbol)) + + symbolAddress = resolveSymbolByName(library, symbol) + debugPrint("Found at 0x%X" % symbolAddress) + + vm.uc.reg_write(UC_ARM64_REG_X0, symbolAddress) + +hook_dlcloseWrapper = hook_emptyStub + +def hook_gettimeofday(vm): + timestamp = time.time() + + cacheTime = False + cachePath = "cache/time.bin" + + if cacheTime: + tBytes = open(cachePath, "rb").read() + print("Loaded time from cache!") + t = c_timeval.from_buffer_copy(tBytes) + print("ok", t) + + t = c_timeval( + tv_sec = math.floor(timestamp // 1), + tv_usec = math.floor((timestamp % 1.0) * 1000 * 1000) + ) + tBytes = bytes(t) + + if cacheTime: + open(cachePath, "wb").write(tBytes) + + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + + tp = x0 + tzp = x1 + + debugTrace("gettimeofday(0x%X, 0x%X)" % (tp, tzp)) + + # We don't need timezone support + assert(tzp == 0) + comment = """ + struct timezone { + int tz_minuteswest; /* of Greenwich */ + int tz_dsttime; /* type of dst correction to apply */ + }; + """ + + # Write the time + debugPrint("%s %s %s" % (t.__dict__, tBytes.hex(), len(tBytes))) + write_data(vm, tp, tBytes) + + # Return success + vm.uc.reg_write(UC_ARM64_REG_X0, 0) + +def setErrno(vm, value): + global errnoAddress + if errnoAddress == None: + errnoAddress = allocTemporary(vm, 4) + write_u32(vm, errnoAddress, value) + +def hook___errno_location(vm): + global errnoAddress + if errnoAddress == None: + debugPrint("Checking errno before first error (!)") + setErrno(vm, 0) + vm.uc.reg_write(UC_ARM64_REG_X0, errnoAddress) + +def hook___system_property_get_impl(vm): + x0 = vm.uc.reg_read(UC_ARM64_REG_X0) + x1 = vm.uc.reg_read(UC_ARM64_REG_X1) + name = read_cstr(vm, x0).decode('utf-8') + debugTrace("__system_property_get(%s, [...])" % name) + value = b"no s/n number" + write_data(vm, x1, value) + vm.uc.reg_write(UC_ARM64_REG_X0, len(value)) + + +def hook_arc4random_impl(vm): + value = 0xDEADBEEF # "Random number, chosen by fair dice roll" + vm.uc.reg_write(UC_ARM64_REG_X0, value) + +stubbedFunctions = { + + # memory management + "malloc": hook_malloc, + "free": hook_free, + + # string + "strncpy": hook_strncpy, + + # fs + "mkdir": hook_mkdir, + "umask": hook_umask, + "chmod": hook_chmod, + "lstat": hook_lstat, + "fstat": hook_fstat, + + # io + "open": hook_open, + "ftruncate": hook_ftruncate, + "read": hook_read, + "write": hook_write, + "close": hook_close, + + # dynamic symbol stuff + "dlsym": hook_dlsymWrapper, + "dlopen": hook_dlopenWrapper, + "dlclose": hook_dlcloseWrapper, + + # pthreads + "pthread_once": hook_emptyStub, + "pthread_create": hook_emptyStub, + "pthread_mutex_lock": hook_emptyStub, + "pthread_rwlock_unlock": hook_emptyStub, + "pthread_rwlock_destroy": hook_emptyStub, + "pthread_rwlock_wrlock": hook_emptyStub, + "pthread_rwlock_init": hook_emptyStub, + "pthread_mutex_unlock": hook_emptyStub, + "pthread_rwlock_rdlock": hook_emptyStub, + + # date and time + "gettimeofday": hook_gettimeofday, + + # misc + "__errno": hook___errno_location, + "__system_property_get": hook___system_property_get_impl, + "arc4random": hook_arc4random_impl, +} + +class Library(): + def __init__(self, name, elf, base, index): + self.name = name + self.elf = elf + self.base = base + self.symbols = {} + self.index = index + +def resolveSymbolByIndex(library, symbolIndex): + #for section in elf.iter_sections(): + # print(section) + if symbolIndex in library.symbols: + #print("Resolving symbol 0x%X from symbols dict" % symbolIndex) + return library.symbols[symbolIndex] + + section = library.elf.get_section_by_name('.dynsym') + assert(isinstance(section, SymbolTableSection)) + + sym = section.get_symbol(symbolIndex) + #print("Resolving symbol 0x%X relative to base" % symbolIndex, sym.__dict__) + + #if sym['st_shndx'] == 11: + # section = library.elf.get_section(sym['st_shndx']) + # print("Fixing section", section.__dict__) + # print("0x%X" % (section['sh_addr'] + sym['st_value'])) + # assert(False) + + return library.base + sym['st_value'] + + assert(False) + +def symbolNameByIndex(library, symbolIndex): + section = library.elf.get_section_by_name('.dynsym') + assert(isinstance(section, SymbolTableSection)) + + sym = section.get_symbol(symbolIndex) + return sym.name + + + +def loadLibrary(vm, libraryName): + + # Do not load the same library multiple times + for library in vm.loadedLibraries: + debugPrint("Comparing '%s' to loaded library '%s'" % (libraryName, library.name)) + if library.name == libraryName: + debugPrint("Library already loaded") + return library + + uc = vm.uc + + libraryIndex = len(vm.loadedLibraries) + elfData = files[libraryName] + + elf = parseElf(elfData) + + chosenBase = vm.libraryAllocator.alloc(0x10000000) + + library = Library(libraryName, elf, chosenBase, libraryIndex) + + # Stub all imports + section = library.elf.get_section_by_name('.dynsym') + num_symbols = section.num_symbols() + for i in range(num_symbols): + sym = section.get_symbol(i) + #print(sym.name) + + #print(sym.__dict__) + #print(sym['st_shndx']) + if sym['st_shndx'] == 'SHN_UNDEF': + library.symbols[i] = importAddress + libraryIndex * 0x01000000 + i * 4 + #print("Registering 0x%X: %s" % (library.symbols[i], sym.name)) + + #print("%s: 0x%X" % (sym.name, resolveSymbolByIndex(library, i))) + + + for segment in elf.iter_segments(): + address = chosenBase + segment['p_vaddr'] + size = segment['p_memsz'] + + addressStart = address + addressEnd = address + size + + alignment = segment['p_align'] + + # Align the start + addressStart &= ~(alignment - 1) + + # Align the end + addressEnd += alignment - 1 + addressEnd &= ~(alignment - 1) + + # Fix size for new alignment + size = addressEnd - address + + dataOffset = segment['p_offset'] + dataSize = segment['p_filesz'] + paddingBeforeSize = address - addressStart + paddingAfterSize = size - dataSize + + debugPrint("Mapping at 0x%X-0x%X (0x%X-0x%X); bytes 0x%X" % (addressStart, addressEnd, address, address + size - 1, size)) + + if segment['p_type'] == 'PT_LOAD': + data = b'\x00' * paddingBeforeSize + elfData[dataOffset:dataOffset+dataSize] + b'\x00' * paddingAfterSize + uc.mem_map(addressStart, len(data)) + uc.mem_write(addressStart, data) + else: + debugPrint("- Skipping %s" % (segment.__dict__)) + + def relocateSection(sectionName): + + reladyn = elf.get_section_by_name(sectionName) + assert(isinstance(reladyn, RelocationSection)) + + for reloc in reladyn.iter_relocations(): + + #print(' Relocation (%s)' % 'RELA' if reloc.is_RELA() else 'REL', end="") + # Relocation entry attributes are available through item lookup + #print(' offset = 0x%X' % reloc['r_offset']) + #print("%s" % reloc.__dict__, end="") + #print("") + + type = reloc['r_info_type'] + address = chosenBase + reloc['r_offset'] + + if type == R_AARCH64_ABS64: + symbolIndex = reloc['r_info_sym'] + symbolAddress = resolveSymbolByIndex(library, symbolIndex) + uc.mem_write(address, int.to_bytes(symbolAddress + reloc['r_addend'], 8, 'little')) #b'\x12\x34\x22\x78\xAB\xCD\xEF\xFF') + elif type == R_AARCH64_GLOB_DAT: + symbolIndex = reloc['r_info_sym'] + symbolAddress = resolveSymbolByIndex(library, symbolIndex) + uc.mem_write(address, int.to_bytes(symbolAddress + reloc['r_addend'], 8, 'little')) #b'\x12\x34\x22\x78\xAB\xCD\xEF\xFF') + elif type == R_AARCH64_JUMP_SLOT: + symbolIndex = reloc['r_info_sym'] + symbolAddress = resolveSymbolByIndex(library, symbolIndex) + uc.mem_write(address, int.to_bytes(symbolAddress, 8, 'little')) #b'\x12\x34\x11\x78\xAB\xCD\xEF\xFF') + elif type == R_AARCH64_RELATIVE: + uc.mem_write(address, int.to_bytes(chosenBase + reloc['r_addend'], 8, 'little')) #b'\x12\x34\x22\x78\xAB\xCD\xEF\xFF') + else: + assert(False) + relocateSection('.rela.dyn') + relocateSection('.rela.plt') + + # Loop over each initializer?! + + vm.loadedLibraries += [library] + return library + + + + +# Quick tool to test some functionality +def main(): + import uuid + + #import pyprovision + pyprovision = sys.modules[__name__] + + from ctypes import c_ulonglong + import secrets + adi = pyprovision.ADI("./anisette/") + adi.provisioning_path = "./anisette/" + device = pyprovision.Device("./anisette/device.json") + if not device.initialized: + print("Initializing device") + # Pretend to be a MacBook Pro + device.server_friendly_description = " " + device.unique_device_identifier = str(uuid.uuid4()).upper() + device.adi_identifier = secrets.token_hex(8).lower() + device.local_user_uuid = secrets.token_hex(32).upper() + else: + print("(Device initialized: server-description='%s' device-uid='%s' adi='%s' user-uid='%s'" % (device.server_friendly_description, device.unique_device_identifier, device.adi_identifier, device.local_user_uuid)) + adi.identifier = device.adi_identifier + dsid = c_ulonglong(-2).value + is_prov = adi.is_machine_provisioned(dsid) + if not is_prov: + print("Provisioning...") + provisioning_session = pyprovision.ProvisioningSession(adi, device) + provisioning_session.provision(dsid) + else: + print("(Already provisioned)") + otp = adi.request_otp(dsid) + a = {"X-Apple-I-MD": base64.b64encode(bytes(otp.one_time_password)).decode(), "X-Apple-I-MD-M": base64.b64encode(bytes(otp.machine_identifier)).decode()} + otp = adi.request_otp(dsid) + # a.update(generate_meta_headers(user_id=USER_ID, device_id=DEVICE_ID)) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + otp = adi.request_otp(dsid) + print(a) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/example/run-node.mjs b/example/run-node.mjs index 9d7efe4..f42e1fd 100644 --- a/example/run-node.mjs +++ b/example/run-node.mjs @@ -3,7 +3,7 @@ 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 distDir = path.join(path.join(__dirname, '..'), 'dist') const modulePath = path.join(distDir, 'anisette_rs.node.js'); const wasmPath = path.join(distDir, 'anisette_rs.node.wasm'); diff --git a/js/bun.lock b/js/bun.lock new file mode 100644 index 0000000..517650d --- /dev/null +++ b/js/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "anisette-js", + "devDependencies": { + "typescript": "^5.4.0", + }, + }, + }, + "packages": { + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + } +} diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..8ea7091 --- /dev/null +++ b/js/package.json @@ -0,0 +1,22 @@ +{ + "name": "anisette-js", + "version": "0.1.0", + "description": "High-level JavaScript/TypeScript API for Apple Anisette headers via WASM", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "bun build src/index.ts --outfile dist/anisette.js --target node --format esm --minify", + "build:cjs": "bun build src/index.ts --outfile dist/anisette.cjs --target node --format cjs --minify", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "typescript": "^5.4.0" + } +} diff --git a/js/src/anisette.ts b/js/src/anisette.ts new file mode 100644 index 0000000..a60834f --- /dev/null +++ b/js/src/anisette.ts @@ -0,0 +1,213 @@ +// Main Anisette class — the public-facing API + +import type { AnisetteDeviceConfig, AnisetteHeaders, InitOptions } from "./types.js"; +import type { HttpClient } from "./http.js"; +import { WasmBridge } from "./wasm-bridge.js"; +import { Device } from "./device.js"; +import { LibraryStore } from "./library.js"; +import { ProvisioningSession } from "./provisioning.js"; +import { + toBase64, + toAppleClientTime, + detectLocale, + encodeUtf8, +} from "./utils.js"; +import type { DeviceJson } from "./types.js"; + +const DEFAULT_DSID = BigInt(-2); +const DEFAULT_LIBRARY_PATH = "./anisette/"; +const MD_RINFO = "17106176"; + +export interface AnisetteOptions { + /** Override the HTTP client (useful for testing or custom proxy) */ + httpClient?: HttpClient; + /** DSID to use when requesting OTP (default: -2) */ + dsid?: bigint; + /** Options passed to WASM init */ + init?: InitOptions; +} + +export class Anisette { + private bridge: WasmBridge; + private device: Device; + private libs: LibraryStore; + private provisioning: ProvisioningSession; + private dsid: bigint; + private libraryPath: string; + + private constructor( + bridge: WasmBridge, + device: Device, + libs: LibraryStore, + provisioning: ProvisioningSession, + dsid: bigint, + libraryPath: string + ) { + this.bridge = bridge; + this.device = device; + this.libs = libs; + this.provisioning = provisioning; + this.dsid = dsid; + this.libraryPath = libraryPath; + } + + // ---- factory methods ---- + + /** + * Initialize from the two Android .so library files. + * @param storeservicescore - bytes of libstoreservicescore.so + * @param coreadi - bytes of libCoreADI.so + */ + static async fromSo( + storeservicescore: Uint8Array, + coreadi: Uint8Array, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wasmModule: any, + options: AnisetteOptions = {} + ): Promise { + const libs = LibraryStore.fromBlobs(storeservicescore, coreadi); + return Anisette._init(libs, wasmModule, options); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private static async _init( + libs: LibraryStore, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wasmModule: any, + options: AnisetteOptions + ): Promise { + const bridge = new WasmBridge(wasmModule); + const initOpts = options.init ?? {}; + const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH; + const provisioningPath = initOpts.provisioningPath ?? libraryPath; + const dsid = options.dsid ?? DEFAULT_DSID; + + // Load or generate device config + const device = Device.fromJson(null, initOpts.deviceConfig); + + // Write device.json into WASM VFS so the emulator can read it + const deviceJson = device.toJson(); + const deviceJsonBytes = encodeUtf8(JSON.stringify(deviceJson, null, 2)); + bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes); + + // Initialize WASM ADI + bridge.initFromBlobs( + libs.storeservicescore, + libs.coreadi, + libraryPath, + provisioningPath, + initOpts.identifier ?? device.adiIdentifier + ); + + const provisioning = new ProvisioningSession( + bridge, + device, + options.httpClient + ); + + return new Anisette(bridge, device, libs, provisioning, dsid, libraryPath); + } + + /** + * Load a previously saved session (device.json + adi.pb written back into VFS). + * Pass the saved device.json and adi.pb bytes alongside the library blobs. + */ + static async fromSaved( + storeservicescore: Uint8Array, + coreadi: Uint8Array, + deviceJsonBytes: Uint8Array, + adiPbBytes: Uint8Array, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + wasmModule: any, + options: AnisetteOptions = {} + ): Promise { + const bridge = new WasmBridge(wasmModule); + const initOpts = options.init ?? {}; + const libraryPath = initOpts.libraryPath ?? DEFAULT_LIBRARY_PATH; + const provisioningPath = initOpts.provisioningPath ?? libraryPath; + const dsid = options.dsid ?? DEFAULT_DSID; + + // Parse saved device config + let deviceJson: DeviceJson | null = null; + try { + deviceJson = JSON.parse(new TextDecoder().decode(deviceJsonBytes)) as DeviceJson; + } catch { + // ignore parse errors — will generate fresh device + } + const device = Device.fromJson(deviceJson, initOpts.deviceConfig); + + // Restore VFS files + bridge.writeVirtualFile(joinPath(libraryPath, "device.json"), deviceJsonBytes); + bridge.writeVirtualFile(joinPath(libraryPath, "adi.pb"), adiPbBytes); + + const libs = LibraryStore.fromBlobs(storeservicescore, coreadi); + + bridge.initFromBlobs( + libs.storeservicescore, + libs.coreadi, + libraryPath, + provisioningPath, + initOpts.identifier ?? device.adiIdentifier + ); + + const provisioning = new ProvisioningSession( + bridge, + device, + options.httpClient + ); + + return new Anisette(bridge, device, libs, provisioning, dsid, libraryPath); + } + + // ---- public API ---- + + /** Whether the device is currently provisioned. */ + get isProvisioned(): boolean { + return this.bridge.isMachineProvisioned(this.dsid); + } + + /** Run the provisioning flow against Apple servers. */ + async provision(): Promise { + await this.provisioning.provision(this.dsid); + } + + /** Generate Anisette headers. Throws if not provisioned. */ + async getData(): Promise { + const { otp, machineId } = this.bridge.requestOtp(this.dsid); + + const now = new Date(); + const tzOffset = -now.getTimezoneOffset(); + const tzSign = tzOffset >= 0 ? "+" : "-"; + const tzHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0"); + const tzMins = String(Math.abs(tzOffset) % 60).padStart(2, "0"); + const timezone = `${tzSign}${tzHours}${tzMins}`; + + return { + "X-Apple-I-Client-Time": toAppleClientTime(now), + "X-Apple-I-MD": toBase64(otp), + "X-Apple-I-MD-LU": this.device.localUserUuid, + "X-Apple-I-MD-M": toBase64(machineId), + "X-Apple-I-MD-RINFO": MD_RINFO, + "X-Apple-I-SRL-NO": "0", + "X-Apple-I-TimeZone": timezone, + "X-Apple-Locale": detectLocale(), + "X-MMe-Client-Info": this.device.serverFriendlyDescription, + "X-Mme-Device-Id": this.device.uniqueDeviceIdentifier, + }; + } + + /** Serialize device.json bytes for persistence. */ + getDeviceJson(): Uint8Array { + return encodeUtf8(JSON.stringify(this.device.toJson(), null, 2)); + } + + /** Expose the device for inspection. */ + getDevice(): Device { + return this.device; + } +} + +function joinPath(base: string, file: string): string { + const b = base.endsWith("/") ? base : `${base}/`; + return `${b}${file}`; +} diff --git a/js/src/device.ts b/js/src/device.ts new file mode 100644 index 0000000..a6b60ad --- /dev/null +++ b/js/src/device.ts @@ -0,0 +1,64 @@ +// Device identity management — loads or generates device.json + +import type { AnisetteDeviceConfig, DeviceJson } from "./types.js"; +import { randomHex, randomUUID } from "./utils.js"; + +const DEFAULT_CLIENT_INFO = + " "; + +export class Device { + readonly uniqueDeviceIdentifier: string; + readonly serverFriendlyDescription: string; + readonly adiIdentifier: string; + readonly localUserUuid: string; + + private constructor(data: DeviceJson) { + this.uniqueDeviceIdentifier = data.UUID; + this.serverFriendlyDescription = data.clientInfo; + this.adiIdentifier = data.identifier; + this.localUserUuid = data.localUUID; + } + + /** Load from a parsed device.json object, or generate defaults if null. */ + static fromJson( + json: DeviceJson | null, + overrides?: Partial + ): Device { + const defaults = Device.generateDefaults(); + const base: DeviceJson = json ?? { + UUID: defaults.uniqueDeviceId, + clientInfo: defaults.serverFriendlyDescription, + identifier: defaults.adiId, + localUUID: defaults.localUserUuid, + }; + + if (overrides) { + if (overrides.uniqueDeviceId) base.UUID = overrides.uniqueDeviceId; + if (overrides.serverFriendlyDescription) + base.clientInfo = overrides.serverFriendlyDescription; + if (overrides.adiId) base.identifier = overrides.adiId; + if (overrides.localUserUuid) base.localUUID = overrides.localUserUuid; + } + + return new Device(base); + } + + /** Serialize back to the device.json wire format. */ + toJson(): DeviceJson { + return { + UUID: this.uniqueDeviceIdentifier, + clientInfo: this.serverFriendlyDescription, + identifier: this.adiIdentifier, + localUUID: this.localUserUuid, + }; + } + + static generateDefaults(): AnisetteDeviceConfig { + return { + serverFriendlyDescription: DEFAULT_CLIENT_INFO, + uniqueDeviceId: randomUUID(), + adiId: randomHex(8, false), + localUserUuid: randomHex(32, true), + }; + } +} diff --git a/js/src/http.ts b/js/src/http.ts new file mode 100644 index 0000000..4998f4e --- /dev/null +++ b/js/src/http.ts @@ -0,0 +1,32 @@ +// HTTP client abstraction — allows swapping fetch vs Node.js http in tests + +export interface HttpClient { + get(url: string, headers: Record): Promise; + post( + url: string, + body: string, + headers: Record + ): Promise; +} + +export class FetchHttpClient implements HttpClient { + async get(url: string, headers: Record): Promise { + const response = await fetch(url, { method: "GET", headers }); + if (!response.ok) { + throw new Error(`HTTP GET ${url} failed: ${response.status} ${response.statusText}`); + } + return new Uint8Array(await response.arrayBuffer()); + } + + async post( + url: string, + body: string, + headers: Record + ): Promise { + const response = await fetch(url, { method: "POST", body, headers }); + if (!response.ok) { + throw new Error(`HTTP POST ${url} failed: ${response.status} ${response.statusText}`); + } + return new Uint8Array(await response.arrayBuffer()); + } +} diff --git a/js/src/index.ts b/js/src/index.ts new file mode 100644 index 0000000..a8b289e --- /dev/null +++ b/js/src/index.ts @@ -0,0 +1,17 @@ +// Public entry point — re-exports everything users need + +export { Anisette } from "./anisette.js"; +export { loadWasm } from "./wasm-loader.js"; +export { WasmBridge } from "./wasm-bridge.js"; +export { Device } from "./device.js"; +export { LibraryStore } from "./library.js"; +export { ProvisioningSession } from "./provisioning.js"; +export { FetchHttpClient } from "./http.js"; +export type { HttpClient } from "./http.js"; +export type { + AnisetteHeaders, + AnisetteDeviceConfig, + InitOptions, + DeviceJson, +} from "./types.js"; +export type { AnisetteOptions } from "./anisette.js"; diff --git a/js/src/library.ts b/js/src/library.ts new file mode 100644 index 0000000..591ebd6 --- /dev/null +++ b/js/src/library.ts @@ -0,0 +1,40 @@ +// LibraryStore — holds the two required Android .so blobs + +const REQUIRED_LIBS = [ + "libstoreservicescore.so", + "libCoreADI.so", +] as const; + +export type LibraryName = (typeof REQUIRED_LIBS)[number]; + +export class LibraryStore { + private libs: Map; + + private constructor(libs: Map) { + this.libs = libs; + } + + static fromBlobs( + storeservicescore: Uint8Array, + coreadi: Uint8Array + ): LibraryStore { + const map = new Map(); + map.set("libstoreservicescore.so", storeservicescore); + map.set("libCoreADI.so", coreadi); + return new LibraryStore(map); + } + + get(name: LibraryName): Uint8Array { + const data = this.libs.get(name); + if (!data) throw new Error(`Library not loaded: ${name}`); + return data; + } + + get storeservicescore(): Uint8Array { + return this.get("libstoreservicescore.so"); + } + + get coreadi(): Uint8Array { + return this.get("libCoreADI.so"); + } +} diff --git a/js/src/provisioning.ts b/js/src/provisioning.ts new file mode 100644 index 0000000..36b51a0 --- /dev/null +++ b/js/src/provisioning.ts @@ -0,0 +1,159 @@ +// ProvisioningSession — communicates with Apple servers to provision the device + +import { fromBase64, toBase64, toAppleClientTime } from "./utils.js"; +import type { WasmBridge } from "./wasm-bridge.js"; +import type { Device } from "./device.js"; +import type { HttpClient } from "./http.js"; +import { FetchHttpClient } from "./http.js"; + +const LOOKUP_URL = "https://gsa.apple.com/grandslam/GsService2/lookup"; + +const START_PROVISIONING_BODY = ` + + + + Header + + Request + + +`; + +export class ProvisioningSession { + private bridge: WasmBridge; + private device: Device; + private http: HttpClient; + private urlBag: Record = {}; + + constructor(bridge: WasmBridge, device: Device, http?: HttpClient) { + this.bridge = bridge; + this.device = device; + this.http = http ?? new FetchHttpClient(); + } + + async provision(dsid: bigint): Promise { + if (Object.keys(this.urlBag).length === 0) { + await this.loadUrlBag(); + } + + const startUrl = this.urlBag["midStartProvisioning"]; + const finishUrl = this.urlBag["midFinishProvisioning"]; + if (!startUrl) throw new Error("url bag missing midStartProvisioning"); + if (!finishUrl) throw new Error("url bag missing midFinishProvisioning"); + + // Step 1: get SPIM from Apple + const startBytes = await this.http.post( + startUrl, + START_PROVISIONING_BODY, + this.commonHeaders(true) + ); + const startPlist = parsePlist(startBytes); + const spimB64 = plistGetStringInResponse(startPlist, "spim"); + const spim = fromBase64(spimB64); + + // Step 2: call WASM start_provisioning + const { cpim, session } = this.bridge.startProvisioning(dsid, spim); + const cpimB64 = toBase64(cpim); + + // Step 3: send CPIM to Apple, get PTM + TK + const finishBody = buildFinishBody(cpimB64); + const finishBytes = await this.http.post( + finishUrl, + finishBody, + this.commonHeaders(true) + ); + const finishPlist = parsePlist(finishBytes); + const ptm = fromBase64(plistGetStringInResponse(finishPlist, "ptm")); + const tk = fromBase64(plistGetStringInResponse(finishPlist, "tk")); + + // Step 4: finalize provisioning in WASM + this.bridge.endProvisioning(session, ptm, tk); + } + + private async loadUrlBag(): Promise { + const bytes = await this.http.get(LOOKUP_URL, this.commonHeaders(false)); + const plist = parsePlist(bytes); + const urls = plistGetDict(plist, "urls"); + this.urlBag = {}; + for (const [k, v] of Object.entries(urls)) { + if (typeof v === "string") this.urlBag[k] = v; + } + } + + private commonHeaders(includeTime: boolean): Record { + const headers: Record = { + "User-Agent": "akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0", + "Content-Type": "application/x-www-form-urlencoded", + Connection: "keep-alive", + "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", + }; + if (includeTime) { + headers["X-Apple-I-Client-Time"] = toAppleClientTime(); + } + return headers; + } +} + +// ---- minimal plist XML parser ---- +// We only need to extract string values from Apple's response plists. + +interface PlistDict { + [key: string]: string | PlistDict; +} + +function parsePlist(bytes: Uint8Array): PlistDict { + const xml = new TextDecoder("utf-8").decode(bytes); + return parsePlistDict(xml); +} + +function parsePlistDict(xml: string): PlistDict { + const result: PlistDict = {}; + // Match ... followed by ... or ... + const keyRe = /([^<]*)<\/key>\s*(([^<]*)<\/string>|([\s\S]*?)<\/dict>)/g; + let m: RegExpExecArray | null; + while ((m = keyRe.exec(xml)) !== null) { + const key = m[1]; + if (m[3] !== undefined) { + result[key] = m[3]; + } else if (m[4] !== undefined) { + result[key] = parsePlistDict(m[4]); + } + } + return result; +} + +function plistGetStringInResponse(plist: PlistDict, key: string): string { + const response = plist; + const value = (response as PlistDict)[key]; + if (typeof value !== "string") { + throw new Error(`plist Response missing string field: ${key}`); + } + return value; +} + +function plistGetDict(plist: PlistDict, key: string): PlistDict { + const value = plist[key]; + if (!value || typeof value === "string") { + throw new Error(`plist missing dict field: ${key}`); + } + return value as PlistDict; +} + +function buildFinishBody(cpimB64: string): string { + return ` + + + + Header + + Request + + cpim + ${cpimB64} + + +`; +} diff --git a/js/src/types.ts b/js/src/types.ts new file mode 100644 index 0000000..d82bdee --- /dev/null +++ b/js/src/types.ts @@ -0,0 +1,44 @@ +// Core type definitions for the Anisette JS/TS API + +export interface AnisetteDeviceConfig { + /** Human-readable device description sent to Apple servers */ + serverFriendlyDescription: string; + /** Unique device UUID (uppercase) */ + uniqueDeviceId: string; + /** ADI identifier (hex string) */ + adiId: string; + /** Local user UUID (uppercase hex) */ + localUserUuid: string; +} + +export interface AnisetteHeaders { + "X-Apple-I-Client-Time": string; + "X-Apple-I-MD": string; + "X-Apple-I-MD-LU": string; + "X-Apple-I-MD-M": string; + "X-Apple-I-MD-RINFO": string; + "X-Apple-I-SRL-NO": string; + "X-Apple-I-TimeZone": string; + "X-Apple-Locale": string; + "X-MMe-Client-Info": string; + "X-Mme-Device-Id": string; +} + +export interface InitOptions { + /** Path prefix used inside the WASM virtual filesystem for library files */ + libraryPath?: string; + /** Path prefix used inside the WASM virtual filesystem for provisioning data */ + provisioningPath?: string; + /** ADI identifier override */ + identifier?: string; + /** Override parts of the generated device config */ + deviceConfig?: Partial; +} + +/** Raw device.json structure as stored on disk / in WASM VFS */ +export interface DeviceJson { + UUID: string; + clientInfo: string; + identifier: string; + localUUID: string; +} diff --git a/js/src/utils.ts b/js/src/utils.ts new file mode 100644 index 0000000..c082aaf --- /dev/null +++ b/js/src/utils.ts @@ -0,0 +1,91 @@ +// Utility functions shared across modules + +const TEXT_ENCODER = new TextEncoder(); +const TEXT_DECODER = new TextDecoder("utf-8"); + +/** Encode string to UTF-8 bytes */ +export function encodeUtf8(str: string): Uint8Array { + return TEXT_ENCODER.encode(str); +} + +/** Decode UTF-8 bytes to string */ +export function decodeUtf8(bytes: Uint8Array): string { + return TEXT_DECODER.decode(bytes); +} + +/** Encode bytes to base64 string */ +export function toBase64(bytes: Uint8Array): string { + if (bytes.length === 0) return ""; + // Works in both browser and Node.js (Node 16+) + if (typeof Buffer !== "undefined") { + return Buffer.from(bytes).toString("base64"); + } + let binary = ""; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +/** Decode base64 string to bytes */ +export function fromBase64(b64: string): Uint8Array { + if (typeof Buffer !== "undefined") { + return new Uint8Array(Buffer.from(b64, "base64")); + } + const binary = atob(b64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +/** Format a Date as Apple client time string (ISO 8601 without milliseconds) */ +export function toAppleClientTime(date: Date = new Date()): string { + return date.toISOString().replace(/\.\d{3}Z$/, "Z"); +} + +/** Detect locale string in Apple format (e.g. "en_US") */ +export function detectLocale(): string { + const locale = + (typeof Intl !== "undefined" && + Intl.DateTimeFormat().resolvedOptions().locale) || + "en-US"; + return locale.replace("-", "_"); +} + +/** Generate a random hex string of the given byte length */ +export function randomHex(byteLen: number, uppercase = false): string { + const bytes = new Uint8Array(byteLen); + if (typeof crypto !== "undefined" && crypto.getRandomValues) { + crypto.getRandomValues(bytes); + } else { + // Node.js fallback + // eslint-disable-next-line @typescript-eslint/no-require-imports + const nodeCrypto = require("crypto") as typeof import("crypto"); + const buf = nodeCrypto.randomBytes(byteLen); + bytes.set(buf); + } + let hex = Array.from(bytes) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + return uppercase ? hex.toUpperCase() : hex; +} + +/** Generate a random UUID v4 (uppercase) */ +export function randomUUID(): string { + if (typeof crypto !== "undefined" && crypto.randomUUID) { + return crypto.randomUUID().toUpperCase(); + } + // Manual fallback + const hex = randomHex(16); + return [ + hex.slice(0, 8), + hex.slice(8, 12), + "4" + hex.slice(13, 16), + ((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20), + hex.slice(20, 32), + ] + .join("-") + .toUpperCase(); +} diff --git a/js/src/wasm-bridge.ts b/js/src/wasm-bridge.ts new file mode 100644 index 0000000..8497fe1 --- /dev/null +++ b/js/src/wasm-bridge.ts @@ -0,0 +1,202 @@ +// Low-level bridge to the Emscripten-generated WASM module. +// Handles all pointer/length marshalling so higher layers never touch raw memory. + +export interface StartProvisioningResult { + cpim: Uint8Array; + session: number; +} + +export interface RequestOtpResult { + otp: Uint8Array; + machineId: Uint8Array; +} + +export class WasmBridge { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private m: any; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(wasmModule: any) { + this.m = wasmModule; + } + + // ---- memory helpers ---- + + private allocBytes(bytes: Uint8Array): number { + const ptr = this.m._malloc(bytes.length) as number; + this.m.HEAPU8.set(bytes, ptr); + return ptr; + } + + private allocCString(value: string | null | undefined): number { + if (!value) return 0; + const size = (this.m.lengthBytesUTF8(value) as number) + 1; + const ptr = this.m._malloc(size) as number; + this.m.stringToUTF8(value, ptr, size); + return ptr; + } + + private readBytes(ptr: number, len: number): Uint8Array { + if (!ptr || !len) return new Uint8Array(0); + return (this.m.HEAPU8 as Uint8Array).slice(ptr, ptr + len); + } + + private free(ptr: number): void { + if (ptr) this.m._free(ptr); + } + + // ---- error handling ---- + + getLastError(): string { + const ptr = this.m._anisette_last_error_ptr() as number; + const len = this.m._anisette_last_error_len() as number; + if (!ptr || !len) return ""; + const bytes = (this.m.HEAPU8 as Uint8Array).subarray(ptr, ptr + len); + return new TextDecoder("utf-8").decode(bytes); + } + + private check(result: number, context: string): void { + if (result !== 0) { + const msg = this.getLastError(); + throw new Error(`${context}: ${msg || "unknown error"}`); + } + } + + // ---- public API ---- + + /** + * Initialize ADI from in-memory library blobs. + */ + initFromBlobs( + storeservices: Uint8Array, + coreadi: Uint8Array, + libraryPath: string, + provisioningPath?: string, + identifier?: string + ): void { + const ssPtr = this.allocBytes(storeservices); + const caPtr = this.allocBytes(coreadi); + const libPtr = this.allocCString(libraryPath); + const provPtr = this.allocCString(provisioningPath ?? null); + const idPtr = this.allocCString(identifier ?? null); + + try { + const result = this.m._anisette_init_from_blobs( + ssPtr, + storeservices.length, + caPtr, + coreadi.length, + libPtr, + provPtr, + idPtr + ) as number; + this.check(result, "anisette_init_from_blobs"); + } finally { + this.free(ssPtr); + this.free(caPtr); + this.free(libPtr); + this.free(provPtr); + this.free(idPtr); + } + } + + /** + * Write a file into the WASM virtual filesystem. + */ + writeVirtualFile(filePath: string, data: Uint8Array): void { + const pathPtr = this.allocCString(filePath); + const dataPtr = this.allocBytes(data); + try { + const result = this.m._anisette_fs_write_file( + pathPtr, + dataPtr, + data.length + ) as number; + this.check(result, `anisette_fs_write_file(${filePath})`); + } finally { + this.free(pathPtr); + this.free(dataPtr); + } + } + + /** + * Returns 1 if provisioned, 0 if not, throws on error. + */ + isMachineProvisioned(dsid: bigint): boolean { + const result = this.m._anisette_is_machine_provisioned(dsid) as number; + if (result < 0) { + throw new Error( + `anisette_is_machine_provisioned: ${this.getLastError()}` + ); + } + return result === 1; + } + + /** + * Start provisioning — returns CPIM bytes and session handle. + */ + startProvisioning( + dsid: bigint, + spim: Uint8Array + ): StartProvisioningResult { + const spimPtr = this.allocBytes(spim); + try { + const result = this.m._anisette_start_provisioning( + dsid, + spimPtr, + spim.length + ) as number; + this.check(result, "anisette_start_provisioning"); + } finally { + this.free(spimPtr); + } + + const cpimPtr = this.m._anisette_get_cpim_ptr() as number; + const cpimLen = this.m._anisette_get_cpim_len() as number; + const session = this.m._anisette_get_session() as number; + + return { + cpim: this.readBytes(cpimPtr, cpimLen), + session, + }; + } + + /** + * Finish provisioning with PTM and TK from Apple servers. + */ + endProvisioning(session: number, ptm: Uint8Array, tk: Uint8Array): void { + const ptmPtr = this.allocBytes(ptm); + const tkPtr = this.allocBytes(tk); + try { + const result = this.m._anisette_end_provisioning( + session, + ptmPtr, + ptm.length, + tkPtr, + tk.length + ) as number; + this.check(result, "anisette_end_provisioning"); + } finally { + this.free(ptmPtr); + this.free(tkPtr); + } + } + + /** + * Request OTP — returns OTP bytes and machine ID bytes. + */ + requestOtp(dsid: bigint): RequestOtpResult { + const result = this.m._anisette_request_otp(dsid) as number; + this.check(result, "anisette_request_otp"); + + const otpPtr = this.m._anisette_get_otp_ptr() as number; + const otpLen = this.m._anisette_get_otp_len() as number; + const midPtr = this.m._anisette_get_mid_ptr() as number; + const midLen = this.m._anisette_get_mid_len() as number; + + return { + otp: this.readBytes(otpPtr, otpLen), + machineId: this.readBytes(midPtr, midLen), + }; + } +} diff --git a/js/src/wasm-loader.ts b/js/src/wasm-loader.ts new file mode 100644 index 0000000..e4f3536 --- /dev/null +++ b/js/src/wasm-loader.ts @@ -0,0 +1,27 @@ +// Loads the Emscripten WASM glue bundled alongside this file. +// The .wasm binary is resolved relative to this JS file at runtime. + +// @ts-ignore — glue file is generated, no types available +import ModuleFactory from "../../dist/anisette_rs.node.js"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// Resolve the .wasm file next to the bundled output JS +function resolveWasmPath(outputFile: string): string { + // __filename of the *bundled* output — bun sets import.meta.url correctly + const dir = path.dirname(fileURLToPath(import.meta.url)); + return path.join(dir, outputFile); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function loadWasm(): Promise { + return ModuleFactory({ + locateFile(file: string) { + if (file.endsWith(".wasm")) { + return resolveWasmPath("anisette_rs.node.wasm"); + } + return file; + }, + }); +} diff --git a/js/tsconfig.json b/js/tsconfig.json new file mode 100644 index 0000000..26e12de --- /dev/null +++ b/js/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/package.json b/package.json index 576c450..ae7ea29 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "build:unicorn": "bash script/rebuild-unicorn.sh", "build:glue": "bash script/build-glue.sh", "build:release": "bash script/build-glue.sh --release", - "run:node": "bun test/run-node.mjs" + "build:js": "cd js && bun install && bun run build", + "build": "bash script/build-glue.sh && cd js && bun install && bun run build", + "run:node": "node example/run-node.mjs", + "run:api": "node example/anisette-api.mjs" } } diff --git a/script/build-glue.sh b/script/build-glue.sh index 223ef55..964b6f2 100755 --- a/script/build-glue.sh +++ b/script/build-glue.sh @@ -95,6 +95,18 @@ echo " ${DIST_DIR}/anisette_rs.wasm" echo " ${NODE_DIST_JS}" echo " ${NODE_DIST_WASM}" +# Bundle TS API + glue into a single JS file +JS_DIR="${ROOT_DIR}/js" +if command -v bun >/dev/null 2>&1 && [[ -f "${JS_DIR}/src/index.ts" ]]; then + echo "bundling TS API..." + bun build "${JS_DIR}/src/index.ts" \ + --outfile "${DIST_DIR}/anisette.js" \ + --target node \ + --format esm \ + --minify + echo " ${DIST_DIR}/anisette.js" +fi + # Copy to frontend if directory exists (skip in CI if not present) if [[ -d "${ROOT_DIR}/../../frontend/public/anisette" ]]; then cp "${DIST_DIR}/anisette_rs.js" "${ROOT_DIR}/../../frontend/public/anisette/anisette_rs.js" diff --git a/src/debug.rs b/src/debug.rs index 5ed529d..b8e13e9 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -33,25 +33,25 @@ pub(crate) fn trace_mem_invalid_hook( let pc = reg_or_zero(uc, RegisterARM64::PC); match access { MemType::READ_UNMAPPED => { - debug_print(format!( + println!( ">>> Missing memory is being READ at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", value as u64 - )); + ); dump_registers(uc, "read unmapped"); } MemType::WRITE_UNMAPPED => { - debug_print(format!( + println!( ">>> Missing memory is being WRITE at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", value as u64 - )); + ); dump_registers(uc, "write unmapped"); } MemType::FETCH_UNMAPPED => { - debug_print(format!( + println!( ">>> Missing memory is being FETCH at 0x{address:x}, data size = {size}, data value = 0x{:x}, PC=0x{pc:x}", value as u64 - )); + ); } _ => {} }