Skip to content

Instantly share code, notes, and snippets.

@UserUnknownFactor
Last active June 7, 2025 08:17
Show Gist options
  • Save UserUnknownFactor/74698a2ec1df3643b164f301981ea961 to your computer and use it in GitHub Desktop.
Save UserUnknownFactor/74698a2ec1df3643b164f301981ea961 to your computer and use it in GitHub Desktop.
Tool to decrypt and re-encrypt Bruns engine game files (images and scripts)
import os
import struct
import argparse
import numpy as np
import zlib
def generate_xor_key(size):
"""Generate the XOR key based on the size value"""
xor_key = np.array([
size & 0xFF,
(size >> 8) & 0xFF,
(size >> 16) & 0xFF,
(size >> 24) & 0xFF
], dtype=np.uint8)
# Apply the transformation from the original code
xor_key[0] ^= (0xBEEF & 0xFF)
xor_key[1] ^= ((0xBEEF >> 8) & 0xFF)
xor_key[2] ^= ((0xDEADBEEF >> 16) & 0xFF)
xor_key[3] ^= ((0xDEADBEEF >> 24) & 0xFF)
return xor_key
def decrypt_file(input_file, output_file):
with open(input_file, 'rb') as f:
data = f.read()
if not (data[0:3] == b'EEN' and (data[3] == ord('C') or data[3] == ord('Z'))):
print(f"Invalid file format: {input_file}")
return False
is_compressed = (data[3] == ord('Z'))
size = struct.unpack('<I', data[4:8])[0]
xor_key = generate_xor_key(size)
encrypted_data = np.frombuffer(data[8:], dtype=np.uint8)
repeated_key = np.tile(xor_key, (len(encrypted_data) + 3) // 4)[:len(encrypted_data)]
decrypted_data = encrypted_data ^ repeated_key
if is_compressed:
try:
decompressed_data = zlib.decompress(decrypted_data.tobytes())
if len(decompressed_data) != size:
print(f"Warning: Decompressed size ({len(decompressed_data)}) doesn't match expected size ({size})")
final_data = decompressed_data
except zlib.error as e:
print(f"Decompression error: {e}")
return False
else:
# For EENC files, just use the decrypted data
if len(decrypted_data) != size:
print(f"Warning: Decrypted size ({len(decrypted_data)}) doesn't match expected size ({size})")
final_data = decrypted_data.tobytes()
# Write the final data to the output file
with open(output_file, 'wb') as f:
f.write(final_data)
print(f"Successfully decrypted: {input_file} -> {output_file}")
return True
def encrypt_file(input_file, output_file, force_format=None):
with open(input_file, 'rb') as f:
data = f.read()
if force_format:
is_compressed = force_format.upper() == 'EENZ'
else:
ext = os.path.splitext(input_file)[1].lower()
is_compressed = (ext != '.png')
header = bytearray()
header.extend(b'EEN')
header.extend(b'Z' if is_compressed else b'C')
if is_compressed:
compressed_data = zlib.compress(data)
processed_data = compressed_data
original_size = len(data)
else:
processed_data = data
original_size = len(data)
# Add size to header
header.extend(struct.pack('<I', original_size))
xor_key = generate_xor_key(original_size)
data_array = np.frombuffer(processed_data, dtype=np.uint8)
repeated_key = np.tile(xor_key, (len(data_array) + 3) // 4)[:len(data_array)]
encrypted_data = data_array ^ repeated_key
with open(output_file, 'wb') as f:
f.write(header)
f.write(encrypted_data.tobytes())
print(f"Successfully encrypted: {input_file} -> {output_file}")
return True
def batch_process(input_dir, output_dir, mode):
if not os.path.exists(output_dir):
os.makedirs(output_dir)
success_count = 0
fail_count = 0
for file in os.listdir(input_dir):
input_path = os.path.join(input_dir, file)
if os.path.isfile(input_path):
if mode == 'decrypt':
if file.endswith('.bso') or file.endswith('.png'):
if decrypt_file(input_path, os.path.join(output_dir, file)):
success_count += 1
else:
fail_count += 1
else: # encrypt
output_path = os.path.join(output_dir, file)
force_format = 'EENC' if file.lower().endswith('.png') else 'EENZ'
if encrypt_file(input_path, output_path, force_format):
success_count += 1
else:
fail_count += 1
print(f"{mode.capitalize()} complete. Success: {success_count}, Failed: {fail_count}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Encrypt/Decrypt Bruns game resource files')
parser.add_argument('--mode', choices=['encrypt', 'decrypt'], required=True, help='Operation mode')
parser.add_argument('--file', help='Single file to process')
parser.add_argument('--output', help='Output file for single file processing')
parser.add_argument('--format', choices=['EENC', 'EENZ'], help='Force format for encryption (EENC for uncompressed, EENZ for compressed)')
parser.add_argument('--input-dir', help='Directory containing files to process')
parser.add_argument('--output-dir', help='Directory to save processed files')
args = parser.parse_args()
if args.file and args.output:
if args.mode == 'decrypt':
decrypt_file(args.file, args.output)
else:
encrypt_file(args.file, args.output, args.format)
elif args.input_dir and args.output_dir:
batch_process(args.input_dir, args.output_dir, args.mode)
else:
parser.print_help()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment