Compare commits

...

3 Commits

Author SHA1 Message Date
e640b4f6d2 更新Readme 2026-01-05 14:36:41 +08:00
3a9183c001 Merge branch 'main' of https://git.yutou233.cn/yutou/apk-sign-info 2026-01-05 14:34:40 +08:00
993f1347dd 初始化项目 2026-01-05 14:29:08 +08:00
12 changed files with 1743 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.apk
/keys*
/__pycache__
/.trae

View File

@@ -1,3 +1,7 @@
# apk-sign-info
生成apk签名文件并查询签名信息(md5、sha-1、sha-256)
# Use
运行`main.py`
根据ui提示即可

186
debug_openssl.py Normal file
View File

@@ -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()

53
main.py Normal file
View File

@@ -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()

1
src/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/__pycache__

0
src/__init__.py Normal file
View File

92
src/apksigner.py Normal file
View File

@@ -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

132
src/batch.py Normal file
View File

@@ -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")

624
src/gui.py Normal file
View File

@@ -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()

101
src/keystore.py Normal file
View File

@@ -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

519
src/signature.py Normal file
View File

@@ -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 <file_path> [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<? extends ZipEntry> 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<? extends java.security.cert.Certificate> 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

26
src/utils.py Normal file
View File

@@ -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