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:
404errordeveloper 2026-10-06 00:25:35 +02:00
commit 2fa8f2435f
121 changed files with 17802 additions and 0 deletions

View file

@ -0,0 +1,181 @@
// Package catalogcmd resolves a channel's stream credentials from its provider's
// public catalog, with no phone involved. It is provider-agnostic: any app module
// implementing app.Catalog works here.
package catalogcmd
import (
"encoding/json"
"flag"
"fmt"
"os"
"sort"
"strings"
"drmdecryption/app"
"drmdecryption/repo"
"drmdecryption/session"
)
// Run performs a catalog lookup for one channel.
func Run(args []string) error {
fs := flag.NewFlagSet("catalog", flag.ExitOnError)
appName := fs.String("app", "", "app module to query")
channel := fs.String("channel", "", "channel id")
asJSON := fs.Bool("json", false, "print streamd-ready JSON only")
list := fs.Bool("list", false, "list the app's channels and their catalog ids")
writeOut := fs.Bool("out", false, "write outputs/<app>/<stamp>/session.json")
withKeys := fs.Bool("keys", false, "also fetch PSSH and resolve KID:KEY via the local CDM")
python := fs.String("python", "", "python for wvkey.py (default: .venv)")
wvd := fs.String("wvd", "", "path to .wvd device file (required with --keys)")
// Each module contributes --<app>.<field> overrides for its local values.
app.BindFlags(fs)
fs.Usage = func() {
fmt.Fprintf(fs.Output(), "catalog — resolve a channel from its public catalog (no phone)\n\nUsage:\n catalog --app <%s> --channel ID [--keys] [--json]\n\nFlags:\n", strings.Join(catalogApps(), "|"))
fs.PrintDefaults()
}
if err := fs.Parse(args); err != nil {
return err
}
available := catalogApps()
if strings.TrimSpace(*appName) == "" {
if len(available) != 1 {
return fmt.Errorf("--app required (apps with a catalog: %s)", strings.Join(available, ", "))
}
*appName = available[0]
}
a, err := app.Get(*appName)
if err != nil {
return err
}
cat, ok := a.(app.Catalog)
if !ok {
return fmt.Errorf("app %q has no catalog — capture it from the phone instead: drm capture --app %s", a.Name(), a.Name())
}
if *list {
return listChannels(a, cat)
}
if strings.TrimSpace(*channel) == "" {
return fmt.Errorf("--channel required (see: drm catalog --app %s --list)", a.Name())
}
info, err := cat.Resolve(*channel)
if err != nil {
return err
}
if *withKeys {
fmt.Fprintln(os.Stderr, "[*] fetching PSSH + Widevine keys…")
if err := cat.EnrichWithKeys(&info, *python, *wvd); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "[+] key %s (%d total)\n", info.Key, len(info.Keys))
}
if *writeOut || *withKeys {
dir, err := session.WriteStream(repo.Root(), a.Name(), info)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "[+] wrote %s\n", dir)
}
if *asJSON {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(info)
}
printInfo(a.Name(), info)
return nil
}
// catalogApps lists compiled-in modules that can answer catalog lookups.
func catalogApps() []string {
out := []string{}
for _, a := range app.All() {
if _, ok := a.(app.Catalog); ok {
out = append(out, a.Name())
}
}
sort.Strings(out)
return out
}
func listChannels(a app.App, cat app.Catalog) error {
if l, ok := cat.(app.CatalogLister); ok {
rows, flags, err := l.ListChannels()
if err != nil {
return err
}
fmt.Printf("%-10s %-16s %s\n", "CHANNEL", "CATALOG ID", "LABEL")
sort.Slice(rows, func(i, j int) bool { return rows[i].ID < rows[j].ID })
for _, r := range rows {
id := r.CatalogID
if id == "" {
id = "-"
}
fmt.Printf("%-10s %-16s %s\n", r.ID, id, r.Label)
}
if len(flags) > 0 {
keys := make([]string, 0, len(flags))
for k := range flags {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%v", k, flags[k]))
}
fmt.Printf("\nprovider flags: %s\n", strings.Join(parts, " "))
}
return nil
}
// Fall back to the plain channel list every app can produce.
fmt.Printf("%-10s %s\n", "CHANNEL", "LABEL")
for _, ch := range a.Channels() {
fmt.Printf("%-10s %s\n", ch.ID, ch.Label)
}
return nil
}
func printInfo(appName string, info session.Stream) {
fmt.Printf("name: %s\n", info.Name)
fmt.Printf("title: %s\n", info.Title)
fmt.Printf("app/channel: %s / %s\n", info.App, info.Channel)
if info.VideoID != "" {
fmt.Printf("catalog id: %s\n", info.VideoID)
}
fmt.Printf("manifest: %s\n", info.MPD)
fmt.Printf("manifest type: %s\n", info.ManifestType)
fmt.Printf("license_url: %s\n", truncate(info.LicenseURL, 100))
if info.PSSH != "" {
fmt.Printf("pssh: %s\n", truncate(info.PSSH, 64))
}
if info.Key != "" {
fmt.Printf("key: %s\n", info.Key)
}
for i, k := range info.Keys {
if k == info.Key {
continue
}
fmt.Printf("key[%d]: %s\n", i, k)
}
body, _ := json.MarshalIndent(map[string]any{
"name": info.Name,
"title": info.Title,
"app": info.App,
"channel": info.Channel,
"mpd": info.MPD,
"key": info.Key,
"headers_json": info.HeadersJSON,
"rewriter": info.Rewriter,
}, "", " ")
fmt.Printf("\nstreamd create body:\n%s\n", body)
fmt.Printf("\nTip: drm catalog --app %s --channel %s --keys --wvd path/to/device.wvd --json\n",
appName, info.Channel)
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}

