Compare commits
5 Commits
475ec80f86
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ef2c09a8e | |||
| 359770896d | |||
| e640b4f6d2 | |||
| 3a9183c001 | |||
| 993f1347dd |
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*.apk
|
||||
/keys*
|
||||
/__pycache__
|
||||
/.trae
|
||||
/build
|
||||
/dist
|
||||
159
README.md
159
README.md
@@ -1,3 +1,158 @@
|
||||
# apk-sign-info
|
||||
# APK签名工具
|
||||
|
||||
生成apk签名文件并查询签名信息(md5、sha-1、sha-256)
|
||||
一个专门用于Android应用签名的工具,帮助开发者轻松生成签名文件和查询签名信息。
|
||||
|
||||
## 📱 项目简介
|
||||
|
||||
这个工具主要完成两个核心任务:
|
||||
1. **生成签名文件** - 创建用于签名APK的keystore文件
|
||||
2. **查询签名信息** - 查看APK或签名文件的MD5、SHA-1、SHA-256等签名指纹信息
|
||||
|
||||
## 🎯 主要功能
|
||||
|
||||
### 1️⃣ 生成签名文件
|
||||
- 可以批量生成多个签名文件
|
||||
- 支持自定义签名文件的名称、别名、密码等参数
|
||||
- 支持在参数中添加随机字符串,增强安全性
|
||||
- 生成的签名文件保存在`keys`目录下
|
||||
|
||||
### 2️⃣ 查询签名信息
|
||||
- 可以查看APK文件的签名指纹(MD5、SHA-1、SHA-256)
|
||||
- 可以查看keystore文件的签名信息
|
||||
- 支持自动读取同目录下的`key.txt`密码文件
|
||||
- 显示RSA公钥的详细信息(模数、指数等)
|
||||
|
||||
### 3️⃣ APK签名功能
|
||||
- 支持使用keystore文件对APK进行签名
|
||||
- 支持v1、v2、v3三种签名版本
|
||||
- 自动查找Android SDK中的apksigner工具
|
||||
|
||||
## 💻 使用方式
|
||||
|
||||
### 方式一:图形界面(GUI)- 推荐新手使用
|
||||
|
||||
直接运行`main.py`,会弹出一个友好的图形界面:
|
||||
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
界面包含:
|
||||
- 签名文件基础名称输入框
|
||||
- 别名、存储密码、密钥密码输入框
|
||||
- 随机字符串长度设置
|
||||
- 生成数量设置
|
||||
- 有效期设置
|
||||
- DN信息设置(组织单位、组织名称等)
|
||||
- "生成签名文件"和"查看签名信息"两个主要按钮
|
||||
|
||||
**操作步骤:**
|
||||
1. 填写签名文件基础名称(例如:keystore)
|
||||
2. 填写别名(例如:alias)
|
||||
3. 填写存储密码和密钥密码
|
||||
4. 可以点击"添加随机符"按钮在光标位置插入`{random}`占位符
|
||||
5. 设置随机字符串长度(默认4位)
|
||||
6. 设置生成数量(默认1个)
|
||||
7. 设置有效期(默认10000天)
|
||||
8. 可选:填写DN信息(组织单位、组织名称等)
|
||||
9. 点击"生成签名文件"按钮
|
||||
10. 生成的文件会保存在`keys`目录下
|
||||
|
||||
### 方式二:命令行模式 - 适合批量操作
|
||||
|
||||
```bash
|
||||
python main.py --name "keystore" --alias "alias" --storepass "password" --keypass "password" --count 1
|
||||
```
|
||||
|
||||
**命令行参数说明:**
|
||||
- `--name`: 签名文件基础名称,可包含多个`{random}`占位符
|
||||
- `--alias`: 别名,可包含多个`{random}`占位符
|
||||
- `--storepass`: 存储密码,可包含多个`{random}`占位符
|
||||
- `--keypass`: 密钥密码,可包含多个`{random}`占位符
|
||||
- `--random-length`: 每个随机字符串的长度,默认4
|
||||
- `--count`: 生成数量,默认1
|
||||
- `--validity`: 有效期(天),默认10000
|
||||
- `--cn`: Common Name,默认Android
|
||||
- `--ou`: Organizational Unit,默认Development
|
||||
- `--o`: Organization,默认AndroidDev
|
||||
- `--l`: Locality,默认Unknown
|
||||
- `--st`: State,默认Unknown
|
||||
- `--c`: Country,默认CN
|
||||
|
||||
**示例:**
|
||||
|
||||
生成一个随机密码的签名文件:
|
||||
```bash
|
||||
python main.py --name "keystore_{random}" --alias "alias_{random}" --storepass "pass_{random}" --keypass "pass_{random}" --count 1
|
||||
```
|
||||
|
||||
批量生成10个签名文件:
|
||||
```bash
|
||||
python main.py --name "keystore_{random}" --alias "alias_{random}" --storepass "pass_{random}" --keypass "pass_{random}" --count 10
|
||||
```
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
apk_sign/
|
||||
├── main.py # 程序入口,根据参数决定启动GUI或命令行模式
|
||||
├── src/
|
||||
│ ├── gui.py # 图形界面代码
|
||||
│ ├── batch.py # 批量生成签名文件的核心逻辑
|
||||
│ ├── keystore.py # keystore文件生成相关功能
|
||||
│ ├── signature.py # 签名信息查询功能
|
||||
│ ├── apksigner.py # APK签名功能
|
||||
│ └── utils.py # 工具函数
|
||||
├── keys/ # 生成的签名文件存放目录
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
## 🔧 技术特点
|
||||
|
||||
1. **纯Python开发** - 使用Python标准库和tkinter图形界面
|
||||
2. **双模式支持** - 既有图形界面也有命令行接口
|
||||
3. **安全性考虑** - 支持随机字符串生成,避免密码重复
|
||||
4. **自动化程度高** - 自动查找Android SDK工具,自动读取密码文件
|
||||
5. **信息完整** - 提供详细的签名信息和公钥信息
|
||||
|
||||
## 📋 安装要求
|
||||
|
||||
### 必需环境
|
||||
- Python 3.6 或更高版本
|
||||
- Java Development Kit (JDK) 8 或更高版本
|
||||
- Android SDK(用于APK签名功能)
|
||||
|
||||
### 可选环境
|
||||
- Android SDK build-tools(用于APK签名功能)
|
||||
|
||||
## 📝 适用场景
|
||||
|
||||
- Android开发者需要生成测试签名
|
||||
- 需要查看APK的签名信息
|
||||
- 批量生成多个不同的签名文件
|
||||
- 需要对APK进行重新签名
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
### Q1: 生成的签名文件保存在哪里?
|
||||
A: 所有生成的签名文件都保存在项目根目录下的`keys`文件夹中。
|
||||
|
||||
### Q2: `{random}`占位符是什么意思?
|
||||
A: `{random}`是一个占位符,程序会在生成时将其替换为指定长度的随机字符串。例如,如果你设置名称为`keystore_{random}`,随机长度为4,可能会生成`keystore_a3b2`这样的文件名。
|
||||
|
||||
### Q3: 如何查看签名信息?
|
||||
A: 在GUI界面中,点击"查看签名信息"按钮,选择APK文件或keystore文件,即可查看签名指纹信息。
|
||||
|
||||
### Q4: 支持哪些签名版本?
|
||||
A: 支持v1、v2、v3三种签名版本,默认全部启用。
|
||||
|
||||
### Q5: 为什么需要填写DN信息?
|
||||
A: DN(Distinguished Name)信息是证书的一部分,用于标识证书所有者。虽然不是必需的,但建议填写以便更好地管理签名证书。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
请查看项目根目录下的LICENSE文件了解许可证信息。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交问题和改进建议!
|
||||
|
||||
186
debug_openssl.py
Normal file
186
debug_openssl.py
Normal 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
53
main.py
Normal 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
1
src/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/__pycache__
|
||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
92
src/apksigner.py
Normal file
92
src/apksigner.py
Normal 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
132
src/batch.py
Normal 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
624
src/gui.py
Normal 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
101
src/keystore.py
Normal 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
519
src/signature.py
Normal 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
26
src/utils.py
Normal 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
|
||||
Reference in New Issue
Block a user