commit 993f1347ddfebdbe632c30b50f0ce03880b7d3b7 Author: zlzw <583819556@qq.com> Date: Mon Jan 5 14:29:08 2026 +0800 初始化项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d3d5ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.apk +/keys* +/__pycache__ +/.trae \ No newline at end of file diff --git a/debug_openssl.py b/debug_openssl.py new file mode 100644 index 0000000..d72cee7 --- /dev/null +++ b/debug_openssl.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +调试脚本:查看OpenSSL命令的输出 +""" + +import subprocess +import os +import tempfile + +# 测试配置 +test_keystore = "keys/key34/keystore.keystore" +test_apk = "keys/key34/app-signed-34.apk" +test_storepass = "password" +test_alias = "alias" +test_keypass = "password" + +def debug_keystore_modulus(): + """调试keystore的模数提取""" + print("=== 调试keystore的模数提取 ===") + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + # 导出证书 + cert_pem_path = os.path.join(temp_dir, 'cert.pem') + export_command = [ + 'keytool', + '-export', + '-keystore', test_keystore, + '-storepass', test_storepass, + '-alias', test_alias, + '-rfc', + '-file', cert_pem_path + ] + + print(f"执行命令: {' '.join(export_command)}") + result = subprocess.run(export_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + if result.returncode == 0: + # 提取公钥 + pubkey_path = os.path.join(temp_dir, 'pubkey.pem') + x509_command = [ + 'openssl', 'x509', + '-in', cert_pem_path, + '-pubkey', + '-noout', + '-out', pubkey_path + ] + + print(f"\n执行命令: {' '.join(x509_command)}") + result = subprocess.run(x509_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + if result.returncode == 0: + # 查看公钥内容 + print(f"\n公钥内容:") + with open(pubkey_path, 'r') as f: + print(f.read()) + + # 尝试使用openssl rsa -modulus + rsa_command = [ + 'openssl', 'rsa', + '-pubin', + '-in', pubkey_path, + '-modulus', + '-noout' + ] + + print(f"\n执行命令: {' '.join(rsa_command)}") + result = subprocess.run(rsa_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + # 尝试使用openssl rsa -text + rsa_command = [ + 'openssl', 'rsa', + '-pubin', + '-in', pubkey_path, + '-text', + '-noout' + ] + + print(f"\n执行命令: {' '.join(rsa_command)}") + result = subprocess.run(rsa_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + +def debug_apk_modulus(): + """调试APK的模数提取""" + print("\n=== 调试APK的模数提取 ===") + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + import zipfile + + # 提取APK中的证书 + with zipfile.ZipFile(test_apk, 'r') as apk: + cert_files = [f for f in apk.namelist() if f.endswith('.RSA') or f.endswith('.DSA') or f.endswith('.EC')] + print(f"找到的证书文件: {cert_files}") + + if cert_files: + cert_file = cert_files[0] + cert_path = os.path.join(temp_dir, 'cert.RSA') + apk.extract(cert_file, temp_dir) + os.rename(os.path.join(temp_dir, cert_file), cert_path) + + # 使用pkcs7提取公钥 + pem_path = os.path.join(temp_dir, 'cert.pem') + pkcs7_command = [ + 'openssl', 'pkcs7', + '-inform', 'DER', + '-in', cert_path, + '-print_certs', + '-out', pem_path + ] + + print(f"执行命令: {' '.join(pkcs7_command)}") + result = subprocess.run(pkcs7_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + if result.returncode == 0: + # 提取公钥 + pubkey_path = os.path.join(temp_dir, 'pubkey.pem') + x509_command = [ + 'openssl', 'x509', + '-in', pem_path, + '-pubkey', + '-noout', + '-out', pubkey_path + ] + + print(f"\n执行命令: {' '.join(x509_command)}") + result = subprocess.run(x509_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + if result.returncode == 0: + # 查看公钥内容 + print(f"\n公钥内容:") + with open(pubkey_path, 'r') as f: + print(f.read()) + + # 尝试使用openssl rsa -modulus + rsa_command = [ + 'openssl', 'rsa', + '-pubin', + '-in', pubkey_path, + '-modulus', + '-noout' + ] + + print(f"\n执行命令: {' '.join(rsa_command)}") + result = subprocess.run(rsa_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + + # 尝试使用openssl rsa -text + rsa_command = [ + 'openssl', 'rsa', + '-pubin', + '-in', pubkey_path, + '-text', + '-noout' + ] + + print(f"\n执行命令: {' '.join(rsa_command)}") + result = subprocess.run(rsa_command, capture_output=True, text=True) + print(f"返回码: {result.returncode}") + print(f"输出: {result.stdout}") + print(f"错误: {result.stderr}") + +if __name__ == "__main__": + debug_keystore_modulus() + debug_apk_modulus() diff --git a/main.py b/main.py new file mode 100644 index 0000000..d3e6e59 --- /dev/null +++ b/main.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""APK签名工具 - 程序入口""" + +import argparse +import sys +from src.batch import batch_generate_keystores +from src.gui import gui_main + +def main(): + """命令行接口""" + parser = argparse.ArgumentParser(description='批量生成Android签名文件工具') + parser.add_argument('--name', required=True, help='签名文件基础名称,可包含多个{random}占位符') + parser.add_argument('--alias', required=True, help='别名,可包含多个{random}占位符') + parser.add_argument('--storepass', required=True, help='存储密码,可包含多个{random}占位符') + parser.add_argument('--keypass', required=True, help='密钥密码,可包含多个{random}占位符') + parser.add_argument('--random-length', type=int, default=4, help='每个随机字符串的长度,默认4') + parser.add_argument('--count', type=int, default=1, help='生成数量,默认1') + parser.add_argument('--validity', type=int, default=10000, help='有效期(天),默认10000') + + # 添加DN字段的命令行参数 + parser.add_argument('--cn', default='Android', help='Common Name,默认Android') + parser.add_argument('--ou', default='Development', help='Organizational Unit,默认Development') + parser.add_argument('--o', default='AndroidDev', help='Organization,默认AndroidDev') + parser.add_argument('--l', default='Unknown', help='Locality,默认Unknown') + parser.add_argument('--st', default='Unknown', help='State,默认Unknown') + parser.add_argument('--c', default='CN', help='Country,默认CN') + + args = parser.parse_args() + + batch_generate_keystores( + base_name=args.name, + base_alias=args.alias, + store_pass=args.storepass, + key_pass=args.keypass, + random_length=args.random_length, + count=args.count, + validity_days=args.validity, + cn=args.cn, + ou=args.ou, + o=args.o, + l=args.l, + st=args.st, + c=args.c + ) + +if __name__ == "__main__": + # 根据命令行参数决定运行模式 + if len(sys.argv) > 1: + # 命令行模式 + main() + else: + # GUI模式 + gui_main() diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..1d7901e --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +/__pycache__ \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apksigner.py b/src/apksigner.py new file mode 100644 index 0000000..8480cea --- /dev/null +++ b/src/apksigner.py @@ -0,0 +1,92 @@ +import subprocess +import os +from .utils import global_debug_enabled + +def get_latest_apksigner_path(): + """获取最新版本的apksigner.bat路径""" + android_sdk_path = "D:/Android/Sdk" + build_tools_path = os.path.join(android_sdk_path, "build-tools") + + if not os.path.exists(build_tools_path): + print(f"错误: 未找到Android SDK build-tools目录: {build_tools_path}") + return None + + # 获取所有build-tools版本目录 + versions = [] + for item in os.listdir(build_tools_path): + item_path = os.path.join(build_tools_path, item) + if os.path.isdir(item_path): + try: + # 尝试将版本号转换为浮点数进行比较 + version_parts = item.split(".") + if len(version_parts) >= 2: + version = float(f"{version_parts[0]}.{version_parts[1]}") + versions.append((version, item)) + except ValueError: + continue + + if not versions: + print("错误: 未找到有效的build-tools版本") + return None + + # 按版本号降序排序,获取最新版本 + versions.sort(reverse=True) + latest_version = versions[0][1] + + apksigner_path = os.path.join(build_tools_path, latest_version, "apksigner.bat") + if not os.path.exists(apksigner_path): + print(f"错误: 未找到apksigner.bat文件: {apksigner_path}") + return None + + return apksigner_path + +def sign_apk(keystore_path, alias, store_pass, key_pass, input_apk, output_apk, signature_versions="v1+v2+v3"): + """使用apksigner对APK文件进行签名,支持v1、v2和v3签名""" + try: + # 检查keystore文件是否存在 + if not os.path.exists(keystore_path): + print(f"错误: 签名文件不存在: {keystore_path}") + return False + + # 检查输入APK文件是否存在 + if not os.path.exists(input_apk): + print(f"错误: 输入APK文件不存在: {input_apk}") + return False + + # 获取最新版本的apksigner.bat路径 + apksigner_path = get_latest_apksigner_path() + if not apksigner_path: + return False + + # 执行apksigner命令进行签名 + command = [ + apksigner_path, + "sign", + "--ks", keystore_path, + "--ks-key-alias", alias, + "--ks-pass", f"pass:{store_pass}", + "--key-pass", f"pass:{key_pass}", + "--out", output_apk, + "--v1-signing-enabled", "true", + "--v2-signing-enabled", "true", + "--v3-signing-enabled", "true", + input_apk + ] + + result = subprocess.run(command, capture_output=True, text=True, encoding="utf-8", errors="replace") + if result.returncode == 0: + print(f"成功签名APK文件: {output_apk}") + print(f"使用签名版本: {signature_versions}") + return True + else: + print(f"签名APK文件失败: {result.stderr}") + return False + except subprocess.CalledProcessError as e: + print(f"签名APK文件失败: {e.stderr}") + return False + except FileNotFoundError: + print("错误: 未找到apksigner。请确保Android SDK已正确安装。") + return False + except Exception as e: + print(f"签名APK文件异常: {str(e)}") + return False diff --git a/src/batch.py b/src/batch.py new file mode 100644 index 0000000..31d949d --- /dev/null +++ b/src/batch.py @@ -0,0 +1,132 @@ +import os +import time +import string +from .utils import generate_random_string, replace_placeholders, global_debug_enabled +from .keystore import generate_keystore, create_key_info_file +from .apksigner import sign_apk +from .signature import get_signature_info + +def batch_generate_keystores(base_name, base_alias, store_pass, key_pass, + random_length, count, validity_days=10000, + cn="Android", ou="Development", o="AndroidDev", + l="Unknown", st="Unknown", c="CN"): + """批量生成签名文件""" + out_dir = "keys" + os.makedirs(out_dir, exist_ok=True) + + # 检查app-release.apk是否存在 + apk_path = os.path.join(os.getcwd(), "app-release.apk") + if not os.path.exists(apk_path): + print(f"错误: 未找到app-release.apk文件,路径: {apk_path}") + return + + # 为alias定义严格的字符集(仅小写字母+数字) + alias_chars = string.ascii_lowercase + string.digits + + # 获取当前已存在的key目录数量 + existing_dirs = [d for d in os.listdir(out_dir) if os.path.isdir(os.path.join(out_dir, d)) and d.startswith("key")] + existing_count = len(existing_dirs) + + for i in range(count): + # 普通字段使用默认字符集,alias使用严格字符集 + keystore_name = replace_placeholders(base_name, random_length) + alias = replace_placeholders(base_alias, random_length, alias_chars) # 关键修改 + processed_store_pass = replace_placeholders(store_pass, random_length) + processed_key_pass = replace_placeholders(key_pass, random_length) + + # 根据已存在目录数量确定新目录名 + dir_index = existing_count + i + 1 + dir_name = os.path.join(out_dir, f"key{dir_index}") + os.makedirs(dir_name, exist_ok=True) + + keystore_path = os.path.join(dir_name, f"{keystore_name}.keystore") + output_apk_path = os.path.join(dir_name, f"app-signed-{dir_index}.apk") + + # 传递DN参数给generate_keystore函数 + generate_keystore(keystore_path, alias, processed_store_pass, processed_key_pass, validity_days, + cn, ou, o, l, st, c) + + # 等待文件完全写入磁盘 + time.sleep(0.5) + + # 检查keystore文件是否存在 + if not os.path.exists(keystore_path): + print(f"\n错误:keystore文件 {keystore_path} 未生成") + continue + + # 使用生成的签名文件对APK进行签名 + print(f"\n正在签名APK文件...") + sign_success = sign_apk(keystore_path, alias, processed_store_pass, processed_key_pass, apk_path, output_apk_path) + + # 等待APK签名完成 + time.sleep(0.5) + + # 只有签名成功后,才获取签名信息 + if sign_success and os.path.exists(output_apk_path): + # 先获取keystore的签名信息(用于显示) + print(f"\n正在获取keystore {keystore_name}.keystore 的签名信息...") + keystore_sig_info = get_signature_info(keystore_path, processed_store_pass, alias, processed_key_pass) + + if keystore_sig_info: + print("keystore签名信息:") + print("=" * 50) + if 'md5' in keystore_sig_info: + print(f"MD5: {keystore_sig_info['md5']}") + else: + print("MD5: 从keystore获取失败") + if 'sha1' in keystore_sig_info: + print(f"SHA-1: {keystore_sig_info['sha1']}") + if 'sha256' in keystore_sig_info: + print(f"SHA-256: {keystore_sig_info['sha256']}") + print("=" * 50) + + # 输出RSA公钥信息 + if any(key in keystore_sig_info for key in ['modulus', 'exponent', 'key_type']): + print("\nRSA公钥信息:") + print("=" * 50) + if 'key_type' in keystore_sig_info: + print(f"公钥类型: {keystore_sig_info['key_type']}") + if 'exponent' in keystore_sig_info: + print(f"指数: {keystore_sig_info['exponent']}") + if 'modulus_bits' in keystore_sig_info: + print(f"模数大小(位): {keystore_sig_info['modulus_bits']}") + if 'modulus' in keystore_sig_info: + print(f"模数: {keystore_sig_info['modulus']}") + print("=" * 50) + + # 获取并输出APK的签名信息(APK的签名信息更可靠,包含MD5) + print(f"\n正在获取APK app-signed-{dir_index}.apk 的签名信息...") + apk_sig_info = get_signature_info(output_apk_path) + + if apk_sig_info: + print("APK签名信息:") + print("=" * 50) + if 'md5' in apk_sig_info: + print(f"MD5: {apk_sig_info['md5']}") + if 'sha1' in apk_sig_info: + print(f"SHA-1: {apk_sig_info['sha1']}") + if 'sha256' in apk_sig_info: + print(f"SHA-256: {apk_sig_info['sha256']}") + print("=" * 50) + + # 输出RSA公钥信息 + if any(key in apk_sig_info for key in ['modulus', 'exponent', 'key_type']): + print("\nRSA公钥信息:") + print("=" * 50) + if 'key_type' in apk_sig_info: + print(f"公钥类型: {apk_sig_info['key_type']}") + if 'exponent' in apk_sig_info: + print(f"指数: {apk_sig_info['exponent']}") + if 'modulus_bits' in apk_sig_info: + print(f"模数大小(位): {apk_sig_info['modulus_bits']}") + if 'modulus' in apk_sig_info: + print(f"模数: {apk_sig_info['modulus']}") + print("=" * 50) + + # 创建信息文件,使用APK的签名信息(包含MD5) + print(f"\n正在创建信息文件...") + create_key_info_file(dir_name, alias, processed_store_pass, processed_key_pass, f"{keystore_name}.keystore", apk_sig_info) + else: + print(f"\n警告:APK签名失败或文件未生成") + # 即使APK签名失败,也要创建基本的key.txt文件 + create_key_info_file(dir_name, alias, processed_store_pass, processed_key_pass, f"{keystore_name}.keystore") diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..fbe7ea9 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,624 @@ +import tkinter as tk +from tkinter import ttk, scrolledtext, messagebox +import os +import sys +import threading +from .signature import get_signature_info +from .batch import batch_generate_keystores +from .utils import global_debug_enabled + + +def view_signature_info_gui(): + """GUI界面:查看签名信息""" + # 创建子窗口 + info_window = tk.Toplevel() + info_window.title("查看签名信息") + info_window.geometry("600x600") + + # 创建主容器 + main_frame = ttk.Frame(info_window, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # 文件路径输入 + file_frame = ttk.Frame(main_frame) + file_frame.pack(fill=tk.X, pady=5) + + ttk.Label(file_frame, text="文件路径:").pack(side=tk.LEFT, padx=5) + file_path_var = tk.StringVar() + ttk.Entry(file_frame, textvariable=file_path_var, width=50).pack(side=tk.LEFT, padx=5) + + def browse_file(): + """浏览文件""" + from tkinter import filedialog + file_types = [ + ("所有支持的文件", "*.keystore *.jks *.apk"), + ("Keystore文件", "*.keystore *.jks"), + ("APK文件", "*.apk") + ] + file_path = filedialog.askopenfilename(filetypes=file_types) + if file_path: + file_path_var.set(file_path) + + # 获取文件所在目录 + file_dir = os.path.dirname(file_path) + # 检查是否存在key.txt文件 + key_txt_path = os.path.join(file_dir, "key.txt") + if os.path.exists(key_txt_path): + try: + # 读取key.txt文件 + with open(key_txt_path, 'r', encoding='utf-8') as f: + content = f.readlines() + + # 解析key.txt内容 + alias_value = "" + storepass_value = "" + keypass_value = "" + + for line in content: + line = line.strip() + if line.startswith("别名:"): + alias_value = line.split(":", 1)[1].strip() + elif line.startswith("存储密码:"): + storepass_value = line.split(":", 1)[1].strip() + elif line.startswith("密钥密码:"): + keypass_value = line.split(":", 1)[1].strip() + + # 填充到输入框 + if alias_value: + alias_var.set(alias_value) + if storepass_value: + storepass_var.set(storepass_value) + if keypass_value: + keypass_var.set(keypass_value) + + info_text.insert(tk.END, f"已自动读取文件 {key_txt_path} 的密码信息\n\n") + info_window.update_idletasks() + + except Exception as e: + info_text.insert(tk.END, f"读取key.txt文件失败: {str(e)}\n\n") + info_window.update_idletasks() + + ttk.Button(file_frame, text="浏览", command=browse_file).pack(side=tk.LEFT, padx=5) + + # 密钥库密码(仅keystore文件需要) + keystore_frame = ttk.LabelFrame(main_frame, text="密钥库信息(仅keystore文件需要)", padding="5") + keystore_frame.pack(fill=tk.X, pady=5) + + # 存储密码 + ttk.Label(keystore_frame, text="存储密码:").grid(row=0, column=0, sticky=tk.W, pady=2, padx=5) + storepass_var = tk.StringVar() + ttk.Entry(keystore_frame, textvariable=storepass_var, show="*").grid(row=0, column=1, sticky=tk.EW, pady=2, padx=5) + + # 别名 + ttk.Label(keystore_frame, text="别名:").grid(row=1, column=0, sticky=tk.W, pady=2, padx=5) + alias_var = tk.StringVar() + ttk.Entry(keystore_frame, textvariable=alias_var).grid(row=1, column=1, sticky=tk.EW, pady=2, padx=5) + + # 密钥密码 + ttk.Label(keystore_frame, text="密钥密码:").grid(row=2, column=0, sticky=tk.W, pady=2, padx=5) + keypass_var = tk.StringVar() + ttk.Entry(keystore_frame, textvariable=keypass_var, show="*").grid(row=2, column=1, sticky=tk.EW, pady=2, padx=5) + + # 输出区域 + output_frame = ttk.LabelFrame(main_frame, text="签名信息", padding="5") + output_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + info_text = scrolledtext.ScrolledText(output_frame, width=70, height=15, wrap=tk.WORD) + info_text.pack(fill=tk.BOTH, expand=True) + + def get_info(): + """获取签名信息""" + info_text.delete(1.0, tk.END) + + file_path = file_path_var.get() + if not file_path: + messagebox.showerror("错误", "请选择文件") + return + + if not os.path.exists(file_path): + messagebox.showerror("错误", "文件不存在") + return + + storepass = storepass_var.get() + alias = alias_var.get() + keypass = keypass_var.get() + + try: + # 处理APK文件的情况,直接获取签名信息 + if file_path.endswith('.apk'): + info_text.insert(tk.END, f"正在获取APK文件 {file_path} 的签名信息...\n\n") + info_window.update_idletasks() + + signature_info = get_signature_info(file_path) + + if signature_info: + info_text.insert(tk.END, "签名信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'md5' in signature_info: + info_text.insert(tk.END, f"MD5: {signature_info['md5']}\n") + else: + info_text.insert(tk.END, "MD5: 无法获取\n") + + if 'sha1' in signature_info: + info_text.insert(tk.END, f"SHA-1: {signature_info['sha1']}\n") + + if 'sha256' in signature_info: + info_text.insert(tk.END, f"SHA-256: {signature_info['sha256']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + + # 显示RSA模数信息 + if any(key in signature_info for key in ['modulus', 'exponent', 'key_type']): + info_text.insert(tk.END, "\nRSA公钥信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'key_type' in signature_info: + info_text.insert(tk.END, f"公钥类型: {signature_info['key_type']}\n") + if 'exponent' in signature_info: + info_text.insert(tk.END, f"指数: {signature_info['exponent']}\n") + if 'modulus_bits' in signature_info: + info_text.insert(tk.END, f"模数大小(位): {signature_info['modulus_bits']}\n") + if 'modulus' in signature_info: + info_text.insert(tk.END, f"模数: {signature_info['modulus']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + else: + info_text.insert(tk.END, "未获取到签名信息\n") + # 处理keystore文件的情况 + elif file_path.endswith('.keystore') or file_path.endswith('.jks'): + info_text.insert(tk.END, f"正在处理签名文件 {file_path}...\n\n") + info_window.update_idletasks() + + # 获取签名文件所在目录 + file_dir = os.path.dirname(file_path) + file_name = os.path.basename(file_path) + + # 检查当前文件夹内是否有APK文件 + apk_files = [] + for f in os.listdir(file_dir): + if f.endswith('.apk'): + apk_files.append(os.path.join(file_dir, f)) + + # 选择第一个APK文件 + target_apk_path = None + if apk_files: + target_apk_path = apk_files[0] + info_text.insert(tk.END, f"发现APK文件 {target_apk_path},将直接使用该文件获取签名信息...\n\n") + info_window.update_idletasks() + else: + info_text.insert(tk.END, f"未发现APK文件,将使用基础APK文件进行签名...\n\n") + info_window.update_idletasks() + + # 检查基础APK文件是否存在 + base_apk_path = os.path.join(os.getcwd(), "app-release.apk") + if not os.path.exists(base_apk_path): + info_text.insert(tk.END, f"错误:基础APK文件 {base_apk_path} 不存在\n\n") + messagebox.showerror("错误", f"基础APK文件 {base_apk_path} 不存在") + return + + # 生成输出APK文件路径 + apk_name = f"app-signed-{os.path.splitext(file_name)[0]}.apk" + target_apk_path = os.path.join(file_dir, apk_name) + + # 导入sign_apk函数 + from .apksigner import sign_apk + + # 使用签名文件对基础APK进行签名 + info_text.insert(tk.END, f"正在使用 {file_name} 对基础APK进行签名...\n") + info_text.insert(tk.END, f"签名后的APK将保存为 {target_apk_path}\n\n") + info_window.update_idletasks() + + # 执行签名 + sign_success = sign_apk( + keystore_path=file_path, + alias=alias, + store_pass=storepass, + key_pass=keypass, + input_apk=base_apk_path, + output_apk=target_apk_path + ) + + if not sign_success: + info_text.insert(tk.END, "APK签名失败,无法获取签名信息\n") + messagebox.showerror("错误", "APK签名失败,无法获取签名信息") + return + + info_text.insert(tk.END, "APK签名成功!\n\n") + info_window.update_idletasks() + + # 使用target_apk_path获取签名信息 + info_text.insert(tk.END, f"正在获取APK文件 {os.path.basename(target_apk_path)} 的签名信息...\n\n") + info_window.update_idletasks() + + signature_info = get_signature_info(target_apk_path) + + if signature_info: + info_text.insert(tk.END, "签名信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'md5' in signature_info: + info_text.insert(tk.END, f"MD5: {signature_info['md5']}\n") + else: + info_text.insert(tk.END, "MD5: 无法获取\n") + + if 'sha1' in signature_info: + info_text.insert(tk.END, f"SHA-1: {signature_info['sha1']}\n") + + if 'sha256' in signature_info: + info_text.insert(tk.END, f"SHA-256: {signature_info['sha256']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + + # 显示RSA模数信息 + if any(key in signature_info for key in ['modulus', 'exponent', 'key_type']): + info_text.insert(tk.END, "\nRSA公钥信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'key_type' in signature_info: + info_text.insert(tk.END, f"公钥类型: {signature_info['key_type']}\n") + if 'exponent' in signature_info: + info_text.insert(tk.END, f"指数: {signature_info['exponent']}\n") + if 'modulus_bits' in signature_info: + info_text.insert(tk.END, f"模数大小(位): {signature_info['modulus_bits']}\n") + if 'modulus' in signature_info: + info_text.insert(tk.END, f"模数: {signature_info['modulus']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + else: + info_text.insert(tk.END, "未获取到签名信息\n") + # 其他文件类型,直接获取签名信息 + else: + info_text.insert(tk.END, f"正在获取文件 {file_path} 的签名信息...\n\n") + info_window.update_idletasks() + + signature_info = get_signature_info(file_path, storepass, alias, keypass) + + if signature_info: + info_text.insert(tk.END, "签名信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'md5' in signature_info: + info_text.insert(tk.END, f"MD5: {signature_info['md5']}\n") + else: + info_text.insert(tk.END, "MD5: 无法获取(高版本JDK可能不支持)\n") + + if 'sha1' in signature_info: + info_text.insert(tk.END, f"SHA-1: {signature_info['sha1']}\n") + + if 'sha256' in signature_info: + info_text.insert(tk.END, f"SHA-256: {signature_info['sha256']}\n") + + if 'valid_from' in signature_info and 'valid_to' in signature_info: + info_text.insert(tk.END, f"有效期: {signature_info['valid_from']} 至 {signature_info['valid_to']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + + # 显示RSA模数信息 + if any(key in signature_info for key in ['modulus', 'exponent', 'key_type']): + info_text.insert(tk.END, "\nRSA公钥信息:\n") + info_text.insert(tk.END, "=" * 50 + "\n") + + if 'key_type' in signature_info: + info_text.insert(tk.END, f"公钥类型: {signature_info['key_type']}\n") + if 'exponent' in signature_info: + info_text.insert(tk.END, f"指数: {signature_info['exponent']}\n") + if 'modulus_bits' in signature_info: + info_text.insert(tk.END, f"模数大小(位): {signature_info['modulus_bits']}\n") + if 'modulus' in signature_info: + info_text.insert(tk.END, f"模数: {signature_info['modulus']}\n") + + info_text.insert(tk.END, "=" * 50 + "\n") + else: + info_text.insert(tk.END, "未获取到签名信息\n") + + except Exception as e: + info_text.insert(tk.END, f"获取信息失败: {str(e)}\n") + import traceback + traceback.print_exc() + + # 操作按钮 + button_frame = ttk.Frame(main_frame, padding="10") + button_frame.pack(fill=tk.X, pady=5) + + ttk.Button(button_frame, text="获取签名信息", command=get_info).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="关闭", command=info_window.destroy).pack(side=tk.RIGHT, padx=5) + + +def gui_main(): + """GUI主函数,创建并显示GUI界面""" + # 创建主窗口 + root = tk.Tk() + root.title("APK签名工具") + root.geometry("800x700") + root.resizable(True, True) + + # 设置全局字体 + default_font = ("微软雅黑", 10) + root.option_add("*Font", default_font) + + # 创建主容器 + main_frame = ttk.Frame(root, padding="10") + main_frame.pack(fill=tk.BOTH, expand=True) + + # 线程状态变量 + generating = False # 生成状态标志,防止重复点击 + generate_thread = None # 生成线程对象 + + # 创建输入区域 + input_frame = ttk.LabelFrame(main_frame, text="输入配置", padding="10") + input_frame.pack(fill=tk.X, pady=5) + + # 行计数器 + row = 0 + + # 1. 签名文件基础名称 + ttk.Label(input_frame, text="签名文件基础名称:").grid(row=row, column=0, sticky=tk.W, pady=2) + name_var = tk.StringVar(value="keystore") + name_entry = ttk.Entry(input_frame, textvariable=name_var, width=50) + name_entry.grid(row=row, column=1, sticky=tk.EW, pady=2, padx=5) + add_random_btn3 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(name_entry)) + add_random_btn3.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 2. 别名 + ttk.Label(input_frame, text="别名:").grid(row=row, column=0, sticky=tk.W, pady=2) + alias_var = tk.StringVar(value="alias") + alias_entry = ttk.Entry(input_frame, textvariable=alias_var, width=50) + alias_entry.grid(row=row, column=1, sticky=tk.EW, pady=2, padx=5) + add_random_btn4 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(alias_entry)) + add_random_btn4.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 3. 存储密码 + ttk.Label(input_frame, text="存储密码:").grid(row=row, column=0, sticky=tk.W, pady=2) + storepass_var = tk.StringVar(value="password") + storepass_entry = ttk.Entry(input_frame, textvariable=storepass_var, width=50) + storepass_entry.grid(row=row, column=1, sticky=tk.EW, pady=2, padx=5) + add_random_btn5 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(storepass_entry)) + add_random_btn5.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 4. 密钥密码 + ttk.Label(input_frame, text="密钥密码:").grid(row=row, column=0, sticky=tk.W, pady=2) + keypass_var = tk.StringVar(value="password") + keypass_entry = ttk.Entry(input_frame, textvariable=keypass_var, width=50) + keypass_entry.grid(row=row, column=1, sticky=tk.EW, pady=2, padx=5) + add_random_btn6 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(keypass_entry)) + add_random_btn6.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 数字输入验证函数 + def validate_digit(action, value_if_allowed): + """验证输入是否为数字""" + if action == '1': # 插入字符 + if not value_if_allowed.isdigit(): + return False + if value_if_allowed and int(value_if_allowed) > 20: + return False + return True + + validate_cmd = root.register(validate_digit) + + # 添加随机符函数 + def add_random_placeholder(entry_widget): + """在输入框光标位置添加{random}占位符""" + current_text = entry_widget.get() + cursor_pos = entry_widget.index(tk.INSERT) + new_text = current_text[:cursor_pos] + "{random}" + current_text[cursor_pos:] + entry_widget.delete(0, tk.END) + entry_widget.insert(0, new_text) + entry_widget.focus() + + # 5. 随机字符串长度 + ttk.Label(input_frame, text="随机字符串长度:").grid(row=row, column=0, sticky=tk.W, pady=2) + random_length_var = tk.StringVar(value="4") + random_length_entry = ttk.Entry(input_frame, textvariable=random_length_var, validate="key", validatecommand=(validate_cmd, '%d', '%P'), width=10) + random_length_entry.grid(row=row, column=1, sticky=tk.W, pady=2, padx=5) + add_random_btn1 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(random_length_entry)) + add_random_btn1.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 6. 生成数量 + ttk.Label(input_frame, text="生成数量:").grid(row=row, column=0, sticky=tk.W, pady=2) + count_var = tk.StringVar(value="1") + count_entry = ttk.Entry(input_frame, textvariable=count_var, validate="key", validatecommand=(validate_cmd, '%d', '%P'), width=10) + count_entry.grid(row=row, column=1, sticky=tk.W, pady=2, padx=5) + add_random_btn2 = ttk.Button(input_frame, text="添加随机符", command=lambda: add_random_placeholder(count_entry)) + add_random_btn2.grid(row=row, column=2, sticky=tk.W, pady=2, padx=5) + row += 1 + + # 7. 有效期 + ttk.Label(input_frame, text="有效期(天):").grid(row=row, column=0, sticky=tk.W, pady=2) + validity_var = tk.IntVar(value=10000) + ttk.Entry(input_frame, textvariable=validity_var, width=20).grid(row=row, column=1, sticky=tk.W, pady=2) + row += 1 + + # 分隔线 + ttk.Separator(input_frame, orient=tk.HORIZONTAL).grid(row=row, column=0, columnspan=3, sticky=tk.EW, pady=5) + row += 1 + + # DN信息区域 + dn_frame = ttk.LabelFrame(input_frame, text="DN信息(可选)", padding="5") + dn_frame.grid(row=row, column=0, columnspan=3, sticky=tk.EW, pady=5) + + # DN字段行计数器 + dn_row = 0 + + # CN (Common Name) + ttk.Label(dn_frame, text="CN (Common Name):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + cn_var = tk.StringVar(value="Android") + ttk.Entry(dn_frame, textvariable=cn_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + # OU (Organizational Unit) + ttk.Label(dn_frame, text="OU (Org Unit):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + ou_var = tk.StringVar(value="Development") + ttk.Entry(dn_frame, textvariable=ou_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + # O (Organization) + ttk.Label(dn_frame, text="O (Organization):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + o_var = tk.StringVar(value="AndroidDev") + ttk.Entry(dn_frame, textvariable=o_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + # L (Locality) + ttk.Label(dn_frame, text="L (Locality):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + l_var = tk.StringVar(value="Unknown") + ttk.Entry(dn_frame, textvariable=l_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + # ST (State) + ttk.Label(dn_frame, text="ST (State):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + st_var = tk.StringVar(value="Unknown") + ttk.Entry(dn_frame, textvariable=st_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + # C (Country) + ttk.Label(dn_frame, text="C (Country):").grid(row=dn_row, column=0, sticky=tk.W, pady=2, padx=5) + c_var = tk.StringVar(value="CN") + ttk.Entry(dn_frame, textvariable=c_var, width=40).grid(row=dn_row, column=1, sticky=tk.EW, pady=2, padx=5) + dn_row += 1 + + row += 1 + + # 创建日志区域 + log_frame = ttk.LabelFrame(main_frame, text="运行日志", padding="10") + log_frame.pack(fill=tk.BOTH, expand=True, pady=5) + + log_text = scrolledtext.ScrolledText(log_frame, width=80, height=0, wrap=tk.WORD) + log_text.pack(fill=tk.BOTH, expand=True) + + # 日志重定向类 + class TextRedirector(object): + def __init__(self, widget): + self.widget = widget + + def write(self, string): + self.widget.insert(tk.END, string) + self.widget.see(tk.END) # 自动滚动到底部 + + def flush(self): + pass # 刷新方法,确保输出立即显示 + + # 将stdout和stderr重定向到日志区 + sys.stdout = TextRedirector(log_text) + sys.stderr = TextRedirector(log_text) + + # 创建操作按钮区域 + button_frame = ttk.Frame(main_frame, padding="10") + button_frame.pack(fill=tk.X, pady=5) + + # 定义debug开关变量 + debug_var = tk.BooleanVar(value=False) + + # Debug开关 + debug_checkbox = ttk.Checkbutton(button_frame, text="启用Debug模式", variable=debug_var, command=lambda: toggle_debug()) + debug_checkbox.pack(side=tk.LEFT, padx=5) + + def toggle_debug(): + """切换debug模式""" + from .utils import global_debug_enabled as gde + global global_debug_enabled + global_debug_enabled = debug_var.get() + if global_debug_enabled: + print("Debug模式已启用") + else: + print("Debug模式已禁用") + + def generate_in_thread(name, alias, storepass, keypass, random_length, count, validity, cn, ou, o, l, st, c): + """在后台线程中执行签名生成操作""" + nonlocal generating + try: + # 调用批量生成函数 + batch_generate_keystores( + base_name=name, + base_alias=alias, + store_pass=storepass, + key_pass=keypass, + random_length=random_length, + count=count, + validity_days=validity, + cn=cn, + ou=ou, + o=o, + l=l, + st=st, + c=c + ) + + # 生成完成,恢复UI状态 + generating = False + generate_btn.config(state=tk.NORMAL) + + # 显示成功消息 + root.after(0, lambda: messagebox.showinfo("成功", f"已成功生成 {count} 个签名文件")) + except Exception as e: + # 处理异常 + generating = False + generate_btn.config(state=tk.NORMAL) + root.after(0, lambda: messagebox.showerror("错误", f"生成失败: {str(e)}")) + + def generate(): + """生成签名文件,创建后台线程执行耗时操作""" + nonlocal generating, generate_thread + + # 检查是否正在生成 + if generating: + messagebox.showinfo("提示", "正在生成签名文件,请稍后...") + return + + try: + # 获取输入值 + name = name_var.get() + alias = alias_var.get() + storepass = storepass_var.get() + keypass = keypass_var.get() + random_length = int(random_length_var.get()) + count = int(count_var.get()) + validity = validity_var.get() + + # 获取DN信息 + cn = cn_var.get() + ou = ou_var.get() + o = o_var.get() + l = l_var.get() + st = st_var.get() + c = c_var.get() + + if not name or not alias or not storepass or not keypass: + messagebox.showerror("错误", "请填写所有必填字段") + return + + # 设置生成状态,禁用生成按钮 + generating = True + generate_btn.config(state=tk.DISABLED) + + # 创建并启动后台线程 + generate_thread = threading.Thread( + target=generate_in_thread, + args=(name, alias, storepass, keypass, random_length, count, validity, cn, ou, o, l, st, c), + daemon=True + ) + generate_thread.start() + + # 显示提示信息 + log_text.insert(tk.END, "\n开始生成签名文件...\n") + log_text.see(tk.END) + + except Exception as e: + # 恢复UI状态 + generating = False + generate_btn.config(state=tk.NORMAL) + messagebox.showerror("错误", f"生成失败: {str(e)}") + + # 生成按钮 + generate_btn = ttk.Button(button_frame, text="生成签名文件", command=generate) + generate_btn.pack(side=tk.RIGHT, padx=5) + + # 查看签名信息按钮 + view_info_btn = ttk.Button(button_frame, text="查看签名信息", command=view_signature_info_gui) + view_info_btn.pack(side=tk.RIGHT, padx=5) + + root.mainloop() diff --git a/src/keystore.py b/src/keystore.py new file mode 100644 index 0000000..07e9680 --- /dev/null +++ b/src/keystore.py @@ -0,0 +1,101 @@ +import subprocess +import os +from .utils import global_debug_enabled + +def format_signature(signature_value): + """格式化签名信息值,确保大写且每两个字符之间用冒号分隔 + + Args: + signature_value: 原始签名信息值 + + Returns: + str: 格式化后的签名信息值 + """ + if not signature_value: + return signature_value + + # 移除所有非十六进制字符 + clean_value = ''.join(c for c in signature_value if c.isalnum()) + + # 转换为大写 + clean_value = clean_value.upper() + + # 每两个字符添加一个冒号 + formatted = ':'.join(clean_value[i:i+2] for i in range(0, len(clean_value), 2)) + + return formatted + +def generate_keystore(keystore_path, alias, store_pass, key_pass, validity_days=10000, + cn="Android", ou="Development", o="AndroidDev", + l="Unknown", st="Unknown", c="CN"): + """使用keytool生成Android签名文件""" + try: + # 构建DN字符串 + dname = f"CN={cn}, OU={ou}, O={o}, L={l}, ST={st}, C={c}" + + command = [ + "keytool", + "-genkey", + "-v", + "-keystore", keystore_path, + "-alias", alias, + "-keyalg", "RSA", + "-keysize", "2048", + "-validity", str(validity_days), + "-storepass", store_pass, + "-keypass", key_pass, + "-dname", dname + ] + + subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + print(f"成功生成签名文件: {keystore_path}") + return True + except subprocess.CalledProcessError as e: + print(f"生成签名文件失败: {e.stderr}") + return False + except FileNotFoundError: + print("错误: 未找到keytool。请确保Java开发环境已正确安装并配置到环境变量中。") + return False + +def create_key_info_file(directory, alias, store_pass, key_pass, keystore_name, signature_info=None): + """创建包含签名信息的key.txt文件""" + file_path = os.path.join(directory, "key.txt") + try: + with open(file_path, 'w', encoding='utf-8') as f: + f.write(f"签名文件名: {keystore_name}\n") + f.write(f"别名: {alias}\n") + f.write(f"存储密码: {store_pass}\n") + f.write(f"密钥密码: {key_pass}\n") + + # 如果提供了签名信息,添加到文件中 + if signature_info: + f.write("\n签名信息:\n") + f.write("=" * 50 + "\n") + if 'md5' in signature_info: + f.write(f"MD5: {format_signature(signature_info['md5'])}\n") + if 'sha1' in signature_info: + f.write(f"SHA-1: {format_signature(signature_info['sha1'])}\n") + if 'sha256' in signature_info: + f.write(f"SHA-256: {format_signature(signature_info['sha256'])}\n") + if 'valid_from' in signature_info and 'valid_to' in signature_info: + f.write(f"有效期: {signature_info['valid_from']} 至 {signature_info['valid_to']}\n") + f.write("=" * 50 + "\n") + + # 添加RSA模数信息 + if signature_info and any(key in signature_info for key in ['modulus', 'exponent', 'key_type']): + f.write("\nRSA公钥信息:\n") + f.write("=" * 50 + "\n") + if 'key_type' in signature_info: + f.write(f"公钥类型: {signature_info['key_type']}\n") + if 'exponent' in signature_info: + f.write(f"指数: {signature_info['exponent']}\n") + if 'modulus_bits' in signature_info: + f.write(f"模数大小(位): {signature_info['modulus_bits']}\n") + if 'modulus' in signature_info: + f.write(f"模数: {signature_info['modulus']}\n") + f.write("=" * 50 + "\n") + print(f"已创建信息文件: {file_path}") + return True + except Exception as e: + print(f"创建信息文件失败: {e}") + return False diff --git a/src/signature.py b/src/signature.py new file mode 100644 index 0000000..47eb079 --- /dev/null +++ b/src/signature.py @@ -0,0 +1,519 @@ +import subprocess +import os +import re +import hashlib +import tempfile +import zipfile +import src.utils +from .apksigner import get_latest_apksigner_path + +# 获取全局debug变量 + +def is_debug_enabled(): + """检查是否启用了debug模式""" + return src.utils.global_debug_enabled + +def get_rsa_modulus(file_path, keystore_pass=None, alias=None, key_pass=None): + """获取RSA公钥模数信息,支持keystore文件和已签名APK文件 + 使用keytool和Java命令提取真实的RSA模数 + + Args: + file_path: 文件路径(keystore或APK) + keystore_pass: keystore密码(仅keystore文件需要) + alias: 别名(仅keystore文件需要) + key_pass: 密钥密码(仅keystore文件需要) + + Returns: + dict: 包含RSA模数信息的字典,包括modulus、exponent、key_type等 + """ + rsa_info = { + 'key_type': 'RSA', + 'exponent': 65537, + 'modulus_bits': 2048 + } + + if is_debug_enabled(): + print(f"[DEBUG] 开始获取 {file_path} 的RSA模数") + + # Check if file exists + if not os.path.exists(file_path): + if is_debug_enabled(): + print(f"[ERROR] 文件 {file_path} 不存在") + return rsa_info + + temp_files = [] + temp_dirs = [] + + try: + # Check if Java is available + java_check_cmd = ['java', '-version'] + java_check_result = subprocess.run( + java_check_cmd, + capture_output=True, + timeout=2, + shell=False + ) + + if java_check_result.returncode != 0: + if is_debug_enabled(): + print(f"[ERROR] Java不可用,无法提取RSA模数") + return rsa_info + + # Generate Java code for extracting RSA modulus + java_code = """ +import java.io.*; +import java.security.*; +import java.security.cert.*; +import java.security.interfaces.RSAPublicKey; +import java.util.*; +import java.util.regex.*; +import java.util.zip.*; +import java.math.BigInteger; + +public class ExtractRsaModulus { + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("Usage: java ExtractRsaModulus [keystore_pass] [alias] [key_pass]"); + System.exit(1); + } + + String filePath = args[0]; + String keystorePass = args.length > 1 ? args[1] : ""; + String alias = args.length > 2 ? args[2] : ""; + String keyPass = args.length > 3 ? args[3] : ""; + + try { + if (filePath.endsWith(".keystore") || filePath.endsWith(".jks")) { + extractFromKeystore(filePath, keystorePass, alias, keyPass); + } else if (filePath.endsWith(".apk")) { + extractFromApk(filePath); + } else { + System.err.println("Unsupported file type: " + filePath); + System.exit(1); + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + + private static void extractFromApk(String apkPath) throws Exception { + // Extract real RSA modulus from APK signature + // First try to extract from META-INF directory (v1 signature) + boolean found = false; + + try (ZipFile zipFile = new ZipFile(apkPath)) { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".RSA") || entry.getName().endsWith(".DSA") || entry.getName().endsWith(".EC")) { + // Found signature file, extract certificate + try (InputStream is = zipFile.getInputStream(entry)) { + // Read certificate data + byte[] certData = is.readAllBytes(); + + // Extract certificates from signature file + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream bais = new ByteArrayInputStream(certData); + Collection certs = cf.generateCertificates(bais); + + if (!certs.isEmpty()) { + // Get first certificate + java.security.cert.Certificate cert = certs.iterator().next(); + PublicKey publicKey = cert.getPublicKey(); + + if (publicKey instanceof RSAPublicKey) { + RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; + System.out.println("MODULUS: " + rsaPublicKey.getModulus()); + System.out.println("EXPONENT: " + rsaPublicKey.getPublicExponent()); + System.out.println("MODULUS_BITS: " + rsaPublicKey.getModulus().bitLength()); + found = true; + break; + } + } + } + } + } + } + + if (!found) { + // If no v1 signature found, use apksigner to extract v2 signature information + System.err.println("No v1 signature found, trying v2 signature extraction"); + + // Use apksigner to get certificate information + ProcessBuilder pb = new ProcessBuilder( + "D:/Android/Sdk/build-tools/36.1.0/apksigner.bat", + "verify", + "--verbose", + "--print-certs", + apkPath + ); + pb.redirectErrorStream(true); + Process process = pb.start(); + + StringBuilder output = new StringBuilder(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + output.append(line).append(System.lineSeparator()); + } + } + + int exitCode = process.waitFor(); + if (exitCode != 0) { + System.err.println("apksigner failed with exit code: " + exitCode); + System.err.println("Output: " + output.toString()); + System.exit(1); + } + + String certOutput = output.toString(); + + // Extract modulus from apksigner output if available + // Note: apksigner doesn't directly output modulus, but we can use the certificate data + + // For v2 signatures, we need a different approach, but this is complex + // As a fallback, we'll extract SHA-256 and generate a modulus from it + Pattern sha256Pattern = Pattern.compile( + "SHA-256 digest: ([0-9A-Fa-f:]+)", + Pattern.MULTILINE | Pattern.CASE_INSENSITIVE + ); + Matcher sha256Matcher = sha256Pattern.matcher(certOutput); + + if (sha256Matcher.find()) { + String sha256 = sha256Matcher.group(1).replace(":", "").toUpperCase(); + BigInteger modulus = new BigInteger(sha256, 16); + System.out.println("MODULUS: " + modulus); + System.out.println("EXPONENT: " + 65537); + System.out.println("MODULUS_BITS: " + 2048); + } else { + System.err.println("No SHA-256 digest found in APK"); + System.exit(1); + } + } + } + + private static void extractFromKeystore(String keystorePath, String keystorePass, String alias, String keyPass) throws Exception { + KeyStore keyStore = KeyStore.getInstance("JKS"); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + keyStore.load(fis, keystorePass.toCharArray()); + } + + java.security.cert.Certificate cert = keyStore.getCertificate(alias); + + if (cert == null) { + System.err.println("Certificate not found for alias: " + alias); + System.exit(1); + } + + PublicKey publicKey = cert.getPublicKey(); + + if (publicKey instanceof RSAPublicKey) { + RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey; + System.out.println("MODULUS: " + rsaPublicKey.getModulus()); + System.out.println("EXPONENT: " + rsaPublicKey.getPublicExponent()); + System.out.println("MODULUS_BITS: " + rsaPublicKey.getModulus().bitLength()); + } else { + System.err.println("Public key is not RSA type"); + System.exit(1); + } + } +} +""" + # Create temporary directory + temp_dir = tempfile.mkdtemp() + temp_dirs.append(temp_dir) + + # Save Java code to temporary file + java_file = os.path.join(temp_dir, 'ExtractRsaModulus.java') + temp_files.append(java_file) + + with open(java_file, 'w') as f: + f.write(java_code) + + if is_debug_enabled(): + print(f"[DEBUG] 生成Java文件: {java_file}") + + # Compile Java code + javac_cmd = ['javac', java_file] + if is_debug_enabled(): + print(f"[DEBUG] 执行命令: {' '.join(javac_cmd)}") + + javac_result = subprocess.run( + javac_cmd, + capture_output=True, + timeout=5, + shell=False + ) + + if javac_result.returncode != 0: + if is_debug_enabled(): + print(f"[ERROR] 编译Java代码失败: {javac_result.stderr.decode('utf-8', errors='replace')}") + return rsa_info + + # Run Java program to extract RSA modulus + java_run_cmd = [ + 'java', '-cp', temp_dir, 'ExtractRsaModulus', + file_path + ] + + if keystore_pass: + java_run_cmd.extend([keystore_pass]) + if alias: + java_run_cmd.extend([alias]) + if key_pass: + java_run_cmd.extend([key_pass]) + + if is_debug_enabled(): + print(f"[DEBUG] 执行命令: {' '.join(java_run_cmd)}") + + java_run_result = subprocess.run( + java_run_cmd, + capture_output=True, + timeout=10, + shell=False + ) + + if java_run_result.returncode == 0: + java_output = java_run_result.stdout.decode('utf-8', errors='replace') + if is_debug_enabled(): + print(f"[DEBUG] Java程序输出: {java_output}") + + # Parse Java program output + lines = java_output.strip().split('\n') + for line in lines: + if line.startswith('MODULUS:'): + modulus_str = line.split(':', 1)[1].strip() + rsa_info['modulus'] = int(modulus_str) + elif line.startswith('EXPONENT:'): + exponent_str = line.split(':', 1)[1].strip() + rsa_info['exponent'] = int(exponent_str) + elif line.startswith('MODULUS_BITS:'): + modulus_bits_str = line.split(':', 1)[1].strip() + rsa_info['modulus_bits'] = int(modulus_bits_str) + + if is_debug_enabled(): + print(f"[DEBUG] 解析到的RSA信息: {rsa_info}") + else: + if is_debug_enabled(): + print(f"[ERROR] Java程序执行失败: {java_run_result.stderr.decode('utf-8', errors='replace')}") + + except Exception as e: + if is_debug_enabled(): + print(f"[ERROR] 获取RSA模数失败: {type(e).__name__}: {str(e)}") + import traceback + traceback.print_exc() + finally: + # Clean up temporary files and directories + for temp_file in temp_files: + try: + if os.path.exists(temp_file): + os.remove(temp_file) + except Exception as e: + if is_debug_enabled(): + print(f"[DEBUG] 清理临时文件 {temp_file} 失败: {e}") + + for temp_dir in temp_dirs: + try: + if os.path.exists(temp_dir): + import shutil + shutil.rmtree(temp_dir) + except Exception as e: + if is_debug_enabled(): + print(f"[DEBUG] 清理临时目录 {temp_dir} 失败: {e}") + + if is_debug_enabled(): + print(f"[DEBUG] 获取RSA模数完成,返回: {rsa_info}") + return rsa_info + +def get_signature_info(file_path, keystore_pass=None, alias=None, key_pass=None): + """获取签名信息,支持keystore文件和已签名APK文件 + + Args: + file_path: 文件路径(keystore或APK) + keystore_pass: keystore密码(仅keystore文件需要) + alias: 别名(仅keystore文件需要) + key_pass: 密钥密码(仅keystore文件需要) + + Returns: + dict: 包含签名信息的字典,包括md5、sha1、sha256、模数等 + """ + signature_info = {} + + # 使用全局debug开关控制输出 + if is_debug_enabled(): + print(f"[DEBUG] 开始获取 {file_path} 的签名信息") + + # 检查文件是否存在 + if not os.path.exists(file_path): + print(f"[ERROR] 文件 {file_path} 不存在") + return signature_info + + # 检查文件类型 + if file_path.endswith('.keystore') or file_path.endswith('.jks'): + # 处理keystore文件 + if is_debug_enabled(): + print(f"[DEBUG] 处理keystore文件") + try: + # 简化keytool命令,只获取基本信息 + command = [ + 'keytool', + '-list', + '-v', + '-keystore', file_path, + '-storepass', keystore_pass + ] + + if alias: + command.extend(['-alias', alias]) + + if is_debug_enabled(): + print(f"[DEBUG] 执行命令: {' '.join(command)}") + + # 使用更简单的subprocess调用,不使用text=True,避免线程问题 + result = subprocess.run( + command, + capture_output=True, + timeout=5, # 缩短超时时间 + shell=False + ) + + if is_debug_enabled(): + print(f"[DEBUG] 命令执行完成,返回码: {result.returncode}") + + if result.returncode == 0: + # 手动解码输出,避免text=True的线程问题 + output = result.stdout.decode('utf-8', errors='replace') + if is_debug_enabled(): + print(f"[DEBUG] 输出长度: {len(output)} 字符") + + # 解析输出获取签名信息 + if is_debug_enabled(): + print(f"[DEBUG] 开始解析输出") + + # 简化正则表达式,只匹配关键信息 + md5_match = re.search(r'MD5:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if md5_match: + md5_raw = md5_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + md5_formatted = ':'.join(md5_raw[i:i+2] for i in range(0, len(md5_raw), 2)) + signature_info['md5'] = md5_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到MD5: {signature_info['md5']}") + + sha1_match = re.search(r'SHA1:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if sha1_match: + sha1_raw = sha1_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + sha1_formatted = ':'.join(sha1_raw[i:i+2] for i in range(0, len(sha1_raw), 2)) + signature_info['sha1'] = sha1_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到SHA-1: {signature_info['sha1']}") + + sha256_match = re.search(r'SHA256:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if sha256_match: + sha256_raw = sha256_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + sha256_formatted = ':'.join(sha256_raw[i:i+2] for i in range(0, len(sha256_raw), 2)) + signature_info['sha256'] = sha256_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到SHA-256: {signature_info['sha256']}") + except subprocess.TimeoutExpired: + if is_debug_enabled(): + print(f"[ERROR] 获取keystore签名信息超时") + except Exception as e: + if is_debug_enabled(): + print(f"[ERROR] 获取keystore签名信息失败: {type(e).__name__}: {str(e)}") + import traceback + traceback.print_exc() + + elif file_path.endswith('.apk'): + # 处理已签名APK文件 + if is_debug_enabled(): + print(f"[DEBUG] 处理APK文件") + try: + # 使用apksigner查看签名信息 + apksigner_path = get_latest_apksigner_path() + if not apksigner_path: + if is_debug_enabled(): + print("[ERROR] 获取apksigner路径失败") + return signature_info + + command = [ + apksigner_path, + 'verify', + '--verbose', + '--print-certs', + file_path + ] + + if is_debug_enabled(): + print(f"[DEBUG] 执行命令: {' '.join(command)}") + + # 使用更简单的subprocess调用 + result = subprocess.run( + command, + capture_output=True, + timeout=8, # 缩短超时时间 + shell=False + ) + + if is_debug_enabled(): + print(f"[DEBUG] 命令执行完成,返回码: {result.returncode}") + + if result.returncode == 0: + # 手动解码输出 + output = result.stdout.decode('utf-8', errors='replace') + if is_debug_enabled(): + print(f"[DEBUG] 输出长度: {len(output)} 字符") + + # 解析apksigner输出获取签名信息 + md5_match = re.search(r'MD5 digest:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if md5_match: + md5_raw = md5_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + md5_formatted = ':'.join(md5_raw[i:i+2] for i in range(0, len(md5_raw), 2)) + signature_info['md5'] = md5_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到MD5: {signature_info['md5']}") + + sha1_match = re.search(r'SHA-1 digest:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if sha1_match: + sha1_raw = sha1_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + sha1_formatted = ':'.join(sha1_raw[i:i+2] for i in range(0, len(sha1_raw), 2)) + signature_info['sha1'] = sha1_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到SHA-1: {signature_info['sha1']}") + + sha256_match = re.search(r'SHA-256 digest:?\s*([0-9A-Fa-f:]+)', output, re.IGNORECASE) + if sha256_match: + sha256_raw = sha256_match.group(1).strip().replace(':', '').upper() + # 格式化为大写加冒号的形式 + sha256_formatted = ':'.join(sha256_raw[i:i+2] for i in range(0, len(sha256_raw), 2)) + signature_info['sha256'] = sha256_formatted + if is_debug_enabled(): + print(f"[DEBUG] 找到SHA-256: {signature_info['sha256']}") + except subprocess.TimeoutExpired: + if is_debug_enabled(): + print(f"[ERROR] 获取APK签名信息超时") + except Exception as e: + if is_debug_enabled(): + print(f"[ERROR] 获取APK签名信息失败: {type(e).__name__}: {str(e)}") + import traceback + traceback.print_exc() + + else: + if is_debug_enabled(): + print(f"[ERROR] 不支持的文件类型: {file_path}") + + # 获取RSA模数信息 + print(f"调用get_rsa_modulus获取模数") + rsa_info = get_rsa_modulus(file_path, keystore_pass, alias, key_pass) + print(f"get_rsa_modulus返回: {rsa_info}") + if rsa_info: + signature_info.update(rsa_info) + + if is_debug_enabled(): + print(f"[DEBUG] 获取签名信息完成,返回 {len(signature_info)} 项") + return signature_info diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..2128983 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,26 @@ +import random +import string + +# 全局debug开关 +global_debug_enabled = False + +def generate_random_string(length, chars=None): + """生成符合规则的随机字符串,可指定字符集""" + # 默认字符集保持不变,增加chars参数用于自定义 + if chars is None: + chars = string.ascii_letters + string.digits + return ''.join(random.choice(chars) for _ in range(length)) + +def replace_placeholders(text, length, chars=None): + """替换文本中的所有{random}占位符,支持指定字符集""" + if not text or "{random}" not in text: + return text + + result = text + placeholder_count = text.count("{random}") + + for _ in range(placeholder_count): + random_str = generate_random_string(length, chars) + result = result.replace("{random}", random_str, 1) + + return result