├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── docker-publish-anisette-server.yml │ ├── cmake.yml │ └── cmake-cross-compile.yml ├── .gitmodules ├── lib └── provision │ ├── package.d │ ├── symbols.d │ ├── androidlibrary.d │ └── adi.d ├── mkcassette ├── constants.d └── app.d ├── anisette_server ├── constants.d └── app.d ├── dub.selections.json ├── cmake └── dependencies.cmake ├── Dockerfile ├── dub.sdl ├── toolchains ├── rpi-4-no-suffix.cmake ├── rpi-4.cmake ├── rpi-2-no-suffix.cmake └── rpi-2.cmake ├── retrieve_headers └── app.d ├── CMakeLists.txt ├── README.md └── LISEZMOI.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | .dub/ 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ["Dadoum"] 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "cmake/cmake-d"] 2 | path = cmake/cmake-d 3 | url = https://github.com/Dadoum/cmake-d 4 | -------------------------------------------------------------------------------- /lib/provision/package.d: -------------------------------------------------------------------------------- 1 | module provision; 2 | 3 | enum provisionVersion = "2.1.0"; 4 | public import provision.adi; 5 | -------------------------------------------------------------------------------- /mkcassette/constants.d: -------------------------------------------------------------------------------- 1 | module constants; 2 | 3 | enum mkcassetteBranding = "mkcassette"; 4 | enum nativesUrl = "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk"; 5 | -------------------------------------------------------------------------------- /anisette_server/constants.d: -------------------------------------------------------------------------------- 1 | module constants; 2 | 3 | enum anisetteServerBranding = "anisette-server-provision"; 4 | enum nativesUrl = "https://apps.mzstatic.com/content/android-apple-music-apk/applemusic.apk"; 5 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "dxml": "0.4.3", 5 | "handy-httpd": "5.2.1", 6 | "httparsed": "1.2.1", 7 | "plist": "~master", 8 | "slf4d": "2.1.1" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cmake/dependencies.cmake: -------------------------------------------------------------------------------- 1 | # ============================================================================== 2 | # Fetching external libraries 3 | 4 | include(UseDub) 5 | 6 | DubProject_Add(slf4d ~2.1.1) 7 | 8 | if(build_anisetteserver) 9 | DubProject_Add(handy-httpd ~5.2.1) 10 | endif() -------------------------------------------------------------------------------- /.github/workflows/docker-publish-anisette-server.yml: -------------------------------------------------------------------------------- 1 | name: Publish docker image anisette_server 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up QEMU 13 | uses: docker/setup-qemu-action@v2 14 | - name: Set up Docker Buildx 15 | uses: docker/setup-buildx-action@v2 16 | - name: Login to DockerHub 17 | uses: docker/login-action@v2 18 | with: 19 | username: ${{ secrets.DOCKERHUB_USERNAME }} 20 | password: ${{ secrets.DOCKERHUB_TOKEN }} 21 | - name: Build and push 22 | uses: docker/build-push-action@v3 23 | with: 24 | push: true 25 | tags: | 26 | dadoum/anisette-server:latest 27 | platforms: linux/386,linux/amd64,linux/arm/v7,linux/arm64/v8 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Base for builder 2 | FROM debian:unstable-slim AS builder 3 | # Deps for builder 4 | RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates ldc git clang dub libz-dev \ 5 | && apt-get clean \ 6 | && rm -rf /var/lib/apt/lists/* 7 | 8 | # Build for builder 9 | WORKDIR /opt/ 10 | COPY lib/ lib/ 11 | COPY anisette_server/ anisette_server/ 12 | COPY dub.sdl dub.selections.json ./ 13 | RUN dub build -c "static" --build-mode allAtOnce -b release --compiler=ldc2 :anisette-server 14 | 15 | # Base for run 16 | FROM debian:unstable-slim 17 | RUN apt-get update && apt-get install --no-install-recommends -y ca-certificates curl \ 18 | && apt-get clean \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Copy build artefacts to run 22 | WORKDIR /opt/ 23 | COPY --from=builder /opt/bin/provision_anisette-server /opt/anisette_server 24 | 25 | # Setup rootless user which works with the volume mount 26 | RUN useradd -ms /bin/bash Chester \ 27 | && mkdir /home/Chester/.config/Provision/lib/ -p \ 28 | && chown -R Chester /home/Chester/ \ 29 | && chmod -R +wx /home/Chester/ \ 30 | && chown -R Chester /opt/ \ 31 | && chmod -R +wx /opt/ 32 | 33 | # Run the artefact 34 | USER Chester 35 | EXPOSE 6969 36 | ENTRYPOINT [ "/opt/anisette_server", "-r=true" ] 37 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: x86_64 builds 2 | 3 | on: push 4 | 5 | env: 6 | BUILD_TYPE: Release 7 | 8 | jobs: 9 | build-anisette-server-x86_64: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | submodules: recursive 16 | 17 | - name: Install dependencies 18 | run: sudo apt-get update && sudo apt-get install -y gdc-12 dub libz-dev 19 | 20 | - name: Build 21 | run: dub build :anisette-server -b release --compiler=gdc-12 -c "static" 22 | 23 | - name: Rename 24 | run: mv "${{github.workspace}}/bin/provision_anisette-server" "${{github.workspace}}/bin/anisette-server-x86_64" 25 | 26 | - uses: actions/upload-artifact@v3 27 | with: 28 | name: anisette-server-x86_64 29 | path: | 30 | ${{github.workspace}}/bin/anisette-server-x86_64 31 | 32 | build-anisette-server-i686: 33 | runs-on: ubuntu-22.04 34 | 35 | steps: 36 | - uses: actions/checkout@v3 37 | with: 38 | submodules: recursive 39 | 40 | - name: Install dependencies 41 | run: sudo apt-get update && sudo apt-get install -y gdc-12-i686-linux-gnu dub libz-dev 42 | 43 | - name: Build 44 | run: dub build :anisette-server -b release --compiler=i686-linux-gnu-gdc-12 -c "static" 45 | 46 | - name: Rename 47 | run: mv "${{github.workspace}}/bin/provision_anisette-server" "${{github.workspace}}/bin/anisette-server-i686" 48 | 49 | - uses: actions/upload-artifact@v3 50 | with: 51 | name: anisette-server-i686 52 | path: | 53 | ${{github.workspace}}/bin/anisette-server-i686 54 | 55 | -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "provision" 2 | description "Set of tools interracting with Apple servers." 3 | authors "Dadoum" 4 | copyright "Copyright © 2023, Dadoum" 5 | 6 | targetType "library" 7 | targetPath "bin" 8 | 9 | sourcePaths "lib" 10 | importPaths "lib" 11 | 12 | dependency "slf4d" version="~>2.1.1" 13 | 14 | configuration "plist-d" { 15 | dependency "plist" version="~master" 16 | } 17 | 18 | configuration "libplist" { 19 | versions "LibPlist" 20 | dependency "plist-d" repository="git+https://github.com/Dadoum/libplist-d.git" version="d494cf3fe79a2bb20583173c0c8cf85ef33b719e" 21 | } 22 | 23 | subPackage { 24 | name "retrieve-headers" 25 | targetType "executable" 26 | targetPath "bin" 27 | 28 | workingDirectory "bin" 29 | 30 | sourcePaths "retrieve_headers" 31 | 32 | dependency "provision" version="*" 33 | } 34 | 35 | subPackage { 36 | name "mkcassette" 37 | targetType "executable" 38 | targetPath "bin" 39 | 40 | workingDirectory "bin" 41 | 42 | sourcePaths "mkcassette" 43 | 44 | dependency "provision" version="*" 45 | } 46 | 47 | subPackage { 48 | name "anisette-server" 49 | targetType "executable" 50 | targetPath "bin" 51 | 52 | workingDirectory "bin" 53 | 54 | sourcePaths "anisette_server" 55 | 56 | dependency "provision" version="*" 57 | dependency "handy-httpd" version="~>5.2.1" 58 | 59 | configuration "default" { 60 | targetType "executable" 61 | } 62 | 63 | configuration "static" { 64 | targetType "executable" 65 | lflags "-lz" platform="ldc" 66 | dflags "--link-defaultlib-shared=false" platform="ldc" 67 | dflags "-defaultlib=:libgphobos.a" platform="gdc" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/cmake-cross-compile.yml: -------------------------------------------------------------------------------- 1 | name: ARM builds 2 | 3 | on: push 4 | 5 | env: 6 | BUILD_TYPE: Release 7 | VERBOSE: 1 8 | 9 | jobs: 10 | build-anisette-server-aarch64: 11 | runs-on: ubuntu-22.04 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | submodules: recursive 17 | 18 | - name: Install dependencies 19 | run: sudo apt-get update && sudo apt-get install -y gdc-12-aarch64-linux-gnu dub libz-dev 20 | 21 | - name: Build 22 | run: dub build :anisette-server -b release --compiler=aarch64-linux-gnu-gdc-12 -c "static" 23 | 24 | - name: Rename 25 | run: mv "${{github.workspace}}/bin/provision_anisette-server" "${{github.workspace}}/bin/anisette-server-aarch64" 26 | 27 | - uses: actions/upload-artifact@v3 28 | with: 29 | name: anisette-server-aarch64 30 | path: | 31 | ${{github.workspace}}/bin/anisette-server-aarch64 32 | 33 | build-anisette-server-armv7: 34 | runs-on: ubuntu-22.04 35 | 36 | steps: 37 | - uses: actions/checkout@v3 38 | with: 39 | submodules: recursive 40 | 41 | - name: Install dependencies 42 | run: sudo apt-get update && sudo apt-get install -y gdc-12-arm-linux-gnueabihf dub libz-dev 43 | 44 | - name: Build 45 | run: dub build :anisette-server -b release --compiler=arm-linux-gnueabihf-gdc-12 -c "static" 46 | 47 | - name: Rename 48 | run: mv "${{github.workspace}}/bin/provision_anisette-server" "${{github.workspace}}/bin/anisette-server-armv7" 49 | 50 | - uses: actions/upload-artifact@v3 51 | with: 52 | name: anisette-server-armv7 53 | path: | 54 | ${{github.workspace}}/bin/anisette-server-armv7 55 | -------------------------------------------------------------------------------- /toolchains/rpi-4-no-suffix.cmake: -------------------------------------------------------------------------------- 1 | #File raspberrytoolchain.cmake for ROS and system packages to cross compile. 2 | SET(CMAKE_SYSTEM_NAME Linux) 3 | 4 | SET(_CMAKE_TOOLCHAIN_PREFIX "aarch64-linux-gnu-") 5 | 6 | SET(CMAKE_C_COMPILER aarch64-linux-gnu-gcc) 7 | SET(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++) 8 | SET(CMAKE_D_COMPILER aarch64-linux-gnu-gdc) 9 | 10 | # Below call is necessary to avoid non-RT problem. 11 | SET(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu) 12 | 13 | SET(RASPBERRY_ROOT_PATH /usr/aarch64-linux-gnu/sys-root) 14 | SET(CMAKE_SYSROOT ${RASPBERRY_ROOT_PATH}) 15 | 16 | SET(CMAKE_FIND_ROOT_PATH ${RASPBERRY_ROOT_PATH}) 17 | 18 | #Have to set this one to BOTH, to allow CMake to find rospack 19 | #This set of variables controls whether the CMAKE_FIND_ROOT_PATH and CMAKE_SYSROOT are used for find_xxx() operations. 20 | SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) 21 | SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 22 | # SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 23 | SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 24 | 25 | #If you have installed cross compiler to somewhere else, please specify that path. 26 | SET(COMPILER_ROOT ${RASPBERRY_ROOT_PATH}) 27 | 28 | SET(CMAKE_PREFIX_PATH ${RASPBERRY_ROOT_PATH}) 29 | 30 | SET(CMAKE_D_FLAGS "${CMAKE_D_FLAGS} -defaultlib=:libgphobos.a -fall-instantiations" CACHE INTERNAL "" FORCE) 31 | # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 32 | # SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 33 | # SET(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 34 | # SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 35 | 36 | SET(LD_LIBRARY_PATH ${RASPBERRY_ROOT_PATH}/lib) 37 | -------------------------------------------------------------------------------- /toolchains/rpi-4.cmake: -------------------------------------------------------------------------------- 1 | #File raspberrytoolchain.cmake for ROS and system packages to cross compile. 2 | SET(CMAKE_SYSTEM_NAME Linux) 3 | 4 | SET(_CMAKE_TOOLCHAIN_PREFIX "aarch64-linux-gnu-") 5 | 6 | SET(CMAKE_C_COMPILER aarch64-linux-gnu-gcc-12) 7 | SET(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++-12) 8 | SET(CMAKE_D_COMPILER aarch64-linux-gnu-gdc-12) 9 | 10 | # Below call is necessary to avoid non-RT problem. 11 | SET(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu) 12 | 13 | SET(RASPBERRY_ROOT_PATH /usr/aarch64-linux-gnu/sys-root) 14 | SET(CMAKE_SYSROOT ${RASPBERRY_ROOT_PATH}) 15 | 16 | SET(CMAKE_FIND_ROOT_PATH ${RASPBERRY_ROOT_PATH}) 17 | 18 | #Have to set this one to BOTH, to allow CMake to find rospack 19 | #This set of variables controls whether the CMAKE_FIND_ROOT_PATH and CMAKE_SYSROOT are used for find_xxx() operations. 20 | SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) 21 | SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 22 | # SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 23 | SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 24 | 25 | #If you have installed cross compiler to somewhere else, please specify that path. 26 | SET(COMPILER_ROOT ${RASPBERRY_ROOT_PATH}) 27 | 28 | SET(CMAKE_PREFIX_PATH ${RASPBERRY_ROOT_PATH}) 29 | 30 | SET(CMAKE_D_FLAGS "${CMAKE_D_FLAGS} -defaultlib=:libgphobos.a -fall-instantiations" CACHE INTERNAL "" FORCE) 31 | # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 32 | # SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 33 | # SET(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 34 | # SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 35 | 36 | SET(LD_LIBRARY_PATH ${RASPBERRY_ROOT_PATH}/lib) 37 | -------------------------------------------------------------------------------- /toolchains/rpi-2-no-suffix.cmake: -------------------------------------------------------------------------------- 1 | #File raspberrytoolchain.cmake for ROS and system packages to cross compile. 2 | SET(CMAKE_SYSTEM_NAME Linux) 3 | 4 | SET(_CMAKE_TOOLCHAIN_PREFIX "arm-linux-gnueabihf-") 5 | 6 | SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) 7 | SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++) 8 | SET(CMAKE_D_COMPILER arm-linux-gnueabihf-gdc) 9 | 10 | # Below call is necessary to avoid non-RT problem. 11 | SET(CMAKE_LIBRARY_ARCHITECTURE arm-linux-gnueabihf) 12 | 13 | SET(RASPBERRY_ROOT_PATH /usr/arm-linux-gnueabihf/sys-root) 14 | SET(CMAKE_SYSROOT ${RASPBERRY_ROOT_PATH}) 15 | 16 | SET(CMAKE_FIND_ROOT_PATH ${RASPBERRY_ROOT_PATH}) 17 | 18 | #Have to set this one to BOTH, to allow CMake to find rospack 19 | #This set of variables controls whether the CMAKE_FIND_ROOT_PATH and CMAKE_SYSROOT are used for find_xxx() operations. 20 | SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) 21 | SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 22 | # SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 23 | SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 24 | 25 | #If you have installed cross compiler to somewhere else, please specify that path. 26 | SET(COMPILER_ROOT ${RASPBERRY_ROOT_PATH}) 27 | 28 | SET(CMAKE_PREFIX_PATH ${RASPBERRY_ROOT_PATH}) 29 | 30 | SET(CMAKE_D_FLAGS "${CMAKE_D_FLAGS} -defaultlib=:libgphobos.a -fall-instantiations" CACHE INTERNAL "" FORCE) 31 | # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 32 | # SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 33 | # SET(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 34 | # SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 35 | 36 | SET(LD_LIBRARY_PATH ${RASPBERRY_ROOT_PATH}/lib) 37 | -------------------------------------------------------------------------------- /toolchains/rpi-2.cmake: -------------------------------------------------------------------------------- 1 | #File raspberrytoolchain.cmake for ROS and system packages to cross compile. 2 | SET(CMAKE_SYSTEM_NAME Linux) 3 | 4 | SET(_CMAKE_TOOLCHAIN_PREFIX "arm-linux-gnueabihf-") 5 | 6 | SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc-12) 7 | SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++-12) 8 | SET(CMAKE_D_COMPILER arm-linux-gnueabihf-gdc-12) 9 | 10 | # Below call is necessary to avoid non-RT problem. 11 | SET(CMAKE_LIBRARY_ARCHITECTURE arm-linux-gnueabihf) 12 | 13 | SET(RASPBERRY_ROOT_PATH /usr/arm-linux-gnueabihf/sys-root) 14 | SET(CMAKE_SYSROOT ${RASPBERRY_ROOT_PATH}) 15 | 16 | SET(CMAKE_FIND_ROOT_PATH ${RASPBERRY_ROOT_PATH}) 17 | 18 | #Have to set this one to BOTH, to allow CMake to find rospack 19 | #This set of variables controls whether the CMAKE_FIND_ROOT_PATH and CMAKE_SYSROOT are used for find_xxx() operations. 20 | SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM BOTH) 21 | SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 22 | # SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 23 | SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 24 | 25 | #If you have installed cross compiler to somewhere else, please specify that path. 26 | SET(COMPILER_ROOT ${RASPBERRY_ROOT_PATH}) 27 | 28 | SET(CMAKE_PREFIX_PATH ${RASPBERRY_ROOT_PATH}) 29 | 30 | SET(CMAKE_D_FLAGS "${CMAKE_D_FLAGS} -defaultlib=:libgphobos.a -fall-instantiations" CACHE INTERNAL "" FORCE) 31 | # SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 32 | # SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 33 | # SET(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 34 | # SET(CMAKE_CXX_LINK_FLAGS "${CMAKE_CXX_LINK_FLAGS} --sysroot=${RASPBERRY_ROOT_PATH}" CACHE INTERNAL "" FORCE) 35 | 36 | SET(LD_LIBRARY_PATH ${RASPBERRY_ROOT_PATH}/lib) 37 | -------------------------------------------------------------------------------- /retrieve_headers/app.d: -------------------------------------------------------------------------------- 1 | module app; 2 | 3 | import std.array; 4 | import std.base64; 5 | import std.conv: to; 6 | import std.format; 7 | import std.path; 8 | import file = std.file; 9 | import provision; 10 | import slf4d; 11 | 12 | static import std.stdio; 13 | 14 | version (X86_64) { 15 | enum string architectureIdentifier = "x86_64"; 16 | } else version (X86) { 17 | enum string architectureIdentifier = "x86"; 18 | } else version (AArch64) { 19 | enum string architectureIdentifier = "arm64-v8a"; 20 | } else version (ARM) { 21 | enum string architectureIdentifier = "armeabi-v7a"; 22 | } else { 23 | static assert(false, "Architecture not supported :("); 24 | } 25 | 26 | int main(string[] args) { 27 | if (args.length == 2 && args[1] == "debug") { 28 | import slf4d.default_provider; 29 | auto provider = new shared DefaultProvider(true, Levels.DEBUG); 30 | configureLoggingProvider(provider); 31 | } 32 | 33 | string configurationPath = expandTilde("~/.config/Provision/"); 34 | if (!file.exists(configurationPath)) { 35 | file.mkdirRecurse(configurationPath); 36 | } 37 | 38 | ADI adi = new ADI("lib/" ~ architectureIdentifier); 39 | adi.provisioningPath = configurationPath; 40 | Device device = new Device(configurationPath.buildPath("device.json")); 41 | 42 | Logger log = getLogger(); 43 | 44 | if (!device.initialized) { 45 | log.info("Creating machine... "); 46 | 47 | import std.digest; 48 | import std.random; 49 | import std.range; 50 | import std.uni; 51 | import std.uuid; 52 | device.serverFriendlyDescription = " "; 53 | device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); 54 | device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); 55 | device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); 56 | 57 | log.info("Machine creation done!"); 58 | } 59 | 60 | adi.identifier = device.adiIdentifier; 61 | if (!adi.isMachineProvisioned(-2)) { 62 | log.info("Machine requires provisioning... "); 63 | 64 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 65 | provisioningSession.provision(-2); 66 | log.info("Provisioning done!"); 67 | } 68 | 69 | auto otp = adi.requestOTP(-2); 70 | 71 | import std.datetime.systime; 72 | auto time = Clock.currTime(); 73 | 74 | std.stdio.writeln( 75 | format!`{ 76 | "X-Apple-I-MD": "%s", 77 | "X-Apple-I-MD-M": "%s", 78 | "X-Apple-I-MD-RINFO": "%d", 79 | "X-Apple-I-MD-LU": "%s", 80 | "X-Apple-I-SRL-NO": "%s", 81 | "X-Mme-Client-Info": "%s", 82 | "X-Apple-I-Client-Time": "%s", 83 | "X-Apple-I-TimeZone": "%s", 84 | "X-Apple-Locale": "en_US", 85 | "X-Mme-Device-Id": "%s" 86 | }`( 87 | Base64.encode(otp.oneTimePassword), 88 | Base64.encode(otp.machineIdentifier), 89 | 17106176, 90 | device.localUserUUID, 91 | "0", 92 | device.serverFriendlyDescription, 93 | time.toISOExtString.split('.')[0] ~ "Z", 94 | time.timezone.dstName, 95 | device.uniqueDeviceIdentifier 96 | ) 97 | ); 98 | 99 | return 0; 100 | } 101 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.20) 2 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake-d/cmake-d) 3 | 4 | project(Provision D) 5 | option(build_anisetteserver "Build Anisette server" ON) 6 | option(build_mkcassette "Build mkcassette" OFF) 7 | option(use_native_plist "Want dlang plist" OFF) 8 | option(link_libplist_dynamic "Load libplist at runtime" OFF) 9 | 10 | include(cmake/dependencies.cmake) 11 | 12 | set(PROVISION_SOURCE_DIR "lib/") 13 | file(GLOB_RECURSE PROVISION_D_SOURCES "${PROVISION_SOURCE_DIR}*.d") 14 | 15 | find_package(PkgConfig REQUIRED) 16 | 17 | add_library(provision STATIC ${PROVISION_D_SOURCES}) 18 | target_include_directories(provision PUBLIC ${PROVISION_SOURCE_DIR}) 19 | 20 | if (link_libplist_dynamic) 21 | target_compile_versions(provision PUBLIC LibPlist) 22 | 23 | include(UseDub) 24 | FetchContent_Declare( 25 | plist_proj 26 | GIT_REPOSITORY https://github.com/Dadoum/libplist-d 27 | GIT_TAG d494cf3fe79a2bb20583173c0c8cf85ef33b719e 28 | PATCH_COMMAND ${DUB_DIRECTORY}/CMakeTmp/DubToCMake -c dynamic -s plist-d 29 | ) 30 | FetchContent_MakeAvailable(plist_proj) 31 | 32 | add_library(plist ALIAS plist-d) 33 | else() 34 | if(NOT CMAKE_CROSSCOMPILING) 35 | pkg_check_modules(plist IMPORTED_TARGET libplist-2.0) 36 | 37 | if (NOT plist_FOUND) 38 | pkg_check_modules(plist IMPORTED_TARGET libplist) 39 | endif() 40 | endif() 41 | 42 | if (plist_FOUND AND NOT use_native_plist) 43 | message("Using libplist. ") 44 | target_compile_versions(provision PUBLIC LibPlist) 45 | 46 | include(UseDub) 47 | FetchContent_Declare( 48 | plist_proj 49 | GIT_REPOSITORY https://github.com/Dadoum/libplist-d 50 | GIT_TAG d494cf3fe79a2bb20583173c0c8cf85ef33b719e 51 | PATCH_COMMAND ${DUB_DIRECTORY}/CMakeTmp/DubToCMake -c static -s plist-d 52 | ) 53 | FetchContent_MakeAvailable(plist_proj) 54 | 55 | add_library(plist INTERFACE) 56 | target_link_libraries(plist INTERFACE PkgConfig::plist plist-d) 57 | else() 58 | message(WARNING "Using fallback Property list parser. ") 59 | 60 | include(UseDub) 61 | FetchContent_Declare( 62 | plist_proj 63 | GIT_REPOSITORY https://github.com/hatf0/plist 64 | PATCH_COMMAND ${DUB_DIRECTORY}/CMakeTmp/DubToCMake -s plist 65 | ) 66 | FetchContent_MakeAvailable(plist_proj) 67 | endif() 68 | endif() 69 | 70 | target_link_libraries(provision PUBLIC plist slf4d) 71 | 72 | set(RETRIEVE_HEADERS_SOURCE_DIR "retrieve_headers/") 73 | file(GLOB_RECURSE RETRIEVE_HEADERS_D_SOURCES "${RETRIEVE_HEADERS_SOURCE_DIR}*.d") 74 | 75 | add_executable(retrieve_headers ${RETRIEVE_HEADERS_D_SOURCES}) 76 | target_include_directories(retrieve_headers PUBLIC ${RETRIEVE_HEADERS_SOURCE_DIR}) 77 | target_link_libraries(retrieve_headers provision) 78 | 79 | if(build_anisetteserver) 80 | set(ANISETTE_SERVER_SOURCE_DIR "anisette_server/") 81 | file(GLOB_RECURSE ANISETTE_SERVER_D_SOURCES "${ANISETTE_SERVER_SOURCE_DIR}*.d") 82 | 83 | add_executable(anisette_server ${ANISETTE_SERVER_D_SOURCES}) 84 | target_include_directories(anisette_server PUBLIC ${ANISETTE_SERVER_SOURCE_DIR}) 85 | 86 | target_link_libraries(anisette_server provision handy-httpd) 87 | endif() 88 | 89 | if(build_mkcassette) 90 | set(MKCASSETTE_SOURCE_DIR "mkcassette/") 91 | file(GLOB_RECURSE MKCASSETTE_D_SOURCES "${MKCASSETTE_SOURCE_DIR}*.d") 92 | 93 | add_executable(mkcassette ${MKCASSETTE_D_SOURCES}) 94 | target_include_directories(mkcassette PUBLIC ${MKCASSETTE_SOURCE_DIR}) 95 | 96 | target_link_libraries(mkcassette provision) 97 | endif() 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Provision 2 | 3 | English ⋅ [Français](LISEZMOI.md) 4 | 5 | > **Warning** \ 6 | > Don't use your main Apple account! Prefer a secondary Apple account. \ 7 | > I am NOT responsible if something happens with your Apple account. 8 | 9 | ## What is this ? 10 | 11 | Provision is a set of tools interracting with Apple servers on Linux. 12 | 13 | It includes: 14 | 15 | - *libprovision*, a library used to register device on Apple servers. 16 | - *anisette_server*, an Anisette provisioning server for other software such as 17 | [AltServer-Linux](https://github.com/NyaMisty/AltServer-Linux). 18 | - *retrieve_headers*, which registers the device with libprovision and returns in the terminal in 19 | JSON the headers to use to identify the device on future requests. 20 | 21 | More precisely, libprovision registers the device to Apple servers and retrieve ADI data for your device. 22 | Once you logged in once with your machine, your machine is remembered as safe by Apple using this data 23 | Be careful to log-in only on trusted machines, and don't leak your ADI data, which is stored in `~/.adi/adi.pb`. 24 | 25 | There used to be a software called *sideload-ipa* listed here. The code has now been removed, as it was not working, 26 | and that I am now helping the development of [SideServer]() (no official link yet), which reimplements the full AltServer 27 | with few more features. 28 | 29 | ## Downloads 30 | 31 | You can download the executables in the [Actions](https://github.com/Dadoum/Provision/actions) tab of the project. 32 | 33 | ## Docker container 34 | 35 | If you wish to run Anisette within docker to host a server public or privately. Make sure to install docker/podman 36 | and run the following command: 37 | 38 | ```bash 39 | docker run -d -v provision_config:/home/Chester/.config/Provision/ --restart=always -p 6969:6969 --name anisette dadoum/anisette-server:latest 40 | ``` 41 | 42 | The above command will pull the image and also run it in the background. The volume 43 | (`provision_config:/home/Chester/.config/Provision/`) will cache Provision's configuration folder. 44 | 45 | It contains Apple Music libraries (from Apple, which are not redistributed for legal reasons), ADI file (identifying the 46 | device as a Mac computer for Apple) and Provision's device file (storing the corresponding device information for Provision). 47 | 48 | ## Dependencies 49 | 50 | At runtime, libprovision requires some Apple libraries to be next to the executables. You should 51 | download Apple Music APK (or just the architecture slice for your device if you prefer), and extract 52 | the lib/ folder next to the executables. If you want to reduce further the size, you can remove in the lib/ 53 | folder all the libraries except libstoreservicescore.so and libCoreADI.so, since they are the only one 54 | needed to run the app. 55 | 56 | To build any of these projects, you need the D SDK, with the compiler and dub. As an option, you can also use libplist. 57 | 58 | ## Compilation 59 | 60 | Clone the project and compile it with DUB: 61 | 62 | ```bash 63 | git clone https://github.com/Dadoum/Provision --recursive 64 | cd Provision 65 | dub build -b release 66 | ``` 67 | 68 | or with CMake: 69 | 70 | ```bash 71 | git clone https://github.com/Dadoum/Provision --recursive 72 | cd Provision 73 | mkdir build 74 | cd build 75 | cmake -G Ninja .. -DCMAKE_BUILD_TYPE=Release 76 | ninja 77 | ``` 78 | 79 | ## libprovision usage 80 | 81 | The Provision API tries to be stay close to the AuthKit API, but written in D. 82 | 83 | ```d 84 | import std.digest: toHexString; 85 | import file = std.file; 86 | import std.path: expandTilde, buildPath; 87 | import std.random: rndGen; 88 | import std.range: take, array; 89 | import std.stdio: stderr, write, writeln; 90 | import std.uni: toUpper; 91 | import std.uuid: randomUUID; 92 | 93 | import provision.adi; 94 | 95 | void main() { 96 | string configuration_folder = expandTilde("~/.config/Provision/"); 97 | if (!file.exists(configuration_folder)) { 98 | file.mkdir(configuration_folder); 99 | } 100 | 101 | ADI adi = new ADI("lib/" ~ architectureIdentifier); 102 | adi.provisioningPath = configuration_folder; 103 | Device device = new Device(configuration_folder.buildPath("device.json")); 104 | 105 | if (!device.initialized) { 106 | stderr.write("Creating machine... "); 107 | device.serverFriendlyDescription = " "; 108 | device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); 109 | device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); 110 | device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); 111 | 112 | stderr.writeln("done !"); 113 | } 114 | 115 | adi.identifier = device.adiIdentifier; 116 | if (!adi.isMachineProvisioned(-2)) { 117 | stderr.write("Machine requires provisioning... "); 118 | 119 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 120 | provisioningSession.provision(-2); 121 | stderr.writeln("done !"); 122 | } 123 | 124 | // Do stuff with adi. 125 | } 126 | ``` 127 | 128 | ## Support 129 | 130 | Donations are welcome with GitHub Sponsors. 131 | -------------------------------------------------------------------------------- /LISEZMOI.md: -------------------------------------------------------------------------------- 1 | # Provision 2 | 3 | [English](README.md) ⋅ Français 4 | 5 | > **Warning** **(attention)** \ 6 | > N'utilisez pas votre compte Apple personnel ! Préférez un compte secondaire sans importance. \ 7 | > Je **NE** suis **PAS** responsable si quoi que ce soit advient à votre compte Apple. 8 | 9 | ## Description 10 | 11 | Provision est un jeu d'outils intéragissant avec les serveurs d'Apple sur Linux. 12 | 13 | Cela inclut : 14 | - *libprovision*, utilisé pour enregistrer l'appareil auprès des serveurs d'Apple. 15 | - *anisette_server*, un serveur d'approvisionnement Anisette pour des logiciels tiers comme 16 | [AltServer-Linux](https://github.com/NyaMisty/AltServer-Linux). 17 | - *retrieve_headers*, qui permet d'enregistrer l'appareil avec libprovision et de retourner 18 | les en-têtes HTTP à utiliser pour identifier l'appareil. 19 | 20 | Plus précisément, libprovision enregistre l'appareil auprès d'apple et récupère les données ADI pour celui-ci. 21 | Une fois connecté avec cette machine, les serveurs d'Apple se rappeleront de votre appareil comme sûre, 22 | donc assurez-vous de ne pas vous connecter n'importe où, et à conserver précieusement les données ADI à `~/.adi/adi.pb`. 23 | 24 | Il y avait *sideload-ipa* aussi précédemment. Le code a été retiré, car il ne fonctionnait de toute façon pas 25 | et que j'aide au développement de [SideServer]() (pas de lien officiel pour le moment), qui fonctionnera AltServer là 26 | où il était disponible avec des fonctions en plus. 27 | 28 | ## Téléchargements 29 | 30 | Vous pouvez télécharger les exécutables depuis la page [Actions](https://github.com/Dadoum/Provision/actions) du projet. 31 | 32 | ## Conteneur Docker 33 | 34 | Si vous souhaitez créer un serveur Anisette pour votre usage personnel ou créer un serveur public, vous pouver utiliser 35 | Docker ou Podman. Dans ce cas, il vous suffira de lancer la commande suivante pour configurer un serveur directement : 36 | 37 | ```bash 38 | docker run -d -v provision_config:/home/Chester/.config/Provision/ --restart=always -p 6969:6969 --name anisette dadoum/anisette-server:latest 39 | ``` 40 | 41 | Cette commande récupèrera l'image Docker et l'exécutera immédiatemnt. Le volume 42 | (`provision_config:/home/Chester/.config/Provision/`) mettra en cache la configuration de Provision. 43 | 44 | Cette configuration inclut les bibliothèques d'Apple Music (qui ne sont pas redistribuées pour des raisons légales), le 45 | fichier ADI (identifiant un faux Mac auprès d'Apple) et le fichier d'appareil de Provision (indiquant à Provision les 46 | informations du Mac). 47 | 48 | ## Dépendances 49 | 50 | Pour lancer les programmes vous devrez extraire les bibliothèques de l'application Android Apple 51 | Music. Vous pouvez ne télécharger que la tranche de votre architecture. Placez le dossier lib/ 52 | à côté des exécutables. Dans le dossier avec toutes les bibliothèques (fichiers en .so), vous pouvez 53 | ne garder que libstoreservicescore.so et libCoreADI.so. Ce sont les seuls nécessaires. 54 | 55 | Pour compiler n'importe lequel des projets, vous devez avoir le kit de développement 56 | pour le D (le compilateur + dub). Si vous le souhaitez, vous pouvez aussi opter pour utiliser 57 | libplist. 58 | 59 | ## Compilation 60 | 61 | Clonez le projet et compilez le avec DUB : 62 | 63 | ```bash 64 | git clone https://github.com/Dadoum/Provision --recursive 65 | cd Provision 66 | dub build -b release 67 | ``` 68 | 69 | ou CMake: 70 | 71 | ```bash 72 | git clone https://github.com/Dadoum/Provision --recursive 73 | cd Provision 74 | mkdir build 75 | cd build 76 | cmake -G Ninja .. -DCMAKE_BUILD_TYPE=Release 77 | ninja 78 | ``` 79 | 80 | ## Utilisation de libprovision 81 | 82 | L'interface essaie tant bien que mal de rester proche de celle d'AuthKit, même si Provision est écrit en D. 83 | 84 | ```d 85 | import std.digest: toHexString; 86 | import file = std.file; 87 | import std.path: expandTilde, buildPath; 88 | import std.random: rndGen; 89 | import std.range: take, array; 90 | import std.stdio: stderr, write, writeln; 91 | import std.uni: toUpper; 92 | import std.uuid: randomUUID; 93 | 94 | import provision.adi; 95 | 96 | void main() { 97 | string configuration_folder = expandTilde("~/.config/Provision/"); 98 | if (!file.exists(configuration_folder)) { 99 | file.mkdir(configuration_folder); 100 | } 101 | 102 | ADI adi = new ADI("lib/" ~ architectureIdentifier); 103 | adi.provisioningPath = configuration_folder; 104 | Device device = new Device(configuration_folder.buildPath("device.json")); 105 | 106 | if (!device.initialized) { 107 | stderr.write("Creating machine... "); 108 | device.serverFriendlyDescription = " "; 109 | device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); 110 | device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); 111 | device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); 112 | 113 | stderr.writeln("done !"); 114 | } 115 | 116 | adi.identifier = device.adiIdentifier; 117 | if (!adi.isMachineProvisioned(-2)) { 118 | stderr.write("Machine requires provisioning... "); 119 | 120 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 121 | provisioningSession.provision(-2); 122 | stderr.writeln("done !"); 123 | } 124 | 125 | // Faites ce que vous voulez avec adi ! 126 | } 127 | ``` 128 | 129 | ## Soutien 130 | 131 | Vous pouvez me soutenir en faisant un don avec GitHub Sponsors. 132 | -------------------------------------------------------------------------------- /lib/provision/symbols.d: -------------------------------------------------------------------------------- 1 | module provision.symbols; 2 | 3 | import core.memory; 4 | import core.stdc.errno; 5 | import core.stdc.stdlib; 6 | import core.stdc.string; 7 | import core.sys.posix.fcntl; 8 | import core.sys.posix.sys.stat; 9 | import core.sys.posix.sys.time; 10 | import core.sys.posix.unistd; 11 | import provision.androidlibrary; 12 | import std.algorithm.mutation; 13 | import std.experimental.allocator; 14 | import std.experimental.allocator.mallocator; 15 | import std.random; 16 | import std.string; 17 | import std.traits : Parameters, ReturnType; 18 | 19 | import slf4d; 20 | 21 | __gshared: 22 | 23 | private extern (C) int __system_property_get_impl(const char* n, char* value) { 24 | auto name = n.fromStringz; 25 | 26 | enum str = "no s/n number"; 27 | 28 | value[0 .. str.length] = str; 29 | return cast(int) str.length; 30 | } 31 | 32 | private extern (C) uint arc4random_impl() { 33 | return Random(unpredictableSeed()).front; 34 | } 35 | 36 | private extern (C) int emptyStub() { 37 | return 0; 38 | } 39 | 40 | private extern (C) noreturn undefinedSymbol() { 41 | throw new UndefinedSymbolException(); 42 | } 43 | 44 | private extern (C) AndroidLibrary dlopenWrapper(const char* name) { 45 | debug { 46 | getLogger().traceF!"Attempting to load %s"(name.fromStringz()); 47 | } 48 | try { 49 | auto caller = rootLibrary(); 50 | auto lib = new AndroidLibrary(cast(string) name.fromStringz(), caller.hooks); 51 | caller.loadedLibraries ~= lib; 52 | return lib; 53 | } catch (Throwable) { 54 | return null; 55 | } 56 | } 57 | 58 | private extern (C) void* dlsymWrapper(AndroidLibrary library, const char* symbolName) { 59 | debug { 60 | getLogger().traceF!"Attempting to load symbol %s"(symbolName.fromStringz()); 61 | } 62 | return library.load(cast(string) symbolName.fromStringz()); 63 | } 64 | 65 | private extern (C) void dlcloseWrapper(AndroidLibrary library) { 66 | if (library) { 67 | rootLibrary().loadedLibraries.remove!((lib) => lib == library); 68 | destroy(library); 69 | } 70 | } 71 | 72 | // gperf generated code: 73 | 74 | private enum totalKeywords = 29; 75 | private enum minWordLength = 4; 76 | private enum maxWordLength = 22; 77 | private enum minHashValue = 4; 78 | private enum maxHashValue = 45; 79 | /* maximum key range = 42, duplicates = 0 */ 80 | 81 | pragma(inline, true) private uint hash(string str, uint len) { 82 | static ubyte[] asso_values = [ 83 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 84 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 85 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 86 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 87 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 88 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 10, 46, 25, 46, 3, 0, 20, 10, 89 | 0, 5, 10, 46, 25, 0, 5, 10, 0, 0, 46, 10, 10, 0, 0, 46, 5, 46, 46, 46, 90 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 91 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 92 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 93 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 94 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 95 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 96 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 97 | 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46 98 | ]; 99 | uint hval = len; 100 | 101 | switch (hval) { 102 | default: 103 | hval += asso_values[cast(ubyte) str[15]]; 104 | goto case; 105 | case 15: 106 | case 14: 107 | case 13: 108 | case 12: 109 | case 11: 110 | case 10: 111 | case 9: 112 | case 8: 113 | case 7: 114 | case 6: 115 | case 5: 116 | case 4: 117 | case 3: 118 | case 2: 119 | hval += asso_values[cast(ubyte) str[1]]; 120 | goto case; 121 | case 1: 122 | hval += asso_values[cast(ubyte) str[0]]; 123 | break; 124 | } 125 | return hval; 126 | } 127 | 128 | private struct FunctionPair { 129 | string name; 130 | void* ptr; 131 | } 132 | 133 | package void* lookupSymbol(string str) { 134 | auto len = cast(uint) str.length; 135 | enum FunctionPair[] wordlist = [ 136 | {""}, {""}, {""}, {""}, {"open", &open}, {"dlsym", &dlsymWrapper}, 137 | {"dlopen", &dlopenWrapper}, {"dlclose", &dlcloseWrapper}, 138 | {"close", &close}, {""}, {"umask", &umask}, {""}, 139 | {"pthread_once", &emptyStub}, {"chmod", &chmod}, 140 | {"pthread_create", &emptyStub}, {"lstat", &lstat}, {""}, 141 | {"strncpy", &strncpy}, {"pthread_mutex_lock", &emptyStub}, 142 | {"ftruncate", &ftruncate}, {"write", &write}, 143 | {"pthread_rwlock_unlock", &emptyStub}, 144 | {"pthread_rwlock_destroy", &emptyStub}, {""}, {"free", &free}, 145 | {"fstat", &fstat}, {"pthread_rwlock_wrlock", &emptyStub}, 146 | {"__errno", &errno}, {""}, {"pthread_rwlock_init", &emptyStub}, 147 | {"pthread_mutex_unlock", &emptyStub}, 148 | {"pthread_rwlock_rdlock", &emptyStub}, { 149 | "gettimeofday", 150 | &gettimeofday 151 | }, {""}, {"read", &read}, 152 | {"mkdir", &mkdir}, {"malloc", &malloc}, {""}, {""}, {""}, {""}, 153 | {"__system_property_get", &__system_property_get_impl}, {""}, {""}, 154 | {""}, {"arc4random", &arc4random_impl}, 155 | ]; 156 | 157 | if (len <= maxWordLength && len >= minWordLength) { 158 | uint key = hash(str, len); 159 | 160 | if (key <= maxHashValue) { 161 | string s = wordlist[key].name; 162 | 163 | if (str == s) 164 | return wordlist[key].ptr; 165 | } 166 | } 167 | return &undefinedSymbol; 168 | } 169 | -------------------------------------------------------------------------------- /mkcassette/app.d: -------------------------------------------------------------------------------- 1 | module app; 2 | 3 | import core.sys.posix.sys.time; 4 | import std.algorithm; 5 | import std.array; 6 | import std.base64; 7 | import std.datetime.stopwatch: StopWatch; 8 | import file = std.file; 9 | import std.format; 10 | import std.getopt; 11 | import std.math; 12 | import std.mmfile; 13 | import std.net.curl; 14 | import std.parallelism; 15 | import std.path; 16 | import std.range; 17 | import std.zip; 18 | 19 | import slf4d; 20 | 21 | import provision; 22 | import provision.androidlibrary; 23 | import provision.symbols; 24 | 25 | import constants; 26 | 27 | version (X86_64) { 28 | enum string architectureIdentifier = "x86_64"; 29 | } else version (X86) { 30 | enum string architectureIdentifier = "x86"; 31 | } else version (AArch64) { 32 | enum string architectureIdentifier = "arm64-v8a"; 33 | } else version (ARM) { 34 | enum string architectureIdentifier = "armeabi-v7a"; 35 | } else { 36 | static assert(false, "Architecture not supported :("); 37 | } 38 | 39 | struct AnisetteCassetteHeader { 40 | align(1): 41 | ubyte[7] magicHeader = [0x69, 'C', 'A', 'S', 'S', 'T', 'E']; 42 | ubyte formatVersion = 0; 43 | ulong baseTime; 44 | 45 | ubyte[64] machineId; 46 | } 47 | 48 | static assert(AnisetteCassetteHeader.sizeof % 16 == 0); 49 | 50 | __gshared ulong origTime; 51 | int main(string[] args) { 52 | Logger log = getLogger(); 53 | log.infoF!"%s v%s"(mkcassetteBranding, provisionVersion); 54 | 55 | char[] identifier = cast(char[]) "ba10defe42ea69ff"; 56 | string configurationPath = expandTilde("~/.config/Provision"); 57 | string outputFile = "./otp-file.acs"; 58 | ulong days = 90; 59 | bool onlyInit = false; 60 | bool apkDownloadAllowed = true; 61 | 62 | // Parse command-line arguments 63 | auto helpInformation = getopt( 64 | args, 65 | "i|identifier", format!"The identifier used for the cassette (default: %s)"(identifier), &identifier, 66 | "a|adi-path", format!"Where the provisioning information should be stored on the computer (default: %s)"(configurationPath), &configurationPath, 67 | "d|days", format!"Number of days in the cassette (default: %s)"(days), &days, 68 | "o|output", format!"Output location (default: %s)"(outputFile), &outputFile, 69 | "init-only", format!"Download libraries and exit (default: %s)"(onlyInit), &onlyInit, 70 | "can-download", format!"If turned on, may download the dependencies automatically (default: %s)"(apkDownloadAllowed), &apkDownloadAllowed, 71 | ); 72 | 73 | if (helpInformation.helpWanted) { 74 | defaultGetoptPrinter("This program allows you to host anisette through libprovision!", helpInformation.options); 75 | return 0; 76 | } 77 | 78 | if (!file.exists(configurationPath)) { 79 | file.mkdirRecurse(configurationPath); 80 | } 81 | 82 | string libraryPath = configurationPath.buildPath("lib/" ~ architectureIdentifier); 83 | 84 | auto coreADIPath = libraryPath.buildPath("libCoreADI.so"); 85 | auto SSCPath = libraryPath.buildPath("libstoreservicescore.so"); 86 | 87 | // Download APK if needed 88 | if (!(file.exists(coreADIPath) && file.exists(SSCPath)) && apkDownloadAllowed) { 89 | auto http = HTTP(); 90 | log.info("Downloading libraries from Apple servers..."); 91 | auto apkData = get!(HTTP, ubyte)(nativesUrl, http); 92 | log.info("Done !"); 93 | auto apk = new ZipArchive(apkData); 94 | auto dir = apk.directory(); 95 | 96 | if (!file.exists(libraryPath)) { 97 | file.mkdirRecurse(libraryPath); 98 | } 99 | file.write(coreADIPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libCoreADI.so"])); 100 | file.write(SSCPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libstoreservicescore.so"])); 101 | } 102 | 103 | if (onlyInit) { 104 | return 0; 105 | } 106 | 107 | // 1 per minute 108 | auto numberOfOTP = days*24*60; 109 | 110 | ubyte[] mid; 111 | ubyte[] nothing; 112 | 113 | // We store the real time in a shared variable, and create a thread-local time variable. 114 | __gshared timeval origTimeVal; 115 | gettimeofday(&origTimeVal, null); 116 | origTime = origTimeVal.tv_sec; 117 | targetTime = taskPool.workerLocalStorage(origTimeVal); 118 | 119 | // Initializing ADI and machine if it has not already been made. 120 | Device device = new Device(configurationPath.buildPath("/dev/null")); 121 | { 122 | ADI adi = new ADI("lib/" ~ architectureIdentifier); 123 | adi.provisioningPath = configurationPath; 124 | 125 | if (!device.initialized) { 126 | log.info("Creating machine... "); 127 | 128 | import std.digest; 129 | import std.random; 130 | import std.range; 131 | import std.uni; 132 | import std.uuid; 133 | device.serverFriendlyDescription = " "; 134 | device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); 135 | device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); 136 | device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); 137 | 138 | log.info("Machine creation done!"); 139 | } 140 | 141 | adi.identifier = device.adiIdentifier; 142 | 143 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 144 | provisioningSession.provision(-2); 145 | 146 | mid = adi.requestOTP(-2).machineIdentifier; 147 | } 148 | 149 | auto adi = taskPool().workerLocalStorage!ADI({ 150 | // We hook the gettimeofday function in the library to change the date. 151 | AndroidLibrary storeServicesCore = new AndroidLibrary(SSCPath, [ 152 | "gettimeofday": cast(void*) &gettimeofday_timeTravel 153 | ]); 154 | 155 | ADI adi = new ADI(libraryPath, storeServicesCore); 156 | 157 | adi.provisioningPath = configurationPath; 158 | adi.identifier = device.adiIdentifier; 159 | 160 | return adi; 161 | }()); 162 | 163 | StopWatch sw; 164 | log.infoF!"Starting generation of %d otps (%d days) with %d threads."(numberOfOTP, days, totalCPUs); 165 | sw.start(); 166 | 167 | auto anisetteCassetteHeader = AnisetteCassetteHeader(); 168 | anisetteCassetteHeader.baseTime = origTime; 169 | anisetteCassetteHeader.machineId[0..mid.length] = mid; 170 | 171 | auto anisetteCassetteHeaderBytes = (cast(ubyte*) &anisetteCassetteHeader)[0..AnisetteCassetteHeader.sizeof]; 172 | 173 | // The file consists of 1 header and then all the 16-bytes long OTPs, so we make a memory-mapped file of the correct size. 174 | scope otpFile = new MmFile(outputFile, MmFile.Mode.readWriteNew, AnisetteCassetteHeader.sizeof + 16 * numberOfOTP * ubyte.sizeof, null); 175 | scope acs = cast(ubyte[]) otpFile[0..$]; 176 | acs[0..AnisetteCassetteHeader.sizeof] = anisetteCassetteHeaderBytes; 177 | 178 | // we take every 16 bytes chunk of the OTP part of the file, and iterate concurrently through it. 179 | foreach (idx, otp; parallel(std.range.chunks(cast(ubyte[]) acs[AnisetteCassetteHeader.sizeof..$], 16))) { 180 | scope localAdi = adi.get(); 181 | scope time = targetTime.get(); 182 | 183 | time.tv_sec = origTime + idx * 30; 184 | targetTime.get() = time; 185 | 186 | otp[] = localAdi.requestOTP(-2).oneTimePassword[8..24]; 187 | 188 | assert(targetTime.get().tv_sec == origTime + idx * 30); 189 | } 190 | 191 | sw.stop(); 192 | 193 | log.infoF!"Success. File written at %s, duration %s"(outputFile, sw.peek()); 194 | 195 | return 0; 196 | } 197 | 198 | import core.sys.posix.sys.time; 199 | import std.parallelism; 200 | 201 | public __gshared TaskPool.WorkerLocalStorage!timeval targetTime; 202 | 203 | private extern (C) int gettimeofday_timeTravel(timeval* timeval, void* ptr) { 204 | *timeval = targetTime.get(); 205 | return 0; 206 | } 207 | -------------------------------------------------------------------------------- /anisette_server/app.d: -------------------------------------------------------------------------------- 1 | import handy_httpd; 2 | import handy_httpd.components.request; 3 | import std.algorithm.searching; 4 | import std.array; 5 | import std.base64; 6 | import file = std.file; 7 | import std.format; 8 | import std.getopt; 9 | import std.math; 10 | import std.net.curl; 11 | import std.parallelism; 12 | import std.path; 13 | import std.zip; 14 | 15 | import slf4d; 16 | import slf4d.default_provider; 17 | 18 | import provision; 19 | 20 | import constants; 21 | 22 | version (X86_64) { 23 | enum string architectureIdentifier = "x86_64"; 24 | } else version (X86) { 25 | enum string architectureIdentifier = "x86"; 26 | } else version (AArch64) { 27 | enum string architectureIdentifier = "arm64-v8a"; 28 | } else version (ARM) { 29 | enum string architectureIdentifier = "armeabi-v7a"; 30 | } else { 31 | static assert(false, "Architecture not supported :("); 32 | } 33 | 34 | __gshared bool allowRemoteProvisioning = false; 35 | __gshared ADI adi; 36 | __gshared Device device; 37 | 38 | void main(string[] args) { 39 | debug { 40 | configureLoggingProvider(new shared DefaultProvider(true, Levels.DEBUG)); 41 | } else { 42 | configureLoggingProvider(new shared DefaultProvider(true, Levels.INFO)); 43 | } 44 | 45 | Logger log = getLogger(); 46 | log.infoF!"%s v%s"(anisetteServerBranding, provisionVersion); 47 | auto serverConfig = ServerConfig.defaultValues; 48 | serverConfig.hostname = "0.0.0.0"; 49 | serverConfig.port = 6969; 50 | 51 | bool rememberMachine = true; 52 | string configurationPath = expandTilde("~/.config/Provision"); 53 | bool onlyInit = false; 54 | bool apkDownloadAllowed = true; 55 | auto helpInformation = getopt( 56 | args, 57 | "n|host", format!"The hostname to bind to (default: %s)"(serverConfig.hostname), &serverConfig.hostname, 58 | "p|port", format!"The port to bind to (default: %s)"(serverConfig.hostname), &serverConfig.port, 59 | "r|remember-machine", format!"Whether this machine should be remembered (default: %s)"(rememberMachine), &rememberMachine, 60 | "a|adi-path", format!"Where the provisioning information should be stored on the computer (default: %s)"(configurationPath), &configurationPath, 61 | "init-only", format!"Download libraries and exit (default: %s)"(onlyInit), &onlyInit, 62 | "can-download", format!"If turned on, may download the dependencies automatically (default: %s)"(apkDownloadAllowed), &apkDownloadAllowed, 63 | "allow-remote-reprovisioning", format!"If turned on, the server may reprovision the server on client demand (default: %s)"(allowRemoteProvisioning), &allowRemoteProvisioning, 64 | ); 65 | 66 | if (helpInformation.helpWanted) { 67 | defaultGetoptPrinter("This program allows you to host anisette through libprovision!", helpInformation.options); 68 | return; 69 | } 70 | 71 | if (!file.exists(configurationPath)) { 72 | file.mkdirRecurse(configurationPath); 73 | } 74 | 75 | string libraryPath = configurationPath.buildPath("lib/"); 76 | 77 | auto coreADIPath = libraryPath.buildPath("libCoreADI.so"); 78 | auto SSCPath = libraryPath.buildPath("libstoreservicescore.so"); 79 | 80 | if (!(file.exists(coreADIPath) && file.exists(SSCPath)) && apkDownloadAllowed) { 81 | auto http = HTTP(); 82 | log.info("Downloading libraries from Apple servers..."); 83 | auto apkData = get!(HTTP, ubyte)(nativesUrl, http); 84 | log.info("Done !"); 85 | auto apk = new ZipArchive(apkData); 86 | auto dir = apk.directory(); 87 | 88 | if (!file.exists(libraryPath)) { 89 | file.mkdirRecurse(libraryPath); 90 | } 91 | file.write(coreADIPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libCoreADI.so"])); 92 | file.write(SSCPath, apk.expand(dir["lib/" ~ architectureIdentifier ~ "/libstoreservicescore.so"])); 93 | } 94 | 95 | if (onlyInit) { 96 | return; 97 | } 98 | 99 | // Initializing ADI and machine if it has not already been made. 100 | device = new Device(rememberMachine ? configurationPath.buildPath("device.json") : "/dev/null"); 101 | adi = new ADI(libraryPath); 102 | adi.provisioningPath = configurationPath; 103 | 104 | if (!device.initialized) { 105 | log.info("Creating machine... "); 106 | 107 | import std.digest; 108 | import std.random; 109 | import std.range; 110 | import std.uni; 111 | import std.uuid; 112 | device.serverFriendlyDescription = " "; 113 | device.uniqueDeviceIdentifier = randomUUID().toString().toUpper(); 114 | device.adiIdentifier = (cast(ubyte[]) rndGen.take(2).array()).toHexString().toLower(); 115 | device.localUserUUID = (cast(ubyte[]) rndGen.take(8).array()).toHexString().toUpper(); 116 | 117 | log.info("Machine creation done!"); 118 | } 119 | 120 | enum dsId = -2; 121 | 122 | adi.identifier = device.adiIdentifier; 123 | if (!adi.isMachineProvisioned(dsId)) { 124 | log.info("Machine requires provisioning... "); 125 | 126 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 127 | provisioningSession.provision(dsId); 128 | log.info("Provisioning done!"); 129 | } 130 | 131 | auto s = new HttpServer((ref ctx) { 132 | Logger log = getLogger(); 133 | 134 | auto req = ctx.request; 135 | ctx.response.addHeader("Implementation-Version", anisetteServerBranding ~ " " ~ provisionVersion); 136 | 137 | log.infoF!"[<<] %s %s"(req.method, req.url); 138 | if (req.method != "GET") { 139 | log.info("[>>] 405 Method Not Allowed"); 140 | ctx.response.setStatus(405).setStatusText("Method Not Allowed"); 141 | return; 142 | } 143 | 144 | if (req.url == "/reprovision") { 145 | if (allowRemoteProvisioning) { 146 | ProvisioningSession provisioningSession = new ProvisioningSession(adi, device); 147 | provisioningSession.provision(dsId); 148 | log.info("[>>] 200 OK"); 149 | ctx.response.setStatus(200); 150 | } else { 151 | log.info("[>>] 403 Forbidden"); 152 | ctx.response.setStatus(403).setStatusText("Forbidden"); 153 | } 154 | return; 155 | } 156 | 157 | if (req.url != "") { 158 | log.info("[>>] 404 Not Found"); 159 | ctx.response.setStatus(404).setStatusText("Not Found"); 160 | return; 161 | } 162 | 163 | try { 164 | import std.datetime.systime; 165 | import std.datetime.timezone; 166 | import core.time; 167 | auto time = Clock.currTime(); 168 | 169 | auto otp = adi.requestOTP(dsId); 170 | 171 | import std.conv; 172 | import std.json; 173 | 174 | JSONValue response = [ 175 | "X-Apple-I-Client-Time": time.toISOExtString.split('.')[0] ~ "Z", 176 | "X-Apple-I-MD": Base64.encode(otp.oneTimePassword), 177 | "X-Apple-I-MD-M": Base64.encode(otp.machineIdentifier), 178 | "X-Apple-I-MD-RINFO": to!string(17106176), 179 | "X-Apple-I-MD-LU": device.localUserUUID, 180 | "X-Apple-I-SRL-NO": "0", 181 | "X-MMe-Client-Info": device.serverFriendlyDescription, 182 | "X-Apple-I-TimeZone": time.timezone.dstName, 183 | "X-Apple-Locale": "en_US", 184 | "X-Mme-Device-Id": device.uniqueDeviceIdentifier, 185 | ]; 186 | ctx.response.writeBodyString(response.toString(JSONOptions.doNotEscapeSlashes), "application/json"); 187 | log.infoF!"[>>] 200 OK %s"(response); 188 | } catch(Throwable t) { 189 | string exception = t.toString(); 190 | log.errorF!"Encountered an error: %s"(exception); 191 | log.info("[>>] 500 Internal Server Error"); 192 | ctx.response.writeBodyString(exception); 193 | ctx.response.setStatus(500).setStatusText("Internal Server Error"); 194 | } 195 | }, serverConfig); 196 | 197 | log.info("Ready! Serving data."); 198 | s.start(); 199 | } 200 | -------------------------------------------------------------------------------- /lib/provision/androidlibrary.d: -------------------------------------------------------------------------------- 1 | module provision.androidlibrary; 2 | 3 | import core.exception; 4 | import core.memory; 5 | import core.stdc.errno; 6 | import core.stdc.stdint; 7 | import core.stdc.stdlib; 8 | import core.sys.linux.elf; 9 | import core.sys.linux.link; 10 | import core.sys.posix.sys.mman; 11 | import std.algorithm; 12 | import std.conv; 13 | import std.experimental.allocator; 14 | import std.experimental.allocator.mallocator; 15 | import std.experimental.allocator.mmap_allocator; 16 | import std.functional; 17 | import std.mmfile; 18 | import std.path; 19 | import std.random; 20 | import std.range; 21 | import std.string; 22 | import std.traits; 23 | 24 | import slf4d; 25 | 26 | public class AndroidLibrary { 27 | package MmFile elfFile; 28 | package void[] allocation; 29 | 30 | package char[] sectionNamesTable; 31 | package char[] dynamicStringTable; 32 | package ElfW!"Sym"[] dynamicSymbolTable; 33 | package SymbolHashTable hashTable; 34 | package AndroidLibrary[] loadedLibraries; 35 | 36 | public void*[string] hooks; 37 | 38 | private ElfW!"Shdr"[] relocationSections; 39 | 40 | public this(string libraryName, void*[string] hooks = null) { 41 | elfFile = new MmFile(libraryName); 42 | if (hooks) this.hooks = hooks; 43 | 44 | auto elfHeader = elfFile.identify!(ElfW!"Ehdr")(0); 45 | auto programHeaders = elfFile.identifyArray!(ElfW!"Phdr")(elfHeader.e_phoff, elfHeader.e_phnum); 46 | 47 | size_t minimum = size_t.max; 48 | size_t maximumFile = size_t.min; 49 | size_t maximumMemory = size_t.min; 50 | 51 | size_t headerStart; 52 | size_t headerEnd; 53 | size_t headerMemoryEnd; 54 | 55 | foreach (programHeader; programHeaders) { 56 | if (programHeader.p_type == PT_LOAD) { 57 | headerStart = programHeader.p_vaddr; 58 | headerEnd = programHeader.p_vaddr + programHeader.p_filesz; 59 | headerMemoryEnd = programHeader.p_vaddr + programHeader.p_memsz; 60 | 61 | if (headerStart < minimum) { 62 | minimum = headerStart; 63 | } 64 | if (headerEnd > maximumFile) { 65 | maximumFile = headerEnd; 66 | } 67 | if (headerMemoryEnd > maximumMemory) { 68 | maximumMemory = headerMemoryEnd; 69 | } 70 | } 71 | } 72 | 73 | auto alignedMinimum = pageFloor(minimum); 74 | auto alignedMaximumMemory = pageCeil(maximumMemory); 75 | 76 | auto allocSize = alignedMaximumMemory - alignedMinimum; 77 | auto mmapped_alloc = mmap(null, allocSize, PROT_READ | PROT_WRITE, 78 | MAP_PRIVATE | MAP_ANON, -1, 0); 79 | if (mmapped_alloc == MAP_FAILED) { 80 | throw new LoaderException("Cannot allocate the memory: " ~ to!string(errno)); 81 | } 82 | memoryTable[MemoryBlock(cast(size_t) mmapped_alloc, cast(size_t) mmapped_alloc + allocSize)] = this; 83 | getLogger().traceF!"Allocating %x - %x for %s"(cast(size_t) mmapped_alloc, cast(size_t) mmapped_alloc + allocSize, libraryName); 84 | allocation = mmapped_alloc[0..allocSize]; 85 | 86 | size_t fileStart; 87 | size_t fileEnd; 88 | 89 | foreach (programHeader; programHeaders) { 90 | if (programHeader.p_type == PT_LOAD) { 91 | headerStart = programHeader.p_vaddr; 92 | headerEnd = programHeader.p_vaddr + programHeader.p_filesz; 93 | fileStart = programHeader.p_offset; 94 | fileEnd = programHeader.p_offset + programHeader.p_filesz; 95 | 96 | allocation[headerStart - alignedMinimum..headerEnd - alignedMinimum] = elfFile[fileStart..fileEnd]; 97 | 98 | auto protectionResult = mprotect(allocation.ptr + pageFloor(headerStart), pageCeil(headerEnd) - pageFloor(headerStart), programHeader.memoryProtection()); 99 | 100 | if (protectionResult != 0) { 101 | throw new LoaderException("Cannot protect the memory correctly."); 102 | } 103 | } 104 | } 105 | 106 | auto sectionHeaders = elfFile.identifyArray!(ElfW!"Shdr")(elfHeader.e_shoff, elfHeader.e_shnum); 107 | auto sectionStrTable = sectionHeaders[elfHeader.e_shstrndx]; 108 | sectionNamesTable = cast(char[]) elfFile[sectionStrTable.sh_offset..sectionStrTable.sh_offset + sectionStrTable.sh_size]; 109 | 110 | foreach (sectionHeader; sectionHeaders) { 111 | switch (sectionHeader.sh_type) { 112 | case SHT_DYNSYM: 113 | dynamicSymbolTable = elfFile.identifyArray!(ElfW!"Sym")(sectionHeader.sh_offset, sectionHeader.sh_size / ElfW!"Sym".sizeof); 114 | break; 115 | case SHT_STRTAB: 116 | if (getSectionName(sectionHeader) == ".dynstr") 117 | dynamicStringTable = cast(char[]) elfFile[sectionHeader.sh_offset..sectionHeader.sh_offset + sectionHeader.sh_size]; 118 | break; 119 | case SHT_GNU_HASH: 120 | hashTable = new GnuHashTable(cast(ubyte[]) elfFile[sectionHeader.sh_offset..sectionHeader.sh_offset + sectionHeader.sh_size]); 121 | break; 122 | case SHT_HASH: 123 | if (!hashTable) { 124 | hashTable = new ElfHashTable(cast(ubyte[]) elfFile[sectionHeader.sh_offset..sectionHeader.sh_offset + sectionHeader.sh_size]); 125 | } 126 | break; 127 | case SHT_REL: 128 | this.relocate!(ElfW!"Rel")(sectionHeader); 129 | relocationSections ~= sectionHeader; 130 | break; 131 | case SHT_RELA: 132 | this.relocate!(ElfW!"Rela")(sectionHeader); 133 | relocationSections ~= sectionHeader; 134 | break; 135 | default: 136 | break; 137 | } 138 | } 139 | } 140 | 141 | ~this() { 142 | foreach (library; loadedLibraries) { 143 | destroy(library); 144 | } 145 | 146 | if (allocation) { 147 | munmap(allocation.ptr, allocation.length); 148 | } 149 | } 150 | 151 | public void relocate() { 152 | foreach (relocationSection; relocationSections) { 153 | switch (relocationSection.sh_type) { 154 | case SHT_REL: 155 | this.relocate!(ElfW!"Rel")(relocationSection); 156 | break; 157 | case SHT_RELA: 158 | this.relocate!(ElfW!"Rela")(relocationSection); 159 | break; 160 | default: 161 | break; 162 | } 163 | } 164 | } 165 | 166 | private void relocate(RelocationType)(ref ElfW!"Shdr" shdr) { 167 | auto relocations = this.elfFile.identifyArray!(RelocationType)(shdr.sh_offset, shdr.sh_size / RelocationType.sizeof); 168 | auto allocation = cast(ubyte[]) allocation; 169 | 170 | foreach (relocation; relocations) { 171 | auto relocationType = ELFW!"R_TYPE"(relocation.r_info); 172 | auto symbolIndex = ELFW!"R_SYM"(relocation.r_info); 173 | 174 | auto offset = relocation.r_offset; 175 | size_t addend; 176 | static if (__traits(hasMember, relocation, "r_addend")) { 177 | addend = relocation.r_addend; 178 | } else { 179 | if (relocationType == R_386_JUMP_SLOT) { 180 | addend = 0; 181 | } else { 182 | addend = *cast(size_t*) (cast(size_t) allocation.ptr + offset); 183 | } 184 | } 185 | auto symbol = getSymbolImplementation(getSymbolName(dynamicSymbolTable[symbolIndex])); 186 | 187 | auto location = cast(size_t*) (cast(size_t) allocation.ptr + offset); 188 | 189 | switch (relocationType) { 190 | case R_GENERIC!"RELATIVE": 191 | *location = cast(size_t) allocation.ptr + addend; 192 | break; 193 | case R_GENERIC!"GLOB_DAT": 194 | *location = cast(size_t) (symbol + addend); 195 | break; 196 | case R_GENERIC!"JUMP_SLOT": 197 | *location = cast(size_t) (symbol); 198 | break; 199 | case R_GENERIC_NATIVE_ABS: 200 | *location = cast(size_t) (symbol + addend); 201 | break; 202 | default: 203 | throw new LoaderException("Unknown relocation type: " ~ to!string(relocationType)); 204 | } 205 | } 206 | } 207 | 208 | private string getSymbolName(ElfW!"Sym" symbol) { 209 | return cast(string) fromStringz(&dynamicStringTable[symbol.st_name]); 210 | } 211 | 212 | private string getSectionName(ElfW!"Shdr" section) { 213 | return cast(string) fromStringz(§ionNamesTable[section.sh_name]); 214 | } 215 | 216 | void* getSymbolImplementation(string symbolName) { 217 | void** hook = symbolName in hooks; 218 | if (hook) { 219 | return *hook; 220 | } 221 | 222 | import provision.symbols; 223 | return lookupSymbol(symbolName); 224 | } 225 | 226 | void* load(string symbolName) { 227 | ElfW!"Sym" sym; 228 | if (hashTable) { 229 | sym = hashTable.lookup(symbolName, this); 230 | } else { 231 | foreach (symbol; dynamicSymbolTable) { 232 | if (getSymbolName(symbol) == symbolName) { 233 | sym = symbol; 234 | break; 235 | } 236 | } 237 | } 238 | return cast(void*) (cast(size_t) allocation.ptr + sym.st_value); 239 | } 240 | } 241 | 242 | private struct MemoryBlock { 243 | size_t start; 244 | size_t end; 245 | } 246 | 247 | private __gshared AndroidLibrary[MemoryBlock] memoryTable; 248 | AndroidLibrary memoryOwner(size_t address) { 249 | foreach(memoryBlock; memoryTable.keys()) { 250 | if (address > memoryBlock.start && address < memoryBlock.end) { 251 | return memoryTable[memoryBlock]; 252 | } 253 | } 254 | 255 | return null; 256 | } 257 | 258 | import core.sys.linux.execinfo; 259 | pragma(inline, true) AndroidLibrary rootLibrary() { 260 | enum MAXFRAMES = 4; 261 | void*[MAXFRAMES] callstack; 262 | auto numframes = backtrace(callstack.ptr, MAXFRAMES); 263 | return memoryOwner(cast(size_t) callstack[numframes - 1]); 264 | } 265 | 266 | interface SymbolHashTable { 267 | ElfW!"Sym" lookup(string symbolName, AndroidLibrary library); 268 | } 269 | 270 | package class ElfHashTable: SymbolHashTable { 271 | struct ElfHashTableStruct { 272 | uint nbucket; 273 | uint nchain; 274 | } 275 | 276 | ElfHashTableStruct table; 277 | 278 | uint[] buckets; 279 | uint[] chain; 280 | 281 | this(ubyte[] tableData) { 282 | table = *cast(ElfHashTableStruct*) tableData.ptr; 283 | auto chainLocation = ElfHashTableStruct.sizeof + table.nbucket * uint.sizeof; 284 | 285 | buckets = cast(uint[]) tableData[ElfHashTableStruct.sizeof..chainLocation]; 286 | chain = cast(uint[]) tableData[chainLocation..$]; 287 | } 288 | 289 | static uint hash(string name) { 290 | uint h = 0, g; 291 | 292 | foreach (c; name) { 293 | h = 16 * h + c; 294 | h ^= h >> 24 & 0xf0; 295 | } 296 | 297 | return h & 0xfffffff; 298 | } 299 | 300 | ElfW!"Sym" lookup(string symbolName, AndroidLibrary library) { 301 | auto targetHash = hash(symbolName); 302 | 303 | scope ElfW!"Sym" symbol; 304 | for (uint i = buckets[targetHash % table.nbucket]; i; i = chain[i]) { 305 | symbol = library.dynamicSymbolTable[i]; 306 | if (symbolName == library.getSymbolName(symbol)) { 307 | return symbol; 308 | } 309 | } 310 | 311 | throw new LoaderException("Symbol not found: " ~ symbolName); 312 | } 313 | } 314 | 315 | package class GnuHashTable: SymbolHashTable { 316 | struct GnuHashTableStruct { 317 | uint nbuckets; 318 | uint symoffset; 319 | uint bloomSize; 320 | uint bloomShift; 321 | } 322 | 323 | GnuHashTableStruct table; 324 | size_t[] bloom; 325 | uint[] buckets; 326 | uint[] chain; 327 | 328 | this(ubyte[] tableData) { 329 | table = *cast(GnuHashTableStruct*) tableData.ptr; 330 | auto bucketsLocation = GnuHashTableStruct.sizeof + table.bloomSize * (size_t.sizeof / ubyte.sizeof); 331 | auto chainLocation = bucketsLocation + table.nbuckets * (uint.sizeof / ubyte.sizeof); 332 | 333 | bloom = cast(size_t[]) tableData[GnuHashTableStruct.sizeof..bucketsLocation]; 334 | buckets = cast(uint[]) tableData[bucketsLocation..chainLocation]; 335 | chain = cast(uint[]) tableData[chainLocation..$]; 336 | } 337 | 338 | static uint hash(string name) { 339 | uint h = 5381; 340 | 341 | foreach (c; name) { 342 | h = (h << 5) + h + c; 343 | } 344 | 345 | return h; 346 | } 347 | 348 | ElfW!"Sym" lookup(string symbolName, AndroidLibrary library) { 349 | auto targetHash = hash(symbolName); 350 | auto bucket = buckets[targetHash % table.nbuckets]; 351 | 352 | if (bucket < table.symoffset) { 353 | throw new LoaderException("Symbol not found: " ~ symbolName); 354 | } 355 | 356 | auto chain_index = bucket - table.symoffset; 357 | targetHash &= ~1; 358 | auto chains = chain[chain_index..$]; 359 | auto dynsyms = library.dynamicSymbolTable[bucket..$]; 360 | foreach (hash, symbol; zip(chains, dynsyms)) { 361 | if ((hash &~ 1) == targetHash && symbolName == library.getSymbolName(symbol)) { 362 | return symbol; 363 | } 364 | 365 | if (hash & 1) { 366 | break; 367 | } 368 | } 369 | 370 | throw new LoaderException("Symbol not found: " ~ symbolName); 371 | } 372 | } 373 | 374 | private size_t pageMask; 375 | 376 | shared static this() 377 | { 378 | pageMask = ~(pageSize - 1); 379 | } 380 | 381 | int memoryProtection(ref ElfW!"Phdr" phdr) 382 | { 383 | int prot = 0; 384 | if (phdr.p_flags & PF_R) { 385 | prot |= PROT_READ; 386 | } 387 | if (phdr.p_flags & PF_W) { 388 | prot |= PROT_WRITE; 389 | } 390 | if (phdr.p_flags & PF_X) { 391 | prot |= PROT_EXEC; 392 | } 393 | 394 | return prot; 395 | } 396 | 397 | template ELFW(string func) { 398 | alias ELFW = mixin("ELF" ~ to!string(size_t.sizeof * 8) ~ "_" ~ func); 399 | } 400 | 401 | version (X86_64) { 402 | private enum string relocationArch = "X86_64"; 403 | private enum R_GENERIC_NATIVE_ABS = R_X86_64_64; 404 | } else version (X86) { 405 | private enum string relocationArch = "386"; 406 | private enum R_GENERIC_NATIVE_ABS = R_386_32; 407 | } else version (AArch64) { 408 | private enum string relocationArch = "AARCH64"; 409 | private enum R_GENERIC_NATIVE_ABS = R_AARCH64_ABS64; 410 | } else version (ARM) { 411 | private enum string relocationArch = "ARM"; 412 | private enum R_GENERIC_NATIVE_ABS = R_ARM_ABS32; 413 | } 414 | 415 | alias R_386_JUMP_SLOT = R_386_JMP_SLOT; 416 | 417 | template R_GENERIC(string relocationType) { 418 | enum R_GENERIC = mixin("R_" ~ relocationArch ~ "_" ~ relocationType); 419 | } 420 | 421 | size_t pageFloor(size_t number) { 422 | return number & pageMask; 423 | } 424 | 425 | size_t pageCeil(size_t number) { 426 | return (number + pageSize - 1) & pageMask; 427 | } 428 | 429 | RetType[] identifyArray(RetType, FromType)(FromType obj, size_t offset, size_t length) { 430 | return (cast(RetType[]) obj[offset..offset + (RetType.sizeof * length)]).ptr[0..length]; 431 | } 432 | 433 | RetType identify(RetType, FromType)(FromType obj, size_t offset) { 434 | return obj[offset..offset + RetType.sizeof].reinterpret!(RetType); 435 | } 436 | 437 | RetType reinterpret(RetType, FromType)(FromType[] obj) { 438 | return (cast(RetType[]) obj)[0]; 439 | } 440 | 441 | class LoaderException: Exception { 442 | this(string message, string file = __FILE__, size_t line = __LINE__) { 443 | super("Cannot load library: " ~ message, file, line); 444 | } 445 | } 446 | 447 | class UndefinedSymbolException: Exception { 448 | this(string file = __FILE__, size_t line = __LINE__) { 449 | super("An undefined symbol has been called!", file, line); 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /lib/provision/adi.d: -------------------------------------------------------------------------------- 1 | module provision.adi; 2 | 3 | import provision.androidlibrary; 4 | import std.base64; 5 | import std.conv; 6 | import std.digest.sha; 7 | import file = std.file; 8 | import std.format; 9 | import std.json; 10 | import std.net.curl; 11 | import std.path; 12 | import std.string; 13 | 14 | import slf4d; 15 | 16 | version (LibPlist) { 17 | import plist; 18 | } else { 19 | import plist; 20 | import plist.types; 21 | } 22 | 23 | alias ADILoadLibraryWithPath_t = extern(C) int function(const char*); 24 | alias ADISetAndroidID_t = extern(C) int function(const char*, uint); 25 | alias ADISetProvisioningPath_t = extern(C) int function(const char*); 26 | alias ADIProvisioningErase_t = extern(C) int function(ulong); 27 | alias ADISynchronize_t = extern(C) int function(ulong, ubyte*, uint, ubyte**, uint*, ubyte**, uint*); 28 | alias ADIProvisioningDestroy_t = extern(C) int function(uint); 29 | alias ADIProvisioningEnd_t = extern(C) int function(uint, ubyte*, uint, ubyte*, uint); 30 | alias ADIProvisioningStart_t = extern(C) int function(ulong, ubyte*, uint, ubyte**, uint*, uint*); 31 | alias ADIGetLoginCode_t = extern(C) int function(ulong); 32 | alias ADIDispose_t = extern(C) int function(void*); 33 | alias ADIOTPRequest_t = extern(C) int function(ulong, ubyte**, uint*, ubyte**, uint*); 34 | 35 | public class ADI { 36 | private ADILoadLibraryWithPath_t pADILoadLibraryWithPath; 37 | private ADISetAndroidID_t pADISetAndroidID; 38 | private ADISetProvisioningPath_t pADISetProvisioningPath; 39 | 40 | private ADIProvisioningErase_t pADIProvisioningErase; 41 | private ADISynchronize_t pADISynchronize; 42 | private ADIProvisioningDestroy_t pADIProvisioningDestroy; 43 | private ADIProvisioningEnd_t pADIProvisioningEnd; 44 | private ADIProvisioningStart_t pADIProvisioningStart; 45 | private ADIGetLoginCode_t pADIGetLoginCode; 46 | private ADIDispose_t pADIDispose; 47 | private ADIOTPRequest_t pADIOTPRequest; 48 | 49 | private AndroidLibrary storeServicesCore; 50 | private Logger logger; 51 | 52 | private string __provisioningPath; 53 | public string provisioningPath() { 54 | return __provisioningPath; 55 | } 56 | 57 | public void provisioningPath(string path) { 58 | __provisioningPath = path; 59 | pADISetProvisioningPath(path.toStringz).unwrapADIError(); 60 | } 61 | 62 | private string __identifier; 63 | public string identifier() { 64 | return __identifier; 65 | } 66 | 67 | public void identifier(string identifier) { 68 | __identifier = identifier; 69 | pADISetAndroidID(identifier.ptr, cast(uint) identifier.length).unwrapADIError(); 70 | } 71 | 72 | public this(string libraryPath) { 73 | string storeServicesCorePath = libraryPath.buildPath("libstoreservicescore.so"); 74 | if (!file.exists(storeServicesCorePath)) { 75 | throw new file.FileException(storeServicesCorePath); 76 | } 77 | 78 | this(libraryPath, new AndroidLibrary(storeServicesCorePath)); 79 | } 80 | 81 | public this(string libraryPath, AndroidLibrary storeServicesCore) { 82 | this.storeServicesCore = storeServicesCore; 83 | this.logger = getLogger(); 84 | 85 | // We are loading the symbols from the ELF library from their name. 86 | // Those has been obfuscated but they keep a consistent obfuscated name, like a hash function would. 87 | logger.debug_("Loading Android-specific symbols…"); 88 | 89 | pADILoadLibraryWithPath = cast(ADILoadLibraryWithPath_t) storeServicesCore.load("kq56gsgHG6"); 90 | pADISetAndroidID = cast(ADISetAndroidID_t) storeServicesCore.load("Sph98paBcz"); 91 | pADISetProvisioningPath = cast(ADISetProvisioningPath_t) storeServicesCore.load("nf92ngaK92"); 92 | 93 | logger.debug_("Loading ADI symbols…"); 94 | 95 | pADIProvisioningErase = cast(ADIProvisioningErase_t) storeServicesCore.load("p435tmhbla"); 96 | pADISynchronize = cast(ADISynchronize_t) storeServicesCore.load("tn46gtiuhw"); 97 | pADIProvisioningDestroy = cast(ADIProvisioningDestroy_t) storeServicesCore.load("fy34trz2st"); 98 | pADIProvisioningEnd = cast(ADIProvisioningEnd_t) storeServicesCore.load("uv5t6nhkui"); 99 | pADIProvisioningStart = cast(ADIProvisioningStart_t) storeServicesCore.load("rsegvyrt87"); 100 | pADIGetLoginCode = cast(ADIGetLoginCode_t) storeServicesCore.load("aslgmuibau"); 101 | pADIDispose = cast(ADIDispose_t) storeServicesCore.load("jk24uiwqrg"); 102 | pADIOTPRequest = cast(ADIOTPRequest_t) storeServicesCore.load("qi864985u0"); 103 | 104 | logger.debug_("Loading libraries…"); 105 | loadLibrary(libraryPath); 106 | 107 | logger.debug_("Initialization…"); 108 | 109 | // We are setting those to be sure to have the same value in the class (used in getter) and the real one in ADI. 110 | provisioningPath = "/"; 111 | identifier = "0000000000000000"; 112 | logger.debug_("Initialization complete !"); 113 | } 114 | 115 | ~this() { 116 | if (storeServicesCore) { 117 | destroy(storeServicesCore); 118 | } 119 | } 120 | 121 | public void loadLibrary(string libraryPath) { 122 | pADILoadLibraryWithPath(libraryPath.toStringz).unwrapADIError(); 123 | } 124 | 125 | public void eraseProvisioning(ulong dsId) { 126 | pADIProvisioningErase(dsId).unwrapADIError(); 127 | } 128 | 129 | struct SynchronizationResumeMetadata { 130 | public ubyte[] synchronizationResumeMetadata; 131 | public ubyte[] machineIdentifier; 132 | private ADI adi; 133 | 134 | @disable this(); 135 | @disable this(this); 136 | 137 | this(ADI adiInstance, ubyte* srm, uint srmLength, ubyte* mid, uint midLength) { 138 | adi = adiInstance; 139 | synchronizationResumeMetadata = srm[0..srmLength]; 140 | machineIdentifier = mid[0..midLength]; 141 | } 142 | 143 | ~this() { 144 | adi.dispose(synchronizationResumeMetadata.ptr); 145 | adi.dispose(machineIdentifier.ptr); 146 | } 147 | } 148 | 149 | public SynchronizationResumeMetadata synchronize(ulong dsId, ubyte[] serverIntermediateMetadata) { 150 | ubyte* srm; 151 | uint srmLength; 152 | ubyte* mid; 153 | uint midLength; 154 | 155 | pADISynchronize( 156 | dsId, 157 | serverIntermediateMetadata.ptr, 158 | cast(uint) serverIntermediateMetadata.length, 159 | &mid, 160 | &midLength, 161 | &srm, 162 | &srmLength 163 | ).unwrapADIError(); 164 | 165 | return SynchronizationResumeMetadata(this, srm, srmLength, mid, midLength); 166 | } 167 | 168 | public void destroyProvisioning(uint session) { 169 | pADIProvisioningDestroy(session).unwrapADIError(); 170 | } 171 | 172 | public void endProvisioning(uint session, ubyte[] persistentTokenMetadata, ubyte[] trustKey) { 173 | pADIProvisioningEnd( 174 | session, 175 | persistentTokenMetadata.ptr, 176 | cast(uint) persistentTokenMetadata.length, 177 | trustKey.ptr, 178 | cast(uint) trustKey.length 179 | ).unwrapADIError(); 180 | } 181 | 182 | struct ClientProvisioningIntermediateMetadata { 183 | public ubyte[] clientProvisioningIntermediateMetadata; 184 | public uint session; 185 | private ADI adi; 186 | 187 | @disable this(); 188 | @disable this(this); 189 | 190 | this(ADI adiInstance, ubyte* cpim, uint cpimLength, uint session) { 191 | adi = adiInstance; 192 | clientProvisioningIntermediateMetadata = cpim[0..cpimLength]; 193 | this.session = session; 194 | } 195 | 196 | ~this() { 197 | adi.dispose(clientProvisioningIntermediateMetadata.ptr); 198 | } 199 | } 200 | 201 | public ClientProvisioningIntermediateMetadata startProvisioning(ulong dsId, ubyte[] serverProvisioningIntermediateMetadata) { 202 | ubyte* cpim; 203 | uint cpimLength; 204 | uint session; 205 | 206 | pADIProvisioningStart( 207 | dsId, 208 | serverProvisioningIntermediateMetadata.ptr, 209 | cast(uint) serverProvisioningIntermediateMetadata.length, 210 | &cpim, 211 | &cpimLength, 212 | &session 213 | ).unwrapADIError(); 214 | 215 | return ClientProvisioningIntermediateMetadata(this, cpim, cpimLength, session); 216 | } 217 | 218 | public bool isMachineProvisioned(ulong dsId) { 219 | int errorCode = pADIGetLoginCode(dsId); 220 | 221 | if (errorCode == 0) { 222 | return true; 223 | } else if (errorCode == -45061) { 224 | return false; 225 | } 226 | 227 | throw new ADIException(errorCode); 228 | } 229 | 230 | public void dispose(void* ptr) { 231 | pADIDispose(ptr).unwrapADIError(); 232 | } 233 | 234 | struct OneTimePassword { 235 | public ubyte[] oneTimePassword; 236 | public ubyte[] machineIdentifier; 237 | private ADI adi; 238 | 239 | @disable this(); 240 | @disable this(this); 241 | 242 | this(ADI adiInstance, ubyte* otp, uint otpLength, ubyte* mid, uint midLength) { 243 | adi = adiInstance; 244 | oneTimePassword = otp[0..otpLength]; 245 | machineIdentifier = mid[0..midLength]; 246 | } 247 | 248 | ~this() { 249 | adi.dispose(oneTimePassword.ptr); 250 | adi.dispose(machineIdentifier.ptr); 251 | } 252 | } 253 | 254 | public OneTimePassword requestOTP(ulong dsId) { 255 | ubyte* otp; 256 | uint otpLength; 257 | ubyte* mid; 258 | uint midLength; 259 | 260 | pADIOTPRequest( 261 | dsId, 262 | &mid, 263 | &midLength, 264 | &otp, 265 | &otpLength 266 | ).unwrapADIError(); 267 | 268 | return OneTimePassword(this, otp, otpLength, mid, midLength); 269 | } 270 | } 271 | 272 | public class Device { 273 | JSONValue deviceData; 274 | 275 | enum uniqueDeviceIdentifierJson = "UUID"; 276 | enum serverFriendlyDescriptionJson = "clientInfo"; 277 | enum adiIdentifierJson = "identifier"; 278 | enum localUserUUIDJson = "localUUID"; 279 | 280 | string uniqueDeviceIdentifier() { return deviceData[uniqueDeviceIdentifierJson].str(); } 281 | string serverFriendlyDescription() { return deviceData[serverFriendlyDescriptionJson].str(); } 282 | string adiIdentifier() { return deviceData[adiIdentifierJson].str(); } 283 | // It is a value computed by AuthKit. On Windows, it takes the path to the home folder and hash every component in. 284 | // We could do the same thing but anyway we can also just hash the ADI identifier, that's good too. 285 | string localUserUUID() { return deviceData[localUserUUIDJson].str(); } 286 | 287 | void uniqueDeviceIdentifier(string value) { deviceData[uniqueDeviceIdentifierJson] = value; write(); } 288 | void serverFriendlyDescription(string value) { deviceData[serverFriendlyDescriptionJson] = value; write(); } 289 | void adiIdentifier(string value) { deviceData[adiIdentifierJson] = value; write(); } 290 | void localUserUUID(string value) { deviceData[localUserUUIDJson] = value; write(); } 291 | 292 | // We could generate that, but we servers don't care so anyway 293 | // string logicBoardSerialNumber; 294 | // string romAddress; 295 | // string machineSerialNumber; 296 | 297 | bool initialized = false; 298 | string path; 299 | 300 | this(string filePath) { 301 | path = filePath; 302 | if (file.exists(path)) { 303 | try { 304 | JSONValue deviceFile = parseJSON(cast(char[]) file.read(filePath)); 305 | uniqueDeviceIdentifier = deviceFile[uniqueDeviceIdentifierJson].str(); 306 | serverFriendlyDescription = deviceFile[serverFriendlyDescriptionJson].str(); 307 | adiIdentifier = deviceFile[adiIdentifierJson].str(); 308 | localUserUUID = deviceFile[localUserUUIDJson].str(); 309 | initialized = true; 310 | } catch (Throwable) { /+ do nothing +/ } 311 | } 312 | } 313 | 314 | public void write(string path) { 315 | this.path = path; 316 | write(); 317 | } 318 | 319 | public void write() { 320 | if (path) { 321 | file.write(path, deviceData.toString()); 322 | initialized = true; 323 | } 324 | } 325 | } 326 | 327 | public class ProvisioningSession { 328 | private HTTP httpClient; 329 | private string[string] urlBag; 330 | 331 | private ADI adi; 332 | private Device device; 333 | 334 | public this(ADI adi, Device device) { 335 | this.adi = adi; 336 | this.device = device; 337 | 338 | httpClient = HTTP(); 339 | 340 | httpClient.setUserAgent("akd/1.0 CFNetwork/1404.0.5 Darwin/22.3.0"); 341 | httpClient.handle.set(CurlOption.ssl_verifypeer, 0); 342 | 343 | // they are somehow not using the plist content-type in AuthKit 344 | httpClient.addRequestHeader("Content-Type", "application/x-www-form-urlencoded"); 345 | httpClient.addRequestHeader("Connection", "keep-alive"); 346 | 347 | httpClient.addRequestHeader("X-Mme-Device-Id", device.uniqueDeviceIdentifier); 348 | // on macOS, MMe for the Client-Info header is written with 2 caps, while on Windows it is Mme... 349 | // and HTTP headers are supposed to be case-insensitive in the HTTP spec... 350 | httpClient.addRequestHeader("X-MMe-Client-Info", device.serverFriendlyDescription); 351 | httpClient.addRequestHeader("X-Apple-I-MD-LU", device.localUserUUID); 352 | 353 | // httpClient.addRequestHeader("X-Apple-I-MLB", device.logicBoardSerialNumber); // 17 letters, uppercase in Apple's base 34 354 | // httpClient.addRequestHeader("X-Apple-I-ROM", device.romAddress); // 6 bytes, lowercase hexadecimal 355 | // httpClient.addRequestHeader("X-Apple-I-SRL-NO", device.machineSerialNumber); // 12 letters, uppercase 356 | 357 | // different apps can be used, I already saw fmfd and Setup here 358 | // and Reprovision uses Xcode in some requests, so maybe it is possible here too. 359 | httpClient.addRequestHeader("X-Apple-Client-App-Name", "Setup"); 360 | } 361 | 362 | public void loadURLBag() { 363 | string content = cast(string) std.net.curl.get("https://gsa.apple.com/grandslam/GsService2/lookup", httpClient); 364 | 365 | version (LibPlist) { 366 | PlistDict plist = cast(PlistDict) Plist.fromXml(content); 367 | auto response = cast(PlistDict) plist["urls"]; 368 | auto responseIter = response.iter(); 369 | 370 | Plist val; 371 | string key; 372 | 373 | while (responseIter.next(val, key)) { 374 | urlBag[key] = cast(string) cast(PlistString) val; 375 | } 376 | } else { 377 | Plist plist = new Plist(); 378 | plist.read(cast(string) content); 379 | auto response = (cast(PlistElementDict) (cast(PlistElementDict) (plist[0]))["urls"]); 380 | 381 | foreach (key; response.keys()) { 382 | urlBag[key] = (cast(PlistElementString) response[key]).value; 383 | } 384 | } 385 | } 386 | 387 | public void provision(ulong dsId) { 388 | if (urlBag.length == 0) { 389 | loadURLBag(); 390 | } 391 | 392 | import std.datetime.systime; 393 | 394 | httpClient.addRequestHeader("X-Apple-I-Client-Time", Clock.currTime().toISOExtString()); 395 | string startProvisioningPlist = cast(string) post(urlBag["midStartProvisioning"], 396 | " 397 | 398 | 399 | 400 | \tHeader 401 | \t 402 | \tRequest 403 | \t 404 | 405 | ", httpClient); 406 | 407 | scope string spimStr; 408 | { 409 | version (LibPlist) { 410 | scope auto spimPlist = cast(PlistDict) Plist.fromXml(startProvisioningPlist); 411 | scope auto spimResponse = cast(PlistDict) spimPlist["Response"]; 412 | spimStr = cast(string) cast(PlistString) spimResponse["spim"]; 413 | } else { 414 | Plist spimPlist = new Plist(); 415 | spimPlist.read(startProvisioningPlist); 416 | PlistElementDict spimResponse = cast(PlistElementDict) (cast(PlistElementDict) (spimPlist[0]))["Response"]; 417 | spimStr = (cast(PlistElementString) spimResponse["spim"]).value; 418 | } 419 | } 420 | 421 | scope ubyte[] spim = Base64.decode(spimStr); 422 | 423 | scope auto cpim = adi.startProvisioning(dsId, spim); 424 | scope (failure) try { adi.destroyProvisioning(cpim.session); } catch(Throwable) {} 425 | 426 | httpClient.addRequestHeader("X-Apple-I-Client-Time", Clock.currTime().toISOExtString()); 427 | string endProvisioningPlist = cast(string) post(urlBag["midFinishProvisioning"], format!" 428 | 429 | 430 | 431 | \tHeader 432 | \t 433 | \tRequest 434 | \t 435 | \t\tcpim 436 | \t\t%s 437 | \t 438 | 439 | "(Base64.encode(cpim.clientProvisioningIntermediateMetadata)), httpClient); 440 | 441 | scope ulong routingInformation; 442 | scope ubyte[] persistentTokenMetadata; 443 | scope ubyte[] trustKey; 444 | 445 | { 446 | version (LibPlist) { 447 | scope PlistDict plist = cast(PlistDict) Plist.fromXml(endProvisioningPlist); 448 | scope PlistDict spimResponse = cast(PlistDict) plist["Response"]; 449 | routingInformation = to!ulong(cast(string) cast(PlistString) spimResponse["X-Apple-I-MD-RINFO"]); 450 | persistentTokenMetadata = Base64.decode(cast(string) cast(PlistString) spimResponse["ptm"]); 451 | trustKey = Base64.decode(cast(string) cast(PlistString) spimResponse["tk"]); 452 | } else { 453 | scope Plist plist = new Plist(); 454 | plist.read(endProvisioningPlist); 455 | scope PlistElementDict spimResponse = cast(PlistElementDict) (cast(PlistElementDict) (plist[0]))["Response"]; 456 | 457 | routingInformation = to!ulong((cast(PlistElementString) spimResponse["X-Apple-I-MD-RINFO"]).value); 458 | persistentTokenMetadata = Base64.decode((cast(PlistElementString) spimResponse["ptm"]).value); 459 | trustKey = Base64.decode((cast(PlistElementString) spimResponse["tk"]).value); 460 | } 461 | } 462 | 463 | adi.endProvisioning(cpim.session, persistentTokenMetadata, trustKey); 464 | } 465 | } 466 | 467 | void unwrapADIError(int error, string file = __FILE__, size_t line = __LINE__) { 468 | if (error) { 469 | throw new ADIException(error, file, line); 470 | } 471 | } 472 | 473 | enum ADIError: int { 474 | invalidParams = -45001, 475 | invalidParams2 = -45002, 476 | invalidTrustKey = -45003, 477 | ptmTkNotMatchingState = -45006, 478 | invalidInputDataParamHeader = -45018, 479 | unknownAdiFunction = -45019, 480 | invalidInputDataParamBody = -45020, 481 | unknownSession = -45025, 482 | emptySession = -45026, 483 | invalidDataHeader = -45031, 484 | dataTooShort = -45032, 485 | invalidDataBody = -45033, 486 | unknownADICallFlags = -45034, 487 | timeError = -45036, 488 | emptyHardwareIds = -45046, 489 | filesystemError = -45054, 490 | notProvisioned = -45061, 491 | noProvisioningToErase = -45062, 492 | pendingSession = -45063, 493 | sessionAlreadyDone = -45066, 494 | libraryLoadingFailed = -45075, 495 | } 496 | 497 | string toString(ADIError error) { 498 | string formatString; 499 | switch (cast(int) error) { 500 | case -45001: 501 | formatString = "invalid parameters (%d), or missing initialization bits, you need to set an identifier and a valid provisioning path first!"; 502 | break; 503 | case -45002: 504 | formatString = "invalid parameters (for decipher) (%d)"; 505 | break; 506 | case -45003: 507 | formatString = "invalid Trust Key (%d)"; 508 | break; 509 | case -45006: 510 | formatString = "ptm and tk are not matching the transmitted cpim (%d)"; 511 | break; 512 | // -45017: exists (observed: iOS), unknown meaning 513 | case -45018: 514 | formatString = "invalid input data header (first uint) (pointer is correct tho) (%d)"; 515 | break; 516 | case -45019: 517 | formatString = "vdfut768ig doesn't know the asked function (%d)"; 518 | break; 519 | case -45020: 520 | formatString = "invalid input data (not the first uint) (%d)"; 521 | break; 522 | case -45025: 523 | formatString = "unknown session (%d)"; 524 | break; 525 | case -45026: 526 | formatString = "empty session (%d)"; 527 | break; 528 | case -45031: 529 | formatString = "invalid data (header) (%d)"; 530 | break; 531 | case -45032: 532 | formatString = "data too short (%d)"; 533 | break; 534 | case -45033: 535 | formatString = "invalid data (body) (%d)"; 536 | break; 537 | case -45034: 538 | formatString = "unknown ADI call flags (%d)"; 539 | break; 540 | case -45036: 541 | formatString = "time error (%d)"; 542 | break; 543 | // -45044: exists (observed: macOS iTunes, from Google), unknown meaning 544 | // -45045: probably a typo of -45054 545 | case -45046: 546 | formatString = "identifier generation failure: empty hardware ids (%d)"; 547 | break; 548 | // -45048: exists (observed: windows iTunes, from Google), unknown meaning, resolved by adi file suppression 549 | case -45054: 550 | formatString = "generic libc/file manipulation error (%d)"; 551 | break; 552 | case -45061: 553 | formatString = "not provisioned (%d)"; 554 | break; 555 | case -45062: 556 | formatString = "cannot erase provisioning: not provisioned (%d)"; 557 | break; 558 | case -45063: 559 | formatString = "provisioning first step is already pending (%d)"; 560 | break; 561 | case -45066: 562 | formatString = "2nd step fail: session already consumed (%d)"; 563 | break; 564 | case -45075: 565 | formatString = "library loading error (%d)"; 566 | break; 567 | // -45076: exists (observed: macOS iTunes, from Google), unknown meaning, seems related to backward compatibility between 12.6.x and 12.7 568 | default: 569 | formatString = "unknown ADI error (%d)"; 570 | break; 571 | } 572 | return format(formatString, error); 573 | } 574 | 575 | public class ADIException: Exception { 576 | private ADIError errorCode; 577 | 578 | this(int error, string file = __FILE__, size_t line = __LINE__) { 579 | this.errorCode = cast(ADIError) error; 580 | super(errorCode.toString(), file, line); 581 | } 582 | 583 | ADIError adiError() { 584 | return errorCode; 585 | } 586 | } 587 | --------------------------------------------------------------------------------