{"id":50385018,"url":"https://github.com/glomatico/wrapper-v2","last_synced_at":"2026-05-30T14:30:43.643Z","repository":{"id":359893789,"uuid":"1241663478","full_name":"glomatico/wrapper-v2","owner":"glomatico","description":"Wrapper V2","archived":false,"fork":false,"pushed_at":"2026-05-24T00:14:09.000Z","size":38010,"stargazers_count":13,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-05-24T02:19:56.409Z","etag":null,"topics":["android","apple","apple-music","fairplay-streaming"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/glomatico.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-17T17:03:56.000Z","updated_at":"2026-05-24T00:14:13.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/glomatico/wrapper-v2","commit_stats":null,"previous_names":["glomatico/wrapper-v2"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/glomatico/wrapper-v2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glomatico%2Fwrapper-v2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glomatico%2Fwrapper-v2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glomatico%2Fwrapper-v2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glomatico%2Fwrapper-v2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/glomatico","download_url":"https://codeload.github.com/glomatico/wrapper-v2/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/glomatico%2Fwrapper-v2/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33696681,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","apple","apple-music","fairplay-streaming"],"created_at":"2026-05-30T14:30:43.571Z","updated_at":"2026-05-30T14:30:43.637Z","avatar_url":"https://github.com/glomatico.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# wrapper-v2\n\nA clean rewrite of the Apple Music FairPlay decryption wrapper, based on\n[`WorldObservationLog/wrapper`](https://github.com/WorldObservationLog/wrapper).\n\n## Development note\n\nThis project has been developed with heavy AI assistance. The code should be\ntreated as research-grade and reviewed carefully, especially around native ABI\ncalls, FairPlay state handling, and experimental endpoints. AI-generated changes\nare not assumed to be correct just because they compile.\n\n## What it is\n\nA small daemon that exposes a local HTTP API for FairPlay key fetching and\nsample decryption, and gives downstream tooling (e.g.\n[`gamdl`](https://github.com/glomatico/gamdl)) a uniform interface that does\nnot depend on platform or language.\n\nAt runtime the binary starts in **supervisor** mode by default. The supervisor\nowns the public HTTP port and starts a private `WRAPPER_MODE=worker` subprocess on\n`127.0.0.1:${WRAPPER_WORKER_PORT:-18080}`. Only the worker loads Apple Music's\nAndroid native libraries inside the Linux chroot. If FairPlay hangs or returns\na CKC/KD-style decrypt error, the supervisor can kill the worker, start a fresh\none, and retry the decrypt request without dropping the public HTTP server. If\nthe worker cannot be started three consecutive times, the supervisor exits so\nthe container supervisor can recreate the whole runtime.\n\nThe daemon ships _no_ Apple code. Apple Music native libraries must be supplied\nby the person building the image and staged into `rootfs/system/lib64/`; the\nexpected `.so` SHA-256 digests are pinned in `LIBS_VERSION.json`.\n\n## HTTP API\n\nMost endpoints accept and return `application/json`. `POST /decrypt`\nuses `application/octet-stream` for successful request and response bodies;\nerrors still return JSON.\n\n| Method   | Path         | Description                                                                                                                                                                                                                                                                                                                                                                        |\n| -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `GET`    | `/health`    | Liveness probe. `{status, version, runtime}` — `runtime.playback_ready` is true when FairPlay decrypt is available.                                                                                                                                                                                                                                                                |\n| `GET`    | `/me`        | `{version, runtime, auth}` — same runtime flags as `/health`.                                                                                                                                                                                                                                                                                                                      |\n| `POST`   | `/login`     | Body: `{\"username\": \"...\", \"password\": \"...\"}` or `{\"apple_id\": \"...\", \"password\": \"...\"}` (synonyms). Drives Apple's `AuthenticateFlow`. Returns `200` + token snapshot, `202` if **2FA** is required (then `POST /login/2fa`), or `401` on failure.                                                                                                                              |\n| `POST`   | `/login/2fa` | Body: `{\"code\": \"123456\"}`. Continues a login waiting for HSA2.                                                                                                                                                                                                                                                                                                                    |\n| `GET`    | `/playback`  | Query string `?adam_id=\u003cnumeric store id\u003e`. Returns `200` with a JSON object `{\"songList\":[...]}` containing the **whole MZ playback dispatch** Apple's `subDownload` URL bag returns (every flavor, key URI, asset URL, metadata field). CFData fields are base64; CFDate fields are ISO 8601. Needs an **authenticated** session; otherwise `401` / `503`. Apple errors → `502`. |\n| `POST`   | `/decrypt`   | Binary FairPlay sample decrypt batch. Request frame contains `adam_id`, SKD `uri`, and one or more encrypted samples. Response frame contains plaintext samples. Needs **authenticated** session and `playback_ready`; otherwise `401` / `503`. On FairPlay errors or worker timeouts, the supervisor restarts the worker and retries once before returning the final result.      |\n| `DELETE` | `/login`     | Aborts an in-flight login or clears cached tokens from memory. Apple's on-disk `mpl_db` cache is unchanged.                                                                                                                                                                                                                                                                        |\n\n### `POST /decrypt` Binary Format\n\nAll integer fields are unsigned 32-bit big-endian.\n\nRequest body:\n\n```text\nadam_id_len\nuri_len\nsample_count\nsample_len[0]\n...\nsample_len[sample_count - 1]\nadam_id bytes\nuri bytes\nsample[0] bytes\n...\nsample[sample_count - 1] bytes\n```\n\nResponse body:\n\n```text\nsample_count\nsample_len[0]\n...\nsample_len[sample_count - 1]\nsample[0] bytes\n...\nsample[sample_count - 1] bytes\n```\n\nThe endpoint accepts and returns `application/octet-stream` on success.\nValidation and Apple/native errors use the normal JSON error envelope.\n\nSign-in matches the legacy wrapper model: you send **email (Apple ID) and password**\nto the daemon; it fills credentials through the native presentation interface.\nWith a persistent `WRAPPER_BASE_DIR` volume, Apple keeps `mpl_db/kvs.sqlitedb` on\ndisk. On each process start the daemon tries **session restore** (default\n`WRAPPER_RESTORE_SESSION=1`): if that session is still valid, `GET /me` can show\n**authenticated** and fresh tokens **without** another `POST /login`. Use\n`POST /login` when the volume is new, restore fails, or you need to re-auth.\nOptional `WRAPPER_APPLE_ID` only sets the `apple_id` label in `/me` after restore.\n\n## Layout\n\n```\n.\n├── CMakeLists.txt            top-level build (host launcher + NDK sub-build)\n├── Dockerfile                multi-stage build\n├── compose.yaml              docker compose entrypoint\n├── LIBS_VERSION.json         per-.so SHA-256 digests\n├── src/\n│   ├── daemon/               C++ daemon (cross-compiled with the NDK)\n│   │   ├── CMakeLists.txt\n│   │   ├── main.cpp          process entry: env parsing, lifecycle\n│   │   ├── server.{hpp,cpp}  HTTP route mounting (cpp-httplib)\n│   │   └── apple/\n│   │       ├── abi.hpp       Apple-lib mangled symbol declarations\n│   │       ├── auth.{hpp,cpp}    Apple ID login + 2FA + token cache\n│   │       ├── loader.{hpp,cpp}  dlopen / dlsym\n│   │       ├── runtime.{hpp,cpp} FootHillConfig + RequestContext + credential UI\n│   │       └── tokens.{hpp,cpp}  dev token + music user token harvest\n│   └── launcher/\n│       └── wrapper.c         host-Linux chroot launcher\n├── rootfs/                   chroot tree assembled at build time\n│   └── system/\n│       ├── bin/              \u003c- main, linker64 (staged)\n│       └── lib64/            \u003c- Apple's .so + Android system .so (staged)\n├── tools/\n│   ├── extract-libs.sh       optional local helper to extract and verify Apple .so files\n│   └── stage-system.sh       copy committed Android binaries into rootfs/\n└── vendor/\n    └── android-system/       linker64 + bionic + AOSP libs, SHA-pinned\n        ├── x86_64/\n        │   ├── bin/linker64\n        │   └── lib64/*.so\n        └── arm64-v8a/\n            ├── bin/linker64\n            └── lib64/*.so\n```\n\n## Building\n\n### One-time setup\n\nYou need a working Docker installation. Apart from that, the entire build\nruns inside the image. There is no host toolchain prerequisite for the\ndefault workflow.\n\nFor the build to succeed, `rootfs/system/lib64/` must already contain the\nrequired Apple Music native libraries for your `TARGET_ARCH`. The recommended\nsource version is Apple Music for Android **3.6.0-beta**. This repository does\nnot provide the download for those files.\n\n### Local build\n\n#### 1. Extract Apple Music native libraries\n\nProvide a local Apple Music `.apk` or `.apkm` for the target architecture. The\ndefault output is `rootfs/system/lib64/`, and every extracted `.so` must match\nthe hashes in `LIBS_VERSION.json`.\n\n```bash\nbash tools/extract-libs.sh --bundle path/to/local/apple-music.apk --arch x86_64\n```\n\n`.apkm` bundles are also accepted:\n\n```bash\nbash tools/extract-libs.sh --bundle path/to/local/apple-music.apkm --arch x86_64\n```\n\n#### 2. Stage Android system binaries\n\nThis copies the committed Android linker and system libraries into `rootfs/`,\nverifying their SHA-256 hashes against `LIBS_VERSION.json`.\n\n```bash\nbash tools/stage-system.sh --arch x86_64\n```\n\n#### 3. Build and run\n\n```bash\ndocker compose up --build\n```\n\n#### 4. Smoke test\n\n```bash\ncurl http://127.0.0.1/health\ncurl http://127.0.0.1/me\n```\n\n#### 5. Sign in\n\nUse your real Apple ID. If the first request returns `202`, continue with the\n2FA request.\n\n```bash\ncurl -X POST http://127.0.0.1/login \\\n     -H 'content-type: application/json' \\\n     -d '{\"username\":\"you@example.com\",\"password\":\"your-app-specific-password\"}'\n```\n\n```bash\ncurl -X POST http://127.0.0.1/login/2fa \\\n     -H 'content-type: application/json' \\\n     -d '{\"code\":\"123456\"}'\n```\n\nCheck the current session or clear the in-memory login state:\n\n```bash\ncurl http://127.0.0.1/me\ncurl -X DELETE http://127.0.0.1/login\n```\n\nThe daemon binds port 80 inside the container and the compose file maps it\nto host port 80 by default. Override with `HTTP_PORT=8080 docker compose up`\non machines that already have something on `:80`.\n\n### arm64-v8a image (Apple Silicon / AArch64 Linux)\n\nStage **arm64-v8a** Android system binaries and Apple Music native libraries,\nthen build a **linux/arm64** image so `wrapper`, the NDK daemon, and the staged\n`linker64` / `.so` set share the same ABI.\n\nThe Docker **compile** stage is always **linux/amd64** (Google ships the Linux NDK as an\nx86_64-host ZIP only). The image then cross-compiles `wrapper` for AArch64 when\n`TARGET_ARCH=arm64-v8a`. Set **runtime** platform to arm64; `BUILD_PLATFORM` in Compose is\nignored but kept for compatibility.\n\nExtract and stage the arm64 files:\n\n```bash\nbash tools/extract-libs.sh --bundle path/to/local/apple-music.apk --arch arm64-v8a\nbash tools/stage-system.sh --arch arm64-v8a\n```\n\nOr use a local `.apkm` bundle:\n\n```bash\nbash tools/extract-libs.sh --bundle path/to/local/apple-music.apkm --arch arm64-v8a\nbash tools/stage-system.sh --arch arm64-v8a\n```\n\nBuild the arm64 image:\n\n```bash\nTARGET_ARCH=arm64-v8a RUNTIME_PLATFORM=linux/arm64 \\\n  docker compose up --build\n```\n\nOn an **x86_64** host, `docker compose` / `docker run` need **QEMU** (binfmt) to run a\n`linux/arm64` container. On an **arm64** host, run the image **natively** (no emulation).\n\n### Daemon configuration\n\nThe daemon reads `WRAPPER_*` environment variables (forwarded via\n`compose.yaml`). See `.env.example` for the full list. The most useful are:\n\n- `WRAPPER_HOST`, `WRAPPER_PORT` - public supervisor bind address inside the\n  chroot.\n- `WRAPPER_MODE` - process role. Default `supervisor`; the supervisor sets\n  `worker` automatically for its private subprocess.\n- `WRAPPER_WORKER_PORT` - private loopback port used by the supervisor to talk\n  to the Apple runtime worker. Default `18080`.\n- `WRAPPER_BASE_DIR` - filesystem dir Apple's libs use for the FairPlay\n  key cache and `mpl_db`. The default matches upstream wrapper.\n- `WRAPPER_RESTORE_SESSION` - set to `0` to skip startup token harvest from\n  an existing on-disk Apple session (default is restore on).\n- `WRAPPER_APPLE_ID` - optional display label for `apple_id` in `GET /me` after\n  session restore only (not sent to Apple).\n- `WRAPPER_DEVICE_INFO` - 9-tuple identifying the fake Apple Music\n  Android client. Same fingerprint upstream uses by default.\n- `WRAPPER_APPLE_INIT=0` - skip Apple lib initialization at startup.\n  Lets you bring up the HTTP server alone for `/health` smoke tests\n  even on builds where you have not staged the Apple libraries yet.\n- `WRAPPER_USERNAME` + `WRAPPER_PASSWORD` - if both are set and the runtime\n  initialized, the daemon runs password sign-in at startup when not already\n  authenticated (same semantics as `POST /login`; 2FA still needs\n  `POST /login/2fa`). Treat these as secrets.\n\n### CI build\n\nThe `.github/workflows/build.yml` workflow runs on **push** to `main`,\non **pull_request** (same-repo only for the full job), and **workflow_dispatch**.\nIt uses the same host steps as above plus a Docker build and `/health` smoke\ntest, with one repository secret:\n\n- `APK_URL` - private/local CI URL for a compatible Apple Music `.apk` or\n  `.apkm`. The artifact is downloaded inside CI only, extracted with\n  `tools/extract-libs.sh`, and is not committed.\n\n**Matrix:** both `x86_64` and `arm64-v8a` jobs use `ubuntu-latest`. The arm64 image is\n`linux/arm64` at runtime; QEMU is enabled before the smoke `docker run` so the job works\non amd64 GitHub runners. The compile stage stays **linux/amd64** for the official NDK ZIP.\n\nPull requests opened from forks skip the build job because they cannot read the\nsecret.\n\n## License\n\n[Unlicense](./LICENSE) - public domain dedication.\n\nThis project is not affiliated with Apple Inc. The Apple-authored libraries\nit loads at runtime are not redistributed by this repository.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglomatico%2Fwrapper-v2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fglomatico%2Fwrapper-v2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fglomatico%2Fwrapper-v2/lists"}