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.
326 lines
14 KiB
HTML
326 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>NULLDRM — capture · decrypt · restream, self-hosted</title>
|
|
<meta name="description" content="NULLDRM captures Widevine live sessions from Android apps and restreams them. One Go binary, modules for each app, free forever." />
|
|
<meta name="color-scheme" content="light" />
|
|
<link rel="stylesheet" href="styles.css" />
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%234c6ef5'/%3E%3Ctext x='16' y='22' text-anchor='middle' font-size='16' font-weight='bold' font-family='system-ui' fill='%23fff'%3EN%3C/text%3E%3C/svg%3E" />
|
|
</head>
|
|
<body>
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
|
|
<header class="site-header">
|
|
<div class="shell header-row">
|
|
<a class="brand" href="index.html">
|
|
<span class="mark" aria-hidden="true">N</span>
|
|
NULLDRM
|
|
</a>
|
|
<nav class="site-nav" aria-label="Main">
|
|
<a href="#how">How it works</a>
|
|
<a href="#ways">Ways to run it</a>
|
|
<a href="#start">Quick start</a>
|
|
<a href="docs.html">Docs</a>
|
|
<a href="#source">Source</a>
|
|
<a href="https://t.me/+GZwQ7d_HD2w2ZTM8" rel="noopener" target="_blank">Telegram</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="main">
|
|
|
|
<section class="hero">
|
|
<div class="shell">
|
|
<span class="pill"><span class="dot" aria-hidden="true"></span> Free forever · open source</span>
|
|
<h1>Capture, decrypt and restream your own subscriptions.</h1>
|
|
<p class="tagline">
|
|
NULLDRM logs the Widevine handshake from an Android app, recovers the content keys,
|
|
and keeps a live restream healthy. One binary, self-hosted, no license keys.
|
|
</p>
|
|
<div class="actions">
|
|
<a class="btn btn-primary" href="#start">Quick start</a>
|
|
<a class="btn" href="docs.html">Read the docs</a>
|
|
<a class="btn" href="https://git.nulldrm.com/nulldrm/Null-DRM-Official" rel="noopener">Source</a>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="how">
|
|
<div class="shell">
|
|
<div class="section-head">
|
|
<span class="eyebrow">How it works</span>
|
|
<h2>A capture, start to finish</h2>
|
|
<p>
|
|
An HTTPS proxy runs on the phone. NULLDRM drives the real app to a channel,
|
|
reads the licence exchange as it happens, and resolves the keys with your own CDM.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="terminal">
|
|
<div class="term-bar">
|
|
<span class="lamp" aria-hidden="true"></span>
|
|
<span class="lamp" aria-hidden="true"></span>
|
|
<span class="lamp" aria-hidden="true"></span>
|
|
<span class="name">drm capture — rteone</span>
|
|
</div>
|
|
<pre aria-label="Example output of a capture run"><span class="t-prompt">PS C:\DRM-Decryption></span> ./bin/drm capture --app rte --channel rteone --auto-play
|
|
<span class="t-dim">[*]</span> Stopping any old appproxy...
|
|
<span class="t-dim">[*]</span> Pushing appproxy...
|
|
<span class="t-dim">20:30:45</span> CA cert: /data/local/tmp/appproxy-ca.crt
|
|
<span class="t-dim">20:30:45</span> capture → /data/local/tmp/appproxy_cap.json
|
|
<span class="t-dim">20:30:45</span> DNS → [1.1.1.1:53 1.0.0.1:53 8.8.8.8:53]
|
|
<span class="t-dim">20:30:45</span> listening on [::]:8080
|
|
<span class="t-ok">[+]</span> appproxy pid=12004; capture -> /data/local/tmp/appproxy_cap.json
|
|
<span class="t-dim">[*]</span> Setting HTTP proxy -> 127.0.0.1:8080 (was "null")
|
|
<span class="t-dim">[*]</span> Re-injecting system CA (Magisk)...
|
|
<span class="t-ok">[+]</span> System CA present in conscrypt
|
|
<span class="t-dim">[*]</span> Launching rte (<span class="t-redact">package from module.yaml</span>)…
|
|
<span class="t-dim">[*]</span> Auto-play RTE One…
|
|
<span class="t-dim">[*]</span> tap_ui "Live"/"Live tab, 2 out of 5" @ 324,2253
|
|
<span class="t-dim">[*]</span> tap Play @ 540,720
|
|
<span class="t-ok">[+]</span> player is PLAYING
|
|
<span class="t-dim">[*]</span> Waiting for license + PSSH + manifest…
|
|
<span class="t-dim">watch: .cache\appproxy-default.log</span>
|
|
<span class="t-ok">[+]</span> Capture:
|
|
pid: <span class="t-redact">••••••••••••</span>
|
|
mpd: <span class="t-url">https://dai.google.com/linear/dash/pa/event/<span class="t-redact">••••</span>/stream/<span class="t-redact">••••</span></span>
|
|
license: <span class="t-url">https://widevine.entitlement.<span class="t-redact">••••</span>/wv/web/ModularDrm?schema=1.0&form=json</span>
|
|
pssh: AAAASnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAACoiIG<span class="t-redact">••••</span>…
|
|
<span class="t-ok">[+]</span> key: <span class="t-redact">••••••••••••••••••••••••••••••••</span>:<span class="t-redact">••••••••••••••••••••••••••••••••</span>
|
|
<span class="t-ok">[+]</span> session: outputs\rte\20261005-203112\session.json
|
|
<span class="t-dim">[*]</span> Closing player…
|
|
<span class="t-dim">[*]</span> HTTP proxy cleared</pre>
|
|
</div>
|
|
<p class="term-legend">
|
|
Redacted above: the package id, licence account, programme id, PSSH and keys.
|
|
Those belong to your device and your subscription — NULLDRM keeps them in a local
|
|
file that is never committed.
|
|
</p>
|
|
|
|
<div class="grid grid-3" style="margin-top:28px">
|
|
<article class="card">
|
|
<h3>Nothing is cracked</h3>
|
|
<p>
|
|
Keys come from a Widevine device file you supply. NULLDRM automates the request
|
|
flow a normal client already performs — it does not break the DRM.
|
|
</p>
|
|
</article>
|
|
<article class="card">
|
|
<h3>Catalogue or phone</h3>
|
|
<p>
|
|
Some apps publish a playback catalogue, so a channel resolves with no phone at all.
|
|
Everything else goes through the on-device proxy.
|
|
</p>
|
|
</article>
|
|
<article class="card">
|
|
<h3>Stale keys heal</h3>
|
|
<p>
|
|
Live keys rotate. The agent notices a dead channel, re-runs the capture on a
|
|
phone and posts fresh credentials back to the restreamer.
|
|
</p>
|
|
</article>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="ways">
|
|
<div class="shell">
|
|
<div class="section-head">
|
|
<span class="eyebrow">Ways to run it</span>
|
|
<h2>Start small, grow if you want to</h2>
|
|
<p>Same binary in all three. You decide how much automation and how much cooperation.</p>
|
|
</div>
|
|
|
|
<div class="grid grid-3">
|
|
<article class="card">
|
|
<div class="step" aria-hidden="true">1</div>
|
|
<h3>On your desk</h3>
|
|
<p>
|
|
Run a capture when you need fresh material, decrypt locally, play it back.
|
|
No server, nothing always-on.
|
|
</p>
|
|
<ul>
|
|
<li>One-shot captures</li>
|
|
<li>No VPS required</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<div class="step" aria-hidden="true">2</div>
|
|
<h3>Server plus a phone</h3>
|
|
<p>
|
|
Host the restreamer so channels stay up. Leave a phone plugged into the agent
|
|
and it refreshes keys on its own when something dies.
|
|
</p>
|
|
<ul>
|
|
<li>Dashboard and API on :8083</li>
|
|
<li>Agent watches health on a loop</li>
|
|
</ul>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<div class="step" aria-hidden="true">3</div>
|
|
<h3>Join a swarm</h3>
|
|
<p>
|
|
A host publishes the channel list. Contributors point agents at it and cover
|
|
only the channels they actually subscribe to.
|
|
</p>
|
|
<ul>
|
|
<li>Nobody needs every platform</li>
|
|
<li>Any matching agent can heal a feed</li>
|
|
</ul>
|
|
</article>
|
|
</div>
|
|
|
|
<p class="note">
|
|
That is the point of a swarm: <strong>distributed ownership</strong>. One person
|
|
should not need every subscription. Many agents, many logins, one shared restream.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="start">
|
|
<div class="shell">
|
|
<div class="section-head">
|
|
<span class="eyebrow">Quick start</span>
|
|
<h2>Four commands</h2>
|
|
<p>Go 1.25+ and Python 3.10+. A phone is only needed for the capture step.</p>
|
|
</div>
|
|
|
|
<pre class="code"><span class="c"># 1. build the one binary</span>
|
|
git clone https://git.nulldrm.com/nulldrm/Null-DRM-Official
|
|
go -C apps/cli build -o ../../bin/drm .
|
|
|
|
<span class="c"># 2. the CDM helper</span>
|
|
python -m venv .venv && .venv/bin/pip install -r apps/wvkey/requirements.txt
|
|
|
|
<span class="c"># 3. see which app modules are compiled in</span>
|
|
./bin/drm modules
|
|
|
|
<span class="c"># 4. capture a channel</span>
|
|
./bin/drm capture --app rte --channel rteone --auto-play --wvd data/device.wvd</pre>
|
|
|
|
<p class="note">
|
|
A fresh clone ships no account ids, keys or hostnames — module code is public,
|
|
its values are not. <a href="docs.html?f=quickstart.md">The quickstart</a> walks
|
|
through filling in your own, and
|
|
<a href="docs.html?f=capture.md">the capture guide</a> shows how to discover them
|
|
for an app nobody has written a module for yet.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="inside">
|
|
<div class="shell">
|
|
<div class="section-head">
|
|
<span class="eyebrow">What is inside</span>
|
|
<h2>One binary, a handful of parts</h2>
|
|
<p>Everything ships as <code>drm</code>. Each subcommand is one of these.</p>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr><th>Part</th><th>What it does</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><code>drm capture</code></td>
|
|
<td>Drives the phone through the on-device proxy and writes a session: manifest, PSSH, keys.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>drm catalog</code></td>
|
|
<td>Resolves a channel from a public playback catalogue, no phone involved.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>drm serve</code></td>
|
|
<td>Control plane: REST API, SQLite, dashboard, and the restream supervisor.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>drm agent</code></td>
|
|
<td>Polls the control plane, claims a free phone and refreshes dead channels.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>drm proxy</code></td>
|
|
<td>Builds and installs the on-device HTTPS proxy, and records unknown apps.</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>drm modules</code></td>
|
|
<td>Lists the app modules this build contains and where each one reads its values.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<p class="note">
|
|
An app module is a small Go package plus a local values file. Adding one means a new
|
|
package and a single import line — no fork, no plugin runtime.
|
|
See <a href="docs.html?f=modules.md">writing a module</a>.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section id="source">
|
|
<div class="shell">
|
|
<div class="section-head">
|
|
<span class="eyebrow">Source</span>
|
|
<h2>git.nulldrm.com</h2>
|
|
<p>
|
|
Open source, free forever: no paid tiers, no licence keys, no feature gates.
|
|
The official source is Forgejo at <code>nulldrm/Null-DRM-Official</code>.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="stats" id="git-stats" aria-live="polite">
|
|
<div class="stat"><span class="stat-value" id="stat-stars">—</span><span class="stat-label">Stars</span></div>
|
|
<div class="stat"><span class="stat-value" id="stat-forks">—</span><span class="stat-label">Forks</span></div>
|
|
<div class="stat"><span class="stat-value" id="stat-issues">—</span><span class="stat-label">Issues</span></div>
|
|
<div class="stat"><span class="stat-value" id="stat-updated">—</span><span class="stat-label">Updated</span></div>
|
|
</div>
|
|
|
|
<div class="actions">
|
|
<a class="btn btn-primary" href="https://git.nulldrm.com/nulldrm/Null-DRM-Official" rel="noopener">Open Forgejo</a>
|
|
<a class="btn" href="https://git.nulldrm.com/nulldrm/Null-DRM-Official/releases" rel="noopener">Releases</a>
|
|
<a class="btn" href="https://t.me/+GZwQ7d_HD2w2ZTM8" rel="noopener" target="_blank">Telegram</a>
|
|
</div>
|
|
|
|
<h3 style="margin-top:32px">Latest releases</h3>
|
|
<div id="releases-status" class="status-msg">Loading releases…</div>
|
|
<ul class="releases" id="releases-list" hidden></ul>
|
|
</div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer class="site-footer">
|
|
<div class="shell footer-row">
|
|
<span>nulldrm.com — self-hosted, free forever.</span>
|
|
<nav aria-label="Footer">
|
|
<a href="docs.html">Docs</a>
|
|
<a href="https://git.nulldrm.com/nulldrm/Null-DRM-Official" rel="noopener">Forgejo</a>
|
|
<a href="https://t.me/+GZwQ7d_HD2w2ZTM8" rel="noopener" target="_blank">Telegram</a>
|
|
</nav>
|
|
</div>
|
|
<div class="shell" style="margin-top:14px">
|
|
<p>Use only on services you are authorised to access, with content you have the right to decrypt.</p>
|
|
</div>
|
|
</footer>
|
|
|
|
<div id="tg-modal" class="tg-modal" hidden>
|
|
<div class="tg-backdrop" data-tg-dismiss></div>
|
|
<div class="tg-dialog" role="dialog" aria-modal="true" aria-labelledby="tg-title">
|
|
<button type="button" class="tg-close" data-tg-dismiss aria-label="Close">✕</button>
|
|
<h2 id="tg-title">Join the Telegram group</h2>
|
|
<p>Modules, swarm tips and open-source updates. Shown once — it is always in the menu.</p>
|
|
<div class="tg-actions">
|
|
<a class="btn btn-primary" id="tg-join" href="https://t.me/+GZwQ7d_HD2w2ZTM8" rel="noopener" target="_blank">Join Telegram</a>
|
|
<button type="button" class="btn" data-tg-dismiss>Not now</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="script.js"></script>
|
|
</body>
|
|
</html>
|