36
apps/cli/go.mod Normal file
View file

@ -0,0 +1,36 @@
module drmdecryption/apps/cli
go 1.25.0
require (
drmdecryption v0.0.0
drmdecryption/apps/agent v0.0.0
drmdecryption/apps/capture v0.0.0
drmdecryption/apps/proxy v0.0.0
drmdecryption/apps/streamd v0.0.0
drmdecryption/modules v0.0.0
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.34.5 // indirect
)
replace (
drmdecryption => ../pkg
drmdecryption/apps/agent => ../agent
drmdecryption/apps/capture => ../capture
drmdecryption/apps/proxy => ../proxy
drmdecryption/apps/streamd => ../streamd
drmdecryption/modules => ../modules
)

49
apps/cli/go.sum Normal file
View file

@ -0,0 +1,49 @@
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

76
apps/cli/main.go Normal file
View file

@ -0,0 +1,76 @@
// Command drm is the single entry point for the toolchain: phone capture,
// catalog lookups, the always-on agent, the streamd control plane and the
// on-device MITM. Every app module under apps/modules is compiled in.
package main
import (
"fmt"
"os"
"drmdecryption/apps/agent/agentcmd"
"drmdecryption/apps/capture/capturecmd"
"drmdecryption/apps/cli/catalogcmd"
"drmdecryption/apps/cli/modulescmd"
"drmdecryption/apps/proxy/proxyctlcmd"
"drmdecryption/apps/streamd/servecmd"
// Links every app module into this binary.
_ "drmdecryption/modules/all"
)
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
cmd, args := os.Args[1], os.Args[2:]
var err error
switch cmd {
case "capture":
err = capturecmd.Run(args)
case "catalog":
err = catalogcmd.Run(args)
case "modules":
err = modulescmd.Run(args)
case "agent":
err = agentcmd.Run(args)
case "serve":
err = servecmd.Run(append([]string{"serve"}, args...))
case "proxy":
err = proxyctlcmd.Run(args)
case "help", "-h", "--help":
usage()
return
default:
usage()
err = fmt.Errorf("unknown command %q", cmd)
}
if err != nil {
fmt.Fprintln(os.Stderr, "[!]", err)
os.Exit(1)
}
}
func usage() {
fmt.Fprint(os.Stderr, `drm — DRM capture / restream toolchain
Usage:
drm modules list compiled-in app modules and their channels
drm capture --app NAME [--channel ID] [--auto-play] [--wvd PATH]
one-shot phone MITM capture
drm catalog --app NAME --channel ID [--keys] [--json] [--list]
resolve a channel from its public catalog (no phone)
drm agent run|status|devices|enqueue|cancel [--config …]
always-on credential refresher
drm serve [--bind …] [--data …] [--token …]
streamd control plane + dashboard
drm proxy build|push|install-ca|start|stop|discover|pull|clear-proxy
on-device HTTPS MITM
App modules are compiled in; their local values live in
apps/modules/<name>/module.yaml (gitignored) and can be overridden per run with
--<app>.<field> flags or <APP>_<FIELD> environment variables.
Run any subcommand with --help for its own flags.
`)
}

View file

@ -0,0 +1,78 @@
// Package modulescmd lists the app modules compiled into this binary, their
// channels, and where each one's local values came from.
package modulescmd
import (
"flag"
"fmt"
"os"
"sort"
"text/tabwriter"
"drmdecryption/app"
"drmdecryption/modcfg"
)
// Run prints the compiled-in app modules.
func Run(args []string) error {
fs := flag.NewFlagSet("modules", flag.ExitOnError)
verbose := fs.Bool("channels", true, "list each module's channels")
if err := fs.Parse(args); err != nil {
return err
}
names := app.Names()
if len(names) == 0 {
return fmt.Errorf("no app modules compiled in — check the blank imports in apps/modules/all/all.go")
}
tw := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(tw, "APP\tPACKAGE\tKEY MODE\tREWRITER\tCHANNELS\tVALUES")
for _, name := range names {
a, err := app.Get(name)
if err != nil {
fmt.Fprintf(tw, "%s\t-\t-\t-\t-\t%v\n", name, err)
continue
}
rewriter := "none"
if sd, ok := a.(app.StreamDefaults); ok && sd.RewriterName() != "" {
rewriter = sd.RewriterName()
}
pkg := a.Package()
if pkg == "" {
pkg = "(not configured)"
}
values := modcfg.Path(name)
if _, err := os.Stat(values); err != nil {
values = "missing: " + values
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%d\t%s\n",
name, pkg, a.KeyMode(), rewriter, len(a.Channels()), values)
}
_ = tw.Flush()
if !*verbose {
return nil
}
for _, name := range names {
a, err := app.Get(name)
if err != nil {
continue
}
chans := a.Channels()
sort.Slice(chans, func(i, j int) bool { return chans[i].ID < chans[j].ID })
fmt.Printf("\n%s channels:", name)
if len(chans) == 0 {
fmt.Printf(" none — add %s\n", modcfg.Path(name))
continue
}
fmt.Println()
for _, ch := range chans {
catalog := ""
if _, ok := a.(app.Catalog); ok {
catalog = " (catalog)"
}
fmt.Printf(" %-10s %s%s\n", ch.ID, ch.Label, catalog)
}
}
return nil
}