From a91dd1d55550d36cda17f8acd777d07d7e8d83ee Mon Sep 17 00:00:00 2001 From: yum Date: Thu, 16 Oct 2025 14:51:18 -0700 Subject: add basic encryption. note that keys are publicly available the intent is just to prevent dragnet snooping --- opt/obsproxy/server.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) (limited to 'opt') diff --git a/opt/obsproxy/server.py b/opt/obsproxy/server.py index df6edb3..e4a2750 100755 --- a/opt/obsproxy/server.py +++ b/opt/obsproxy/server.py @@ -7,6 +7,7 @@ import threading import shutil import time from pathlib import Path +from typing import Optional from flask import Flask, request import logging import atexit @@ -34,6 +35,9 @@ SERVER_DOMAIN = os.environ.get('SERVER_DOMAIN', 'yummers.b-cdn.net') STREAM_HEX = secrets.token_hex(16) STREAM_PATH = BASE_DIR / 'live' / STREAM_HEX HLS_ROUTE_PREFIX = f"/hls/{STREAM_HEX}" +SESSION_KEY_NAME = 'session.key' +SESSION_KEYINFO_NAME = 'session.keyinfo' +SESSION_KEY_URI: Optional[str] = None # Media output settings tuned for VRChat playback AUDIO_BITRATE = '256k' AUDIO_CHANNELS = 2 @@ -65,10 +69,38 @@ if not INGEST_PSK: BASE_DIR.mkdir(parents=True, exist_ok=True) STREAM_PATH.mkdir(parents=True, exist_ok=True) + +def _session_key_path() -> Path: + return STREAM_PATH / SESSION_KEY_NAME + + +def _session_keyinfo_path() -> Path: + return STREAM_PATH / SESSION_KEYINFO_NAME + + +def _write_key_material() -> None: + """Generate and persist AES-128 key + keyinfo for the current session.""" + global SESSION_KEY_URI + + key_bytes = secrets.token_bytes(16) + iv_bytes = secrets.token_bytes(16) + key_path = _session_key_path() + key_path.write_bytes(key_bytes) + + key_uri = f"https://{SERVER_DOMAIN}{HLS_ROUTE_PREFIX}/{SESSION_KEY_NAME}" + keyinfo_path = _session_keyinfo_path() + iv_hex = format(int.from_bytes(iv_bytes, 'big'), '032x') + keyinfo_path.write_text( + f"{key_uri}\n{key_path}\n{iv_hex}\n", + encoding="utf-8", + ) + SESSION_KEY_URI = key_uri + def reset_stream_path(): """Ensure the live stream directory is empty and ready.""" shutil.rmtree(STREAM_PATH, ignore_errors=True) STREAM_PATH.mkdir(parents=True, exist_ok=True) + _write_key_material() def _safe_reset_stream_path(context: str) -> bool: @@ -160,6 +192,11 @@ def _terminate_ffmpeg_process(process: subprocess.Popen[str], *, timeout: float def _build_ffmpeg_command() -> list[str]: """Construct the ffmpeg command line we execute for each attempt.""" + keyinfo_path = _session_keyinfo_path() + if not keyinfo_path.exists(): + _write_key_material() + + keyinfo_path = _session_keyinfo_path() return [ 'ffmpeg', '-nostdin', @@ -180,6 +217,7 @@ def _build_ffmpeg_command() -> list[str]: '-hls_list_size', str(HLS_PLAYLIST_SIZE), '-hls_flags', 'delete_segments+independent_segments', '-hls_delete_threshold', str(HLS_DELETE_THRESHOLD), + '-hls_key_info_file', str(keyinfo_path), '-hls_segment_filename', str(STREAM_PATH / 'segment-%05d.ts'), str(STREAM_PATH / 'stream.m3u8'), ] @@ -381,6 +419,8 @@ def print_instructions(): print("\n[URLS]") print(f" OBS ingest: {obs_url}") print(f" HLS: {hls_url}") + if SESSION_KEY_URI: + print(f" HLS key: {SESSION_KEY_URI}") print("\n[STATUS]") print(f" Stream is {'ACTIVE' if ffmpeg_process else 'INACTIVE'}") -- cgit v1.2.3