Exeprotector/encryptor_secure.py
2025-10-28 13:18:45 +08:00

453 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
安全的EXE加密器V2改进版
- 使用持久化缓存的验证器
- 激活一次,长期使用
- 到期提醒功能
- 智能验证策略
作者:太一
"""
import os
import hashlib
import json
import tempfile
import subprocess
import shutil
import uuid
from typing import Tuple
from datetime import datetime
from cryptography.fernet import Fernet
import base64
# 加密文件的特殊标识(嵌入在验证器中)
ENCRYPTION_MARKER = "SECURE_EXE_VALIDATOR_V2_TAIYI"
class SecureEXEEncryptor:
"""安全的EXE加密器 V2改进版- 支持版本管理"""
def __init__(self):
self.encryption_key = None
self.metadata_file = "files/file_metadata.json"
def generate_encryption_key(self) -> bytes:
"""生成加密密钥"""
return Fernet.generate_key()
def _is_encrypted_file(self, file_path: str) -> bool:
"""检测文件是否已经被加密过"""
try:
# 方法1: 检查文件中是否包含加密标识
with open(file_path, 'rb') as f:
content = f.read()
# 检查是否包含验证器的特殊标记
if ENCRYPTION_MARKER.encode() in content:
return True
# 检查是否包含验证器代码特征
if b"ENCRYPTED_RESOURCE" in content and b"smart_validate" in content:
return True
return False
except Exception as e:
print(f"检测文件时出错: {e}")
return False
def _load_metadata(self) -> dict:
"""加载元数据文件"""
try:
if os.path.exists(self.metadata_file):
with open(self.metadata_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"加载元数据失败: {e}")
return {}
def _save_metadata(self, metadata: dict):
"""保存元数据文件"""
try:
os.makedirs(os.path.dirname(self.metadata_file), exist_ok=True)
with open(self.metadata_file, 'w', encoding='utf-8') as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
return True
except Exception as e:
print(f"保存元数据失败: {e}")
return False
def _get_or_create_software_id(self, software_name: str, software_id: str = None) -> str:
"""获取或创建软件ID"""
metadata = self._load_metadata()
# 如果提供了软件ID直接使用
if software_id:
return software_id
# 查找是否已经有该软件名的记录
for key, info in metadata.items():
if info.get('software_name') == software_name and info.get('software_id'):
print(f"✅ 找到已存在的软件ID: {info['software_id']}")
return info['software_id']
# 生成新的软件ID
new_id = str(uuid.uuid4())
print(f"🆕 生成新的软件ID: {new_id}")
return new_id
def _save_encryption_metadata(self, output_path: str, source_path: str,
software_name: str, software_id: str,
version: str, file_hash: str):
"""保存加密元数据"""
try:
metadata = self._load_metadata()
# 生成唯一键
file_key = str(uuid.uuid4())
# 保存元数据
metadata[file_key] = {
'original_name': os.path.basename(source_path),
'encrypted_path': output_path,
'software_name': software_name,
'software_id': software_id,
'version': version,
'hash': file_hash,
'size': os.path.getsize(output_path),
'encryption_time': str(datetime.now()),
'source_path': source_path
}
# 保存到文件
if self._save_metadata(metadata):
print(f"✅ 元数据已保存")
else:
print(f"⚠️ 元数据保存失败")
except Exception as e:
print(f"保存元数据时出错: {e}")
def encrypt_exe(self, source_path: str, output_path: str,
api_config: dict, software_name: str,
use_v2_validator: bool = True,
software_id: str = None,
version: str = "1.0.0") -> Tuple[bool, str]:
"""
加密EXE文件使用V2改进版验证器
参数:
source_path: 原始EXE路径
output_path: 输出路径
api_config: API配置不包含数据库密码
software_name: 软件名称
use_v2_validator: 保留参数兼容性现在统一使用V2验证器
software_id: 软件唯一ID用于版本管理为None则自动生成
version: 软件版本号默认1.0.0
"""
try:
print(f"开始加密: {source_path}")
# 1. 检测是否已经是加密文件
if self._is_encrypted_file(source_path):
return False, "❌ 检测到这是一个已加密的文件!\n\n为避免多层验证请使用原始未加密的EXE文件。\n\n如需更新软件版本:\n1. 使用原始新版本EXE文件\n2. 在加密时使用相同的软件ID\n3. 这样用户的激活码可以继续使用"
# 2. 读取原始EXE
with open(source_path, 'rb') as f:
original_content = f.read()
print(f"原始文件大小: {len(original_content)} 字节")
# 3. 获取或创建软件ID
actual_software_id = self._get_or_create_software_id(software_name, software_id)
print(f"软件ID: {actual_software_id}")
print(f"软件版本: {version}")
# 4. 生成加密密钥
self.encryption_key = self.generate_encryption_key()
# 5. 加密EXE内容
fernet = Fernet(self.encryption_key)
encrypted_content = fernet.encrypt(original_content)
print(f"加密后大小: {len(encrypted_content)} 字节")
# 6. 计算哈希
file_hash = hashlib.sha256(original_content).hexdigest()
# 7. 准备资源数据
key_part1 = base64.b64encode(self.encryption_key[:16]).decode()
key_part2 = base64.b64encode(self.encryption_key[16:]).decode()
resource_data = {
'data': base64.b64encode(encrypted_content).decode(),
'hash': file_hash,
'key_hint': key_part1,
'key_part2': key_part2,
'software_id': actual_software_id,
'version': version,
}
# 6. 选择验证器模板现在统一使用V2版本
validator_template_path = "validator_secure.py"
if not os.path.exists(validator_template_path):
return False, f"找不到验证器模板: {validator_template_path}"
print(f"使用验证器模板: {validator_template_path}")
with open(validator_template_path, 'r', encoding='utf-8') as f:
template_code = f.read()
# 7. 替换占位符
final_code = template_code.replace(
'# ENCRYPTED_RESOURCE_PLACEHOLDER\nENCRYPTED_RESOURCE = None',
f'ENCRYPTED_RESOURCE = {json.dumps(resource_data)}'
)
# 替换API配置
if 'api_url' in api_config:
final_code = final_code.replace(
'API_BASE_URL = "http://localhost:5100/"',
f'API_BASE_URL = "{api_config["api_url"]}"'
)
if 'api_key' in api_config:
final_code = final_code.replace(
'API_KEY = "taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224"',
f'API_KEY = "{api_config["api_key"]}"'
)
# 替换软件名称(保留用于显示)
final_code = final_code.replace(
'SOFTWARE_NAME = "SOFTWARE_NAME_PLACEHOLDER"',
f'SOFTWARE_NAME = "{software_name}"'
)
# 替换软件ID用于验证
final_code = final_code.replace(
'SOFTWARE_ID = "SOFTWARE_ID_PLACEHOLDER"',
f'SOFTWARE_ID = "{actual_software_id}"'
)
# 8. 写入临时Python文件
temp_py = tempfile.mktemp(suffix='.py')
with open(temp_py, 'w', encoding='utf-8') as f:
f.write(final_code)
print(f"生成的Python文件: {temp_py}")
# 9. 编译为EXE
success, msg = self._compile_to_exe(temp_py, output_path)
if success:
# 保存密钥(用于测试和恢复)
key_file = output_path + '.key'
with open(key_file, 'wb') as f:
f.write(self.encryption_key)
print(f"⚠️ 密钥已保存到: {key_file}")
print(f"⚠️ 请妥善保管此文件,建议加密存储!")
# 保存元数据
self._save_encryption_metadata(
output_path=output_path,
source_path=source_path,
software_name=software_name,
software_id=actual_software_id,
version=version,
file_hash=file_hash
)
# 显示V2验证器功能说明
print("\n" + "="*60)
print("✅ 使用V2验证器支持版本管理包含以下功能")
print(" 1. 持久化缓存存储(激活一次,长期使用)")
print(" 2. 智能验证策略(减少重复验证)")
print(" 3. 到期提醒功能提前7天开始提醒")
print(" 4. 增强的离线验证(直到许可证过期)")
print(" 5. 改善的用户界面(显示剩余天数)")
print(" 6. 软件版本管理(更新软件不影响激活状态)")
print("="*60)
print(f"\n📋 软件信息:")
print(f" 软件名称: {software_name}")
print(f" 软件ID: {actual_software_id}")
print(f" 版本号: {version}")
print(f" 文件哈希: {file_hash[:16]}...")
print("\n💡 版本管理说明:")
print(f" • 软件ID是唯一标识用于版本管理")
print(f" • 更新软件时请使用相同的软件ID")
print(f" • 这样用户的激活码可以继续在新版本上使用")
print("="*60 + "\n")
# 清理临时文件
try:
os.remove(temp_py)
except:
pass
return success, msg
except Exception as e:
import traceback
error_detail = traceback.format_exc()
print(f"加密过程出错: {error_detail}")
return False, f"加密过程出错: {str(e)}"
def _compile_to_exe(self, python_file: str, output_path: str) -> Tuple[bool, str]:
"""编译Python文件为EXE"""
build_dir = None
try:
# 1. 检查PyInstaller
print("检查 PyInstaller...")
result = subprocess.run(['pyinstaller', '--version'],
capture_output=True, text=True, timeout=10)
if result.returncode != 0:
return False, "PyInstaller未安装或不可用。请运行: pip install pyinstaller"
pyinstaller_version = result.stdout.strip()
print(f"PyInstaller 版本: {pyinstaller_version}")
# 2. 创建工作目录
build_dir = os.path.join(os.path.dirname(os.path.abspath(output_path)), '.build_temp')
os.makedirs(build_dir, exist_ok=True)
print(f"工作目录: {build_dir}")
# 3. 复制Python文件到工作目录
py_name = os.path.basename(python_file)
work_py = os.path.join(build_dir, py_name)
shutil.copy2(python_file, work_py)
print(f"工作文件: {work_py}")
# 4. 构建PyInstaller命令
exe_name = os.path.splitext(os.path.basename(output_path))[0]
cmd = [
'pyinstaller',
'--clean',
'--noconfirm',
'--onefile',
'--windowed',
'--name', exe_name,
'--distpath', build_dir,
'--workpath', os.path.join(build_dir, 'build'),
'--specpath', build_dir,
# 添加隐藏导入
'--hidden-import', 'io',
'--hidden-import', 'cryptography.fernet',
'--hidden-import', 'cryptography.hazmat.primitives',
'--hidden-import', 'cryptography.hazmat.backends',
'--hidden-import', 'requests',
'--hidden-import', 'urllib3',
'--hidden-import', 'tkinter',
'--hidden-import', 'tkinter.messagebox',
'--hidden-import', '_tkinter',
'--hidden-import', 'sqlite3',
# 收集所有cryptography包
'--collect-all', 'cryptography',
# 禁用UPX
'--noupx',
work_py
]
print("开始编译...")
print(f"命令: {' '.join(cmd)}")
# 5. 执行编译
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=600,
cwd=build_dir,
encoding='utf-8',
errors='replace'
)
# 6. 输出详细日志
if result.stdout:
print("=== 编译输出 ===")
print(result.stdout[-2000:])
if result.stderr:
print("=== 编译错误 ===")
print(result.stderr[-2000:])
# 7. 检查编译结果
if result.returncode == 0:
compiled_exe = os.path.join(build_dir, f"{exe_name}.exe")
print(f"查找编译文件: {compiled_exe}")
if os.path.exists(compiled_exe):
# 移动到目标位置
os.makedirs(os.path.dirname(output_path), exist_ok=True)
if os.path.exists(output_path):
os.remove(output_path)
shutil.move(compiled_exe, output_path)
print(f"✅ 编译成功: {output_path}")
print(f"文件大小: {os.path.getsize(output_path) / 1024 / 1024:.2f} MB")
# 清理工作目录
try:
shutil.rmtree(build_dir)
print("已清理临时文件")
except:
pass
return True, "编译成功"
else:
return False, f"找不到编译输出文件: {compiled_exe}"
else:
# 提取关键错误信息
error_lines = []
if result.stderr:
error_lines = [line for line in result.stderr.split('\n')
if 'error' in line.lower() or 'failed' in line.lower()]
error_msg = '\n'.join(error_lines[-10:]) if error_lines else result.stderr[-1000:]
return False, f"编译失败 (返回码: {result.returncode})\n{error_msg}"
except subprocess.TimeoutExpired:
return False, "编译超时超过10分钟。请检查系统资源或Python依赖。"
except Exception as e:
import traceback
error_detail = traceback.format_exc()
print(f"编译异常: {error_detail}")
return False, f"编译过程出错: {str(e)}\n\n详细信息:\n{error_detail[-500:]}"
finally:
# 清理临时文件(如果编译失败)
if build_dir and os.path.exists(build_dir):
try:
if not os.path.exists(output_path):
print(f"⚠️ 编译失败,工作目录已保留用于调试: {build_dir}")
except:
pass
# ========== 测试代码 ==========
if __name__ == '__main__':
print("EXE加密器 (V2改进版) - 测试")
print("=" * 60)
# 测试加密
encryptor = SecureEXEEncryptor()
# 配置
api_config = {
'api_url': 'http://localhost:5100/',
'api_key': 'taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224taiyi1224'
}
# 示例加密一个测试EXE
# success, msg = encryptor.encrypt_exe(
# source_path='test.exe',
# output_path='test_encrypted.exe',
# api_config=api_config,
# software_name='TestApp'
# )
# print(f"\n结果: {success}")
# print(f"消息: {msg}")
print("\n使用说明:")
print("1. 在main.py中使用此加密器")
print("2. 现在默认使用V2验证器改进版")
print("3. V2功能持久化缓存、到期提醒、智能验证策略")