337 lines
12 KiB
Python
337 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):
|
||
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
|