Initial commit: Null DRM Official
Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
262
docs/capture.md
Normal file
262
docs/capture.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Using the capture binary
|
||||
|
||||
`drm capture` runs an HTTPS MITM **on the phone**, drives the app, and records the
|
||||
DRM exchange. It is both the production capture path and the tool you use to
|
||||
discover everything a new module needs.
|
||||
|
||||
## Device setup (once)
|
||||
|
||||
```bash
|
||||
adb devices # phone listed and authorised
|
||||
./bin/drm proxy build # cross-compile the on-device MITM (linux/arm64)
|
||||
./bin/drm proxy install-ca --reinject
|
||||
```
|
||||
|
||||
The phone must be rooted (Magisk). `install-ca` pushes the MITM CA and bind-mounts
|
||||
it over the system trust store, including into already-running zygote namespaces —
|
||||
a user-store CA is not enough, modern apps ignore it.
|
||||
|
||||
Verify the mount:
|
||||
|
||||
```bash
|
||||
adb shell 'ls /system/etc/security/cacerts/ | grep 6c3578b4'
|
||||
adb shell 'su -c "ls /apex/com.android.conscrypt/cacerts/6c3578b4.0"'
|
||||
```
|
||||
|
||||
If an app still reports `tls: unknown certificate`, re-run the reinject and
|
||||
force-stop the app so it inherits the mount:
|
||||
|
||||
```bash
|
||||
./bin/drm proxy reinject-ca
|
||||
adb shell am force-stop <package>
|
||||
```
|
||||
|
||||
Keep the screen on for the whole session — UI automation cannot tap a dark screen:
|
||||
|
||||
```bash
|
||||
adb shell svc power stayon true
|
||||
adb shell settings put system screen_off_timeout 1800000
|
||||
```
|
||||
|
||||
Restore the timeout afterwards (default is usually 60000).
|
||||
|
||||
## Three modes
|
||||
|
||||
### Passive — no module needed
|
||||
|
||||
```bash
|
||||
./bin/drm capture --wait 300
|
||||
```
|
||||
|
||||
Starts the MITM and nothing else. You open any app and play something; it records
|
||||
whatever license, PSSH and manifest traffic appears and writes
|
||||
`outputs/capture/<stamp>/`. This is the first thing to run against an unknown app.
|
||||
|
||||
### Launch only
|
||||
|
||||
```bash
|
||||
./bin/drm capture --app myapp --wvd data/device.wvd
|
||||
```
|
||||
|
||||
Launches the app, then waits while you navigate by hand. Useful while you are still
|
||||
working out the UI sequence.
|
||||
|
||||
### Full automation
|
||||
|
||||
```bash
|
||||
./bin/drm capture --app myapp --channel main --auto-play --wvd data/device.wvd
|
||||
```
|
||||
|
||||
Runs the module's `Launch` and `AutoPlay`, then waits for the required fields.
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `--app` | _(empty)_ | module to drive; empty means passive |
|
||||
| `--channel` | — | channel id, required by `--auto-play` |
|
||||
| `--auto-play` | false | run the module's navigation |
|
||||
| `--wait` | 180 | seconds to wait for the capture to complete |
|
||||
| `--wvd` | — | Widevine device file; required to fetch keys |
|
||||
| `--python` | `.venv` | interpreter for `wvkey.py` |
|
||||
| `--key-mode` | from the module | force `modulardrm` or `raw` |
|
||||
| `--user-agent` | — | UA for the license request |
|
||||
| `--serial` | sole device | target a specific phone |
|
||||
| `--close` | true | force-stop the app when done |
|
||||
| `--<app>.<field>` | — | override any of that module's values |
|
||||
|
||||
## What a run looks like
|
||||
|
||||
```text
|
||||
[*] Stopping any old appproxy...
|
||||
[*] Pushing appproxy...
|
||||
[+] appproxy pid=7604; capture -> /data/local/tmp/appproxy_cap.json
|
||||
[*] Setting HTTP proxy -> 127.0.0.1:8080 (was "null")
|
||||
[*] Re-injecting system CA (Magisk)...
|
||||
[+] System CA present in conscrypt
|
||||
[*] Launching rte (air.RTE.OSMF.Minimal)…
|
||||
[*] Auto-play RTE 2…
|
||||
[*] tap_ui "Live"/"Live tab, 2 out of 5" @ 324,2253
|
||||
[*] tap chip @ 356,297
|
||||
[*] tap Play @ 540,720
|
||||
[+] air.RTE.OSMF.Minimal is PLAYING
|
||||
[*] Waiting for license + PSSH + manifest…
|
||||
[+] Capture:
|
||||
pid: RKRPCEib8z9V
|
||||
mpd: https://dai.google.com/linear/dash/pa/event/.../stream/...
|
||||
license: https://widevine.entitlement.eu.theplatform.com/wv/web/ModularDrm?...
|
||||
pssh: AAAASnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACoiIGRmMTYzMzgyMWRk…
|
||||
[+] key: 5490781a77572107ec251237e94e3b53:cee5fc45b70040ffea2b2576fb5abda5
|
||||
[+] session: outputs/rte/20261005-220252/session.json
|
||||
[*] Closing air.RTE.OSMF.Minimal…
|
||||
[*] HTTP proxy cleared
|
||||
```
|
||||
|
||||
### Output
|
||||
|
||||
```text
|
||||
outputs/<app>/<stamp>/
|
||||
session.json everything, machine-readable
|
||||
mpd.txt manifest URL
|
||||
key.txt KID:KEY (one per line for multi-key streams)
|
||||
pssh.txt Widevine init data
|
||||
auth.txt Authorization value, when the app uses one
|
||||
pid.txt provider id, when the app uses one
|
||||
outputs/<app>/latest/ a copy of the most recent run
|
||||
```
|
||||
|
||||
Live log while a capture runs: `.cache/appproxy-<serial>.log`.
|
||||
|
||||
---
|
||||
|
||||
## Discovering a new app
|
||||
|
||||
### 1. Record everything
|
||||
|
||||
```bash
|
||||
./bin/drm proxy discover --install-ca --reinject
|
||||
# play the app on the phone; Ctrl+C when done
|
||||
```
|
||||
|
||||
Writes `outputs/discover/<stamp>/`:
|
||||
|
||||
| File | Contents |
|
||||
|---|---|
|
||||
| `appproxy_traffic.jsonl` | every HTTP(S) request/response, one JSON object per line |
|
||||
| `appproxy_cap.json` | the structured fields the proxy recognised |
|
||||
| `appproxy.log` | tagged lines: `[MPD]`, `[LIC]`, `[PSSH]`, `[MAN]` |
|
||||
|
||||
### 2. Mine it
|
||||
|
||||
```bash
|
||||
# the android package id
|
||||
adb shell dumpsys window | grep mCurrentFocus
|
||||
|
||||
# license endpoint
|
||||
grep -iE 'license|licence|widevine|/lic/' outputs/discover/*/appproxy.log
|
||||
|
||||
# manifest candidates
|
||||
grep -oE 'https?://[^"]+\.(m3u8|mpd)' outputs/discover/*/appproxy_traffic.jsonl \
|
||||
| sort -u
|
||||
|
||||
# the hosts involved, by frequency
|
||||
grep -oE '"host":"[^"]+"' outputs/discover/*/appproxy_traffic.jsonl \
|
||||
| sort | uniq -c | sort -rn | head -20
|
||||
|
||||
# config blobs that often carry ids and tokens
|
||||
grep -oE 'https?://[^"]+(config|playback)[^"]*\.json' \
|
||||
outputs/discover/*/appproxy_traffic.jsonl | sort -u
|
||||
```
|
||||
|
||||
What to pull out, and where it goes in `module.yaml`:
|
||||
|
||||
| Found | Key |
|
||||
|---|---|
|
||||
| package id | `package` |
|
||||
| license endpoint | `license_url` |
|
||||
| CDN host serving the manifest | `score.hosts` |
|
||||
| analytics / EPG hosts polluting the results | `score.deny` |
|
||||
| account ids, policy keys, video ids | provider-specific keys |
|
||||
|
||||
### 3. Work out the UI
|
||||
|
||||
Dump the tree at each step and read off stable selectors:
|
||||
|
||||
```bash
|
||||
adb shell uiautomator dump /sdcard/ui.xml
|
||||
adb shell cat /sdcard/ui.xml > ui.xml
|
||||
```
|
||||
|
||||
Extract the useful attributes:
|
||||
|
||||
```bash
|
||||
grep -oE 'resource-id="[^"]*"' ui.xml | sort -u
|
||||
grep -oE 'content-desc="[^"]*"' ui.xml | sort -u
|
||||
```
|
||||
|
||||
Prefer `resource-id` over `content-desc` or `text` — ids survive translation and
|
||||
copy changes. Check a candidate marker really exists before you rely on it; a
|
||||
launch marker that never matches costs the full timeout on every capture and is
|
||||
easy to miss because the pipeline recovers anyway.
|
||||
|
||||
Test taps by hand before writing Go:
|
||||
|
||||
```bash
|
||||
adb shell input tap 540 732
|
||||
adb shell input swipe 540 1700 540 700 350
|
||||
```
|
||||
|
||||
### 4. Determine the key mode
|
||||
|
||||
| The app sends | Use |
|
||||
|---|---|
|
||||
| a JSON wrapper around the challenge, with an Authorization header | `modulardrm` |
|
||||
| a raw binary challenge, `Content-Type: application/octet-stream` | `raw` |
|
||||
|
||||
Check the license request body in `appproxy_traffic.jsonl`. Then set `KeyMode()`
|
||||
in the module — never sniff it from the URL at runtime.
|
||||
|
||||
### 5. Decide which fields are required
|
||||
|
||||
Look at what `appproxy_cap.json` actually contained after a successful play:
|
||||
|
||||
- DASH + JSON license → usually `auth`, `pid`, `pssh`, `mpd`
|
||||
- HLS + binary license → usually `license_url`, `mpd` (the PSSH comes from the
|
||||
playlist afterwards)
|
||||
|
||||
Pass exactly those to `a.Hints(...)`. Asking for a field the app never emits makes
|
||||
every capture time out.
|
||||
|
||||
### 6. Verify
|
||||
|
||||
Where an app has a public catalog, capture the same channel both ways and compare —
|
||||
independent paths agreeing on the PSSH and key set is strong evidence both are
|
||||
right:
|
||||
|
||||
```bash
|
||||
./bin/drm catalog --app myapp --channel main --keys --wvd data/device.wvd --json > a.json
|
||||
./bin/drm capture --app myapp --channel main --auto-play --wvd data/device.wvd
|
||||
diff <(jq -r '.keys[]' a.json | sort) <(sort outputs/myapp/latest/key.txt)
|
||||
```
|
||||
|
||||
Also sanity-check that the KID you captured matches whatever you recorded in
|
||||
`module.yaml` for that channel.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause |
|
||||
|---|---|
|
||||
| `tls: unknown certificate` in the app | CA not mounted into conscrypt; reinject, then force-stop the app |
|
||||
| `timed out waiting for capture ([auth pid pssh mpd])` | playback never started, or `Hints` asks for a field this app does not send |
|
||||
| `launch: ui node not found within Ns` | launch marker does not match — dump the tree and pick a resource id |
|
||||
| `launch: exit status 1` | monkey raced a force-stop; usually transient, retry |
|
||||
| `auto-play failed: ... not found` | UI changed, or the phone is on a different screen than expected |
|
||||
| manifest field holds an EPG or config URL | add that host to `score.deny` |
|
||||
| `CA inject skipped/failed` | transient after repeated runs; re-run `proxy reinject-ca` |
|
||||
| Screen asleep mid-run | `adb shell svc power stayon true` |
|
||||
|
||||
Clear the phone's proxy if a run is interrupted:
|
||||
|
||||
```bash
|
||||
./bin/drm proxy clear-proxy
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue