2025-10-10 12:53:00 +00:00
|
|
|
|
import binascii
|
2025-07-08 00:58:35 +00:00
|
|
|
|
import hashlib
|
2025-10-10 12:53:00 +00:00
|
|
|
|
import json
|
2025-07-08 00:58:35 +00:00
|
|
|
|
import os
|
2025-07-10 16:10:25 +00:00
|
|
|
|
import re
|
2025-10-10 12:53:00 +00:00
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from Crypto.Cipher import AES
|
|
|
|
|
from Crypto.Util.Padding import pad
|
2025-07-08 00:58:35 +00:00
|
|
|
|
|
|
|
|
|
from scripts.context import Context
|
|
|
|
|
from scripts.task import Task
|
|
|
|
|
from utils import FileUtils
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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
|
2025-07-08 00:58:35 +00:00
|
|
|
|
|
|
|
|
|
|
2025-07-10 16:10:25 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2025-07-08 00:58:35 +00:00
|
|
|
|
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)
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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 = ""
|
2025-07-08 00:58:35 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2025-07-10 16:10:25 +00:00
|
|
|
|
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
|
|
|
|
|
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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
|
|
|
|
|
|
2025-07-08 00:58:35 +00:00
|
|
|
|
def execute(self):
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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))
|
2025-07-10 16:10:25 +00:00
|
|
|
|
self.write_proguard()
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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")
|
2025-07-08 00:58:35 +00:00
|
|
|
|
|
|
|
|
|
self.context.proguard_dict = {
|
|
|
|
|
k: v for k, v in sorted(
|
|
|
|
|
self.context.proguard_dict.items(),
|
|
|
|
|
key=lambda item: len(item[0]),
|
|
|
|
|
reverse=True
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-07-10 16:10:25 +00:00
|
|
|
|
|
2025-10-10 12:53:00 +00:00
|
|
|
|
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
|