""" COPYRIGHT Bill Demirkapi 2025 Small utility I wrote with some help from Gemini Pro 2.5 to decrypt/encrypt BMC config backups from my ASUS ASMB11-iKVM AST2600 BMC. Fed the model a bunch of decompiled code from BMC firmware libaries like libaes and libBackupConf. Probably works with other Megarac-based BMCs too. Expects an AESKey and AESIV file in script directory. This varies by OEM, often requires unpacking firmware/flash image. For my ASUS BMC, however, the Key/IV for config are just NULL keys. AESKey file content is "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", AESIV's is "AAAAAAAAAAAAAAAAAAAAAA==". """ import argparse import base64 import hashlib import os import re import sys from pathlib import Path try: from Crypto.Cipher import AES except ImportError: print("Error: PyCryptodome is not installed. Please install it with 'pip install pycryptodome'", file=sys.stderr) sys.exit(1) # --- Constants --- # Based on a definitive reading of the C code's processing loop PLAINTEXT_CHUNK_SIZE = 255 ENCRYPTED_CHUNK_SIZE = 256 BASE64_CHUNK_SIZE = 344 SIGNATURE_LENGTH = 40 # Based on reverse-engineering the GetCheckSumKey function's array. # The index from the file maps directly to a key in this dictionary. CHECKSUM_KEYS = { 0: "megarac", 1: "megaracsp", 2: "megaracsp2", 3: "megaracspx", 4: "megarac1", 5: "megarac3", 6: "megarac4", 7: "megarac5", 8: "megarac6", } AES_BLOCK_SIZE = 16 class BmcBackupTool: """A tool to decrypt and encrypt ASUS BMC configuration backup files.""" def __init__(self, verbose=False): self.key = None self.iv = None self.verbose = verbose self.active_checksum_key = None def _vprint(self, *args, **kwargs): if self.verbose: print("[VERBOSE]", *args, **kwargs) def _load_keys(self): try: with open("AESKey", "r") as f: key_b64 = f.read().strip() with open("AESIV", "r") as f: iv_b64 = f.read().strip() self.key = base64.b64decode(key_b64) self.iv = base64.b64decode(iv_b64) self._vprint(f"Loaded AESKey (b64): {key_b64}") self._vprint(f"Loaded AESIV (b64): {iv_b64}") except FileNotFoundError as e: print(f"Error: Could not find key file '{e.filename}'.", file=sys.stderr) sys.exit(1) def decrypt(self, input_path: Path, output_path: Path): print(f"--> Loading backup file: {input_path}") parsed_content = self._parse_backup_file(input_path) if not parsed_content: print("Error: Failed to parse backup file. File may be empty or malformed.", file=sys.stderr) return print("--> Verifying SHA1 signature...") if not self._verify_signature(parsed_content): print("Warning: SHA1 signature verification FAILED.", file=sys.stderr) else: print("--> Signature OK.") print("--> Loading AES key and IV...") self._load_keys() print("--> Decrypting data...") decrypted_archive = self._decrypt_data_blob(parsed_content["data_blob"]) if decrypted_archive is None: return print(f"--> Extracting files to: {output_path}") self._extract_archive(decrypted_archive, output_path) print("\nDecryption complete.") def encrypt(self, input_dir: Path, output_file: Path): unencrypted_archive = self._build_unencrypted_archive(input_dir) self._load_keys() encrypted_blob = self._encrypt_archive(unencrypted_archive) final_content = self._build_final_file(encrypted_blob) output_file.write_bytes(final_content) print(f"\nEncryption complete. File saved to: {output_file}") def _parse_backup_file(self, path: Path) -> dict | None: """Parses the backup file by strictly adhering to the C code's slicing method.""" try: raw_content = path.read_bytes() if len(raw_content) < SIGNATURE_LENGTH: return None content_for_hashing = raw_content[:-SIGNATURE_LENGTH] signature_bytes = raw_content[-SIGNATURE_LENGTH:] data_header_marker = b'$$$Data=' data_start_pos = content_for_hashing.find(data_header_marker) if data_start_pos == -1: return None blob_start_offset = data_start_pos + len(data_header_marker) data_blob_bytes = content_for_hashing[blob_start_offset:] match_key_index = re.search(rb"\$\$\$CheckSumKeyIndex=(\d+)\$", content_for_hashing) if not match_key_index: return None checksum_key_index = int(match_key_index.group(1)) return { "content_for_hashing": content_for_hashing, "signature": signature_bytes.decode('ascii').strip(), "checksum_key_index": checksum_key_index, "data_blob": data_blob_bytes.decode('ascii'), } except Exception as e: self._vprint(f"Parsing failed with exception: {e}") return None def _verify_signature(self, parsed_content: dict) -> bool: """Verifies the SHA1 signature by using the correct checksum key.""" key_index = parsed_content["checksum_key_index"] content_before_hash = parsed_content["content_for_hashing"] signature = parsed_content["signature"] checksum_key = CHECKSUM_KEYS.get(key_index) if not checksum_key: print(f"Error: CheckSumKeyIndex '{key_index}' is unknown.", file=sys.stderr) return False self._vprint(f"Using Checksum Key: '{checksum_key}' for index {key_index}") data_to_hash = content_before_hash + f"\nKEY={checksum_key}".encode("ascii") calculated_hash = hashlib.sha1(data_to_hash).hexdigest() self._vprint(f"Calculated SHA1: {calculated_hash}") self._vprint(f"Expected SHA1: {signature}") is_valid = calculated_hash.lower() == signature.lower() if is_valid: self.active_checksum_key = checksum_key return is_valid def _decrypt_data_blob(self, data_blob_str: str) -> bytes | None: decrypted_chunks = [] sanitized_blob = "".join(data_blob_str.split()) for i in range(0, len(sanitized_blob), BASE64_CHUNK_SIZE): cipher = AES.new(self.key, AES.MODE_CBC, self.iv) b64_chunk = sanitized_blob[i : i + BASE64_CHUNK_SIZE] if len(b64_chunk) != BASE64_CHUNK_SIZE: continue encrypted_chunk_full = base64.b64decode(b64_chunk) encrypted_chunk_for_decryption = encrypted_chunk_full[:ENCRYPTED_CHUNK_SIZE] if len(encrypted_chunk_for_decryption) != ENCRYPTED_CHUNK_SIZE: return None decrypted_padded_chunk = cipher.decrypt(encrypted_chunk_for_decryption) decrypted_chunks.append(decrypted_padded_chunk[:PLAINTEXT_CHUNK_SIZE]) return b"".join(decrypted_chunks).rstrip(b'\x00') def _extract_archive(self, decrypted_data: bytes, output_dir: Path): output_dir.mkdir(parents=True, exist_ok=True) file_entries = re.split(rb"\n?\[\$\$\$", decrypted_data) for entry in file_entries: if not entry: continue full_entry = b"[$$$" + entry header_match = re.match(rb'\[\$\$\$([^\]]+)\]\s*\$\$\$DataLength=(\d+)\s*', full_entry, re.DOTALL) if not header_match: continue original_path_str = header_match.group(1).decode("utf-8", "ignore") file_content = full_entry[header_match.end() : header_match.end() + int(header_match.group(2))] target_path = output_dir / Path(original_path_str.lstrip('/\\')) target_path.parent.mkdir(parents=True, exist_ok=True) print(f" - Writing {target_path} ({len(file_content)} bytes)") target_path.write_bytes(file_content) def _encrypt_archive(self, plaintext_archive: bytes) -> str: final_base64_blob = '' for i in range(0, len(plaintext_archive), PLAINTEXT_CHUNK_SIZE): cipher = AES.new(self.key, AES.MODE_CBC, self.iv) plaintext_chunk = plaintext_archive[i : i + PLAINTEXT_CHUNK_SIZE] padded_chunk = plaintext_chunk.ljust(ENCRYPTED_CHUNK_SIZE, b'\x00') encrypted_chunk = cipher.encrypt(padded_chunk) final_base64_blob += base64.b64encode(encrypted_chunk).decode('ascii') return final_base64_blob def _build_unencrypted_archive(self, input_dir: Path) -> bytes: archive_parts = [] for root, _, files in os.walk(input_dir): for filename in files: file_path = Path(root) / filename relative_path = file_path.relative_to(input_dir) archive_path = "/" + str(relative_path).replace("\\", "/") file_content = file_path.read_bytes() header = f"\n[$$${archive_path}]\n$$$DataLength={len(file_content)}$\n".encode() archive_parts.append(header) archive_parts.append(file_content) return b"".join(archive_parts).lstrip() def _build_final_file(self, encrypted_blob: str) -> bytes: # For creating new files, we will use the same index and key that was validated. # Default to the one from the sample file if no validation was run. checksum_key_index = 1 checksum_key = self.active_checksum_key or CHECKSUM_KEYS.get(checksum_key_index) self._vprint(f"Using key '{checksum_key}' for new backup signature.") version = 1 headers_part = ( f"$$$Version={version}$\n" f"$$$CheckSumKeyIndex={checksum_key_index}$\n\n" f"[$$$/tmp/conf.bak]\n" f"$$$IsEncrypted=1$\n" f"$$$DataLength={len(encrypted_blob)}$\n" f"$$$Data=" ).encode('ascii') content_before_hash = headers_part + encrypted_blob.encode('ascii') data_to_hash = content_before_hash + f"\nKEY={checksum_key}".encode("ascii") signature = hashlib.sha1(data_to_hash).hexdigest() return content_before_hash + signature.encode('ascii') def main(): parser = argparse.ArgumentParser(description="A tool to decrypt and encrypt ASUS ASMBM11-iKVM BMC configuration backups.") parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose/debug output.") subparsers = parser.add_subparsers(dest="command", required=True) parser_decrypt = subparsers.add_parser("decrypt") parser_decrypt.add_argument("input_file", type=Path) parser_decrypt.add_argument("output_dir", type=Path) parser_encrypt = subparsers.add_parser("encrypt") parser_encrypt.add_argument("input_dir", type=Path) parser_encrypt.add_argument("output_file", type=Path) args = parser.parse_args() tool = BmcBackupTool(verbose=args.verbose) if args.command == "decrypt": tool.decrypt(args.input_file, args.output_dir) elif args.command == "encrypt": tool.encrypt(args.input_dir, args.output_file) if __name__ == "__main__": main()