Null-DRM-Official/www/index.html
404errordeveloper 2fa8f2435f 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.
2026-10-06 00:25:35 +02:00

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&gt;</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 -&gt; /data/local/tmp/appproxy_cap.json
<span class="t-dim">[*]</span> Setting HTTP proxy -&gt; 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&amp;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 &amp;&amp; .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>