#!/usr/bin/env python3 import os, sys, struct, argparse from Crypto.Cipher import AES KEY_DEFAULT = b"rK7CcATuZk7LAmhqqU4iBLNmAq8QbK3s" KEY_ALTERNATE = b"x4DZmD6D2HhkzT6qD8HpKeZdgM9HCmXP" def xor_crypt(data, key): cipher = AES.new(key, AES.MODE_ECB) out = bytearray(len(data)) for off in range(0, len(data), 16): ctr = off // 16 + 1 ks = cipher.encrypt(struct.pack(' {os.path.basename(outpath)}") return True encrypted = xor_crypt(data, key) os.makedirs(os.path.dirname(outpath) or '.', exist_ok=True) with open(outpath, 'wb') as f: f.write(encrypted) print(f" {os.path.basename(inpath)} -> {os.path.basename(outpath)} ({len(data)} bytes)") return True except Exception as e: print(f" ERROR {os.path.basename(inpath)}: {e}", file=sys.stderr) return False def main(): parser = argparse.ArgumentParser( description="Re-encrypt decrypted Unity bundles for Holo's Hanafuda") parser.add_argument('input', help='Input .unity3d file or directory of .unity3d files') parser.add_argument('-o', '--output', help='Output file or directory') parser.add_argument('--key-alternate', action='store_true', help='Use alternate bundle key candidate (x4DZmD6D2HhkzT6qD8HpKeZdgM9HCmXP)') parser.add_argument('--key-hex', metavar='HEX', help='64-char hex AES-256 key (overrides defaults)') parser.add_argument('--in-place', action='store_true', help='Write output next to the original .bundle in StandaloneWindows64/') parser.add_argument('--dry-run', action='store_true', help='List what would be encrypted without writing') args = parser.parse_args() if args.key_hex: key = bytes.fromhex(args.key_hex) if len(key) != 32: print("Error: --key-hex must be exactly 64 hex chars (32 bytes)", file=sys.stderr) sys.exit(1) elif args.key_alternate: key = KEY_ALTERNATE else: key = KEY_DEFAULT inpath = args.input if os.path.isdir(inpath): files = sorted(f for f in os.listdir(inpath) if f.endswith('.unity3d')) if not files: print(f"No .unity3d files found in '{inpath}'") return if args.output: outdir = args.output elif args.in_place: outdir = os.path.normpath(os.path.join(inpath, '..', '..', 'StandaloneWindows64')) else: outdir = os.path.normpath(os.path.join(inpath, '..', 'reencrypted')) if not args.dry_run: os.makedirs(outdir, exist_ok=True) ok = errs = 0 for fname in files: fpath = os.path.join(inpath, fname) base = fname.removesuffix('.unity3d') outpath = os.path.join(outdir, base + '.bundle') if process_file(fpath, outpath, key, args.dry_run): ok += 1 else: errs += 1 total = len(files) print(f"\n{ok} OK, {errs} errors of {total}") if not args.dry_run: print(f"Output directory: {outdir}/") else: if not os.path.exists(inpath): print(f"Error: '{inpath}' not found", file=sys.stderr) sys.exit(1) if args.output: outpath = args.output elif args.in_place: base = os.path.splitext(os.path.basename(inpath))[0] bundle_name = base + '.bundle' candidates = [ os.path.join(os.path.dirname(inpath), bundle_name), os.path.normpath(os.path.join( os.path.dirname(inpath), '..', '..', 'StandaloneWindows64', bundle_name)), ] outpath = None for c in candidates: if os.path.exists(c): outpath = c break if not outpath: print(f"Error: --in-place but no existing .bundle found for '{inpath}'", file=sys.stderr) sys.exit(1) else: basedir = os.path.dirname(inpath) or '.' basename = os.path.splitext(os.path.basename(inpath))[0] + '.bundle' outpath = os.path.join(basedir, basename) process_file(inpath, outpath, key, args.dry_run) if __name__ == '__main__': main()