import binascii import hashlib import json import os import re import xml.etree.ElementTree as ET from pathlib import Path from Crypto.Cipher import AES from Crypto.Util.Padding import pad from scripts.context import Context from scripts.task import Task from utils import FileUtils from utils.logger_utils import app_logger def encrypt_content(key: str, content: str) -> str: """ 加密内容,与Kotlin版本算法保持一致 使用AES/ECB/PKCS5Padding模式,MD5生成密钥 """ if not key: raise ValueError("Key cannot be null or empty.") if not content: raise ValueError("Content cannot be null or empty.") # 生成密钥的MD5哈希 key_bytes = generate_key_hash(key) # 将内容转换为UTF-8字节 content_bytes = content.encode('utf-8') # 初始化AES加密器,使用ECB模式和PKCS5填充 cipher = AES.new(key_bytes, AES.MODE_ECB) # 对内容进行填充并加密 padded_content = pad(content_bytes, AES.block_size) encrypted_bytes = cipher.encrypt(padded_content) # 将加密后的字节转换为小写十六进制字符串 return binascii.hexlify(encrypted_bytes).decode().lower() def generate_key_hash(key: str) -> bytes: """生成密钥的MD5哈希,返回16字节的密钥""" md5 = hashlib.md5() md5.update(key.encode('utf-8')) return md5.digest() def simple_encrypt(text, key): return encrypt_content(key, text) def encrypt_xml_resources(file_path, backup=True, key=""): """读取XML资源文件,加密所有string内容,然后写回""" try: # 创建备份文件 if backup and os.path.exists(file_path): backup_path = f"{file_path}.bak" with open(file_path, 'r', encoding='utf-8') as f_in, \ open(backup_path, 'w', encoding='utf-8') as f_out: f_out.write(f_in.read()) print(f"已创建备份文件: {backup_path}") # 解析XML文件 tree = ET.parse(file_path) root = tree.getroot() # 遍历所有string元素并加密内容 for string_elem in root.findall('string'): original_text = string_elem.text.replace("\\'", "'") if original_text is not None: # 加密文本 encrypted_text = simple_encrypt(original_text, key) # 更新元素内容 string_elem.text = encrypted_text print(f"加密: {original_text} -> {encrypted_text}") # 写回加密后的内容 tree.write(file_path, encoding='utf-8', xml_declaration=True) print(f"加密完成,已更新文件: {file_path}") except Exception as e: print(f"处理文件时出错: {str(e)}") def get_all_string_names(xml_file): # 解析XML文件 tree = ET.parse(xml_file) root = tree.getroot() # 存储所有name属性值 string_names = [] # 遍历所有string标签 for string_elem in root.findall('string'): # 获取name属性值 name = string_elem.get('name') if name: string_names.append(name) return string_names def get_all_style_names(xml_file): # 解析XML文件 tree = ET.parse(xml_file) root = tree.getroot() # 存储所有name属性值 string_names = [] # 遍历所有string标签 for string_elem in root.findall('style'): # 获取name属性值 name = string_elem.get('name') if name: string_names.append(name) return string_names def extract_launcher_ids(xml_content): """ 修复版:提取所有以 "launcher_" 开头的 android:id 值 """ pattern = r'android:id\s*=\s*"@\+id/(launcher_\w+)"' matches = re.findall(pattern, xml_content) return matches def string_to_md5(text): # 将字符串编码为UTF-8字节 text_bytes = text.encode('utf-8') # 创建MD5哈希对象并更新字节数据 md5_hash = hashlib.md5(text_bytes) # 返回十六进制哈希字符串 return md5_hash.hexdigest() def generate_encryption_key(key: str, s_len: int = -1, target_package_name: str = "") -> str: # if game_editor == "Cocos": # return key handle_key = target_package_name + key + target_package_name processed_key = string_to_md5(handle_key) while processed_key[0].isdigit(): processed_key = processed_key[1:] # 移除首字符 # 计算目标长度 if s_len > 0: target_length = s_len else: target_length = len(key) # 取前N位(根据原始key长度),不足则全部保留 # 如果处理后的key为空(极小概率),则返回空字符串 return processed_key[:target_length] if processed_key else "" class ProjectProguard(Task): def __init__(self, context: Context): super().__init__(context) self.root = self.context.temp_project_path self.module_path = os.path.join(self.root, "launcher-game") self.code_path = os.path.join(self.module_path, "src") self.res_path = os.path.join(self.module_path, "res") self.drawable_path = os.path.join(self.res_path, "drawable") self.drawable_xxhdpi_path = os.path.join(self.res_path, "drawable-xxhdpi") self.layout_path = os.path.join(self.res_path, "layout") self.string_path = "" self.launcher_xml = "" self.launcher_java = "" self.style_path = "" def add_proguard_key(self, key: str) -> str: value = self.context.proguard_dict.get(key) if value: return value value = generate_encryption_key(key, target_package_name=self.context.package_name) self.context.proguard_dict[key] = value return value def write_proguard(self): file_path = os.path.join(self.context.temp_project_path, "proguard.pro") lines = open(file_path, "r", encoding="UTF-8").readlines() lines.append(f""" -repackageclasses '{self.context.package_name}' # 将所有类移动到 '{self.context.package_name}' 包下 -flattenpackagehierarchy '{self.context.package_name}' # 扁平化包结构 """) open(file_path, "w", encoding="UTF-8").writelines(lines) pass def proguard_code(self): code_package_name_list = ["com.game.launcher.activity", "com.game.launcher.view", "com.game.launcher.service"] for code_package in code_package_name_list: code_dir = os.path.join(self.code_path, code_package.replace(".", os.sep)) files = os.listdir(code_dir) self.context.proguard_dict[code_package] = self.context.package_name for file in files: file_path = Path(file) file_name = file_path.stem file_ext = file_path.suffix target_class_name = self.add_proguard_key(file_name) target_file_path = os.path.join(self.code_path, self.context.package_name.replace(".", os.sep), target_class_name + file_ext) o = os.path.join(code_dir, file) self.context.proguard_dict[ code_package + "." + file_name] = self.context.package_name + "." + target_class_name FileUtils.move(o, target_file_path) pass pass def proguard_pag(self): pag_file = os.path.join(self.module_path, "assets/pag_gl_slide.pag".replace("/", os.sep)) target_pag = self.add_proguard_key("pag_gl_slide") FileUtils.move(pag_file, pag_file.replace("pag_gl_slide", target_pag)) pass def proguard_file_name(self, dir_path: str): for file in os.listdir(dir_path): path = Path(file) file_name = path.stem target_file_name = self.add_proguard_key(file_name) abspath = os.path.join(dir_path, file) FileUtils.move(abspath, abspath.replace(file_name, target_file_name)) pass def analysis_id_dir(self, dir_path: str): for file in os.listdir(dir_path): file = os.path.join(dir_path, file) self.analysis_id_file(file) pass def analysis_id_file(self, path: str): ids = extract_launcher_ids(open(path, "r", encoding="UTF-8").read()) for id in ids: self.add_proguard_key(id) pass def analysis_string_name(self, file_path: str): names = get_all_string_names(file_path) for name in names: self.add_proguard_key(name) pass def analysis_style_name(self, file_path: str): names = get_all_style_names(file_path) for name in names: self.add_proguard_key(name) pass def update_proguard_file(self, file_path: str): if not (file_path.endswith(".java") or file_path.endswith(".xml") or file_path.endswith(".kt")): return text = open(file_path, "r", encoding="UTF-8").read() for key, value in self.context.proguard_dict.items(): text = text.replace(key, value) open(file_path, "w", encoding="UTF-8").writelines(text) def update_proguard_dir(self, dir_path: str): for root, dirs, files in os.walk(dir_path): for file in files: file_path = os.path.join(root, file) self.update_proguard_file(file_path) pass def execute(self): self.root = self.context.temp_project_path self.module_path = os.path.join(self.root, "launcher-game") self.code_path = os.path.join(self.module_path, "src") self.res_path = os.path.join(self.module_path, "res") self.drawable_path = os.path.join(self.res_path, "drawable") self.drawable_xxhdpi_path = os.path.join(self.res_path, "drawable-xxhdpi") self.layout_path = os.path.join(self.res_path, "layout") self.string_path = os.path.join(self.res_path, "values", "strings.xml") self.style_path = os.path.join(self.res_path, "values", "styles.xml") self.launcher_xml = os.path.join(self.root, "res/layout/launcher.xml".replace("/", os.sep)) self.launcher_java = os.path.join(self.root, "src/com/android/launcher3/Launcher.java".replace("/", os.sep)) self.write_proguard() self.proguard_code() self.proguard_pag() self.proguard_file_name(self.drawable_path) self.proguard_file_name(self.drawable_xxhdpi_path) self.proguard_file_name(self.layout_path) self.analysis_id_file(self.launcher_xml) self.analysis_id_dir(self.layout_path) self.analysis_string_name(self.string_path) self.analysis_style_name(self.style_path) self.add_proguard_key("gltaskaffinity") self.context.proguard_dict = { k: v for k, v in sorted( self.context.proguard_dict.items(), key=lambda item: len(item[0]), reverse=True ) } self.update_proguard_file(os.path.join(self.context.temp_project_path, "src/com/android/launcher3/views/OptionsPopupView.java".replace("/", os.sep))) self.update_proguard_file(self.launcher_xml) self.update_proguard_file(self.launcher_java) self.update_proguard_file(self.string_path) self.update_proguard_dir(self.layout_path) self.update_proguard_dir(self.drawable_xxhdpi_path) self.update_proguard_dir(self.drawable_path) self.update_proguard_dir(self.module_path) self.update_proguard_dir(os.path.join(self.context.temp_project_path, "lawnchair/res/xml")) encrypt_xml_resources(self.string_path, False, string_to_md5(self.context.package_name).upper()) app_logger().info(json.dumps(self.context.proguard_dict, indent=4)) # if __name__ == '__main__': # encrypt_xml_resources( # "/Users/luojian/Documents/project/zicp/Lawnchair_DollMasterLauncher/launcher-game/res/values/strings.xml") # # key = string_to_md5("com.drop.meme.merge.game.fsaew.puzzle").upper() # # print(key) # # result = encrypt_content(key, "hello World") # # print(result) # pass