342 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			342 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
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):
 | 
						||
        if not self.context.update_code:
 | 
						||
            app_logger().info("No update project proguard found")
 | 
						||
            self.context.proguard_dict = self.context.get_map()
 | 
						||
            return
 | 
						||
        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())
 | 
						||
 | 
						||
        self.context.save_map(self.context.proguard_dict)
 | 
						||
        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
 |