Files
brfc-stream-pirate/creds.py
T
Mikkeli Matlock 21113bd6ba Capture Bristol Rovers video pass streams for personal time-shifting
Verified end to end against a live match: 1080p H.264/AAC MPEG-TS at
~5.2 Mbit/s, recorded while the browser played the same stream.

Components:
- tampermonkey sniffer hooks fetch/XHR at document-start, captures the
  manifest URL verbatim and POSTs it to the local catcher. v0.4 also traces
  the API chain to find which call mints the token.
- catcher receives the URL and launches yt-dlp. Dedupes on the Kaltura asset
  id and checks downloader liveness, so a mid-match page reload is both safe
  and the recovery path for a dead downloader.
- creds stores secrets in the OS vault rather than a gitignored file, and
  refuses to run on a backend that does not encrypt.

Protocol notes are in the README: the stream uses a two-stage token chain
(a ~25s hdnts token for the master playlist, a ~24h hdntl token for media),
concurrency is unrestricted, and the live window is ~30s with no DVR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kyt7QHQvLJtQRuiY4wAqKB
2026-08-29 23:57:05 +09:00

157 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Credential storage for unattended recording.
Secrets live in the Windows Credential Manager (DPAPI, keyed to your Windows
account), not in a file. Nothing here can be committed by accident because
nothing here is written to the repo.
python creds.py --set-login store email + password (prompts)
python creds.py --show show what is stored (never the password)
python creds.py --clear remove everything
What this does and does not protect against:
DOES -- plaintext at rest, and the classic "gitignored file gets committed
after a git add -A" accident.
DOES -- readable by another Windows user on this machine.
NOT -- a process already running as you. Credential Manager unlocks with
your login session, so anything running as you can read it. That is
inherent to unattended operation: a job that logs in without you
present must be able to reach the secret without you present.
Caveat for Task Scheduler: "Run whether user is logged on or not" runs in a
session where DPAPI may not be able to decrypt these entries. Prefer "Run only
when user is logged on" for any task that needs credentials.
PLATFORM NOTE -- this module is only safe where a real vault backend exists:
Windows WinVaultKeyring (Credential Manager, DPAPI). Good.
Linux+GUI SecretService (GNOME Keyring / KWallet). Good.
Headless No D-Bus session, no unlocked keyring. `keyring` NOT SAFE.
Linux falls back to a fail or plaintext backend.
On a headless Pi or server, do NOT use this module. Use systemd-creds, which
encrypts to the host/TPM key and is decrypted only for the service:
systemd-creds encrypt --name=brfc-pass - /etc/credstore.encrypted/brfc-pass
# then in the unit: LoadCredential=brfc-pass
# and read it from: $CREDENTIALS_DIRECTORY/brfc-pass
Run `python creds.py --check` on each host before trusting it with a secret.
"""
import argparse
import getpass
import sys
try:
import keyring
except ImportError:
sys.exit("keyring not installed. Run: pip install keyring")
SERVICE = "brfc-stream-pirate"
# Keys held in the vault.
K_USER = "username"
K_PASS = "password"
K_COOKIE = "session-cookie" # cached login session, refreshed periodically
def get(key):
return keyring.get_password(SERVICE, key)
def put(key, value):
keyring.set_password(SERVICE, key, value)
def drop(key):
try:
keyring.delete_password(SERVICE, key)
except keyring.errors.PasswordDeleteError:
pass # already absent
def load_login():
"""Return (username, password), or None if not configured yet."""
user, pw = get(K_USER), get(K_PASS)
return (user, pw) if user and pw else None
def load_cookie():
"""Cached session cookie, or None. Callers must handle it being stale."""
return get(K_COOKIE)
def save_cookie(value):
put(K_COOKIE, value)
# Backends that do not actually protect anything. `keyring` selects these
# silently on a headless box, so a secret stored there is effectively plaintext.
UNSAFE_BACKENDS = {"PlaintextKeyring", "Keyring", "fail.Keyring", "EnvironCredential"}
def backend():
return keyring.get_keyring().__class__.__name__
def backend_is_safe():
name = backend()
return name not in UNSAFE_BACKENDS and "fail" not in name.lower()
def _mask(s):
if not s:
return "(not set)"
return s[:2] + "*" * max(0, len(s) - 4) + s[-2:] if len(s) > 4 else "*" * len(s)
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
g = ap.add_mutually_exclusive_group(required=True)
g.add_argument("--set-login", action="store_true", help="store email + password")
g.add_argument("--show", action="store_true", help="show stored username")
g.add_argument("--clear", action="store_true", help="remove all stored secrets")
g.add_argument("--check", action="store_true",
help="report whether this host has a real vault backend")
args = ap.parse_args()
if args.check:
name = backend()
safe = backend_is_safe()
print(f"backend : {name}")
print(f"verdict : {'SAFE' if safe else 'NOT SAFE -- do not store secrets here'}")
if not safe:
print(" use systemd-creds on this host (see module docstring)")
sys.exit(0 if safe else 1)
if not backend_is_safe():
sys.exit(f"refusing to use backend {backend()!r}: it does not encrypt. "
"Run 'python creds.py --check' for guidance.")
if args.set_login:
user = input("email: ").strip()
if not user:
sys.exit("aborted: empty email")
pw = getpass.getpass("password: ")
if not pw:
sys.exit("aborted: empty password")
put(K_USER, user)
put(K_PASS, pw)
print(f"stored in Windows Credential Manager under '{SERVICE}'")
elif args.show:
print(f"service : {SERVICE}")
print(f"username: {get(K_USER) or '(not set)'}")
print(f"password: {'(set)' if get(K_PASS) else '(not set)'}")
print(f"cookie : {_mask(get(K_COOKIE))}")
elif args.clear:
for k in (K_USER, K_PASS, K_COOKIE):
drop(k)
print("cleared")
if __name__ == "__main__":
main()