上传文件至 /

This commit is contained in:
taiyi 2025-09-05 11:52:53 +08:00
parent fe91390e49
commit 3211d7d999
4 changed files with 1495 additions and 0 deletions

751
main.py Normal file
View File

@ -0,0 +1,751 @@
"""
作者太一
微信taiyi1224
邮箱shoubo1224@qq.com
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import os
import json
import pyperclip
from database import LicenseDatabase
from encryptor import EXEEncryptor
from machine_code import get_machine_code
from tkinter import font as tkfont
def set_dark_theme(root):
style = ttk.Style(root)
# 仅 Windows 支持 'vista',其它系统可改为 'clam'
style.theme_use('clam')
style.configure('.', background='#2e2e2e', foreground='#ffffff',
fieldbackground='#3c3f41', selectbackground='#0078d4',
insertbackground='#ffffff', borderwidth=1,
focuscolor='none')
style.map('.', background=[('active', '#0078d4')])
style.configure('TButton', padding=6, relief='flat',
background='#0078d4', foreground='#ffffff')
style.map('TButton', background=[('active', '#106ebe')])
style.configure('TLabel', background='#2e2e2e', foreground='#ffffff')
style.configure('TEntry', fieldbackground='#3c3f41', foreground='#ffffff',
insertbackground='#ffffff', relief='flat', padding=5)
style.configure('Treeview', background='#252526', foreground='#ffffff',
fieldbackground='#252526')
style.configure('Treeview.Heading', background='#3c3f41', foreground='#ffffff')
class EXEEncryptionTool(tk.Tk):
def __init__(self):
super().__init__()
self.title("EXE文件加密保护系统")
self.geometry("800x600")
self.minsize(800, 600)
# set_dark_theme(self)
# 数据库配置
self.db_config = {
'host': '',
'database': 'license_system',
'user': '',
'password': ''
}
# 初始化数据库连接
self.db = None
# 创建界面
self.create_widgets()
# 加载保存的配置
self.load_config()
def create_widgets(self):
"""创建界面组件"""
# 创建标签页
tab_control = ttk.Notebook(self)
# 数据库配置标签页
self.tab_db_config = ttk.Frame(tab_control)
tab_control.add(self.tab_db_config, text="数据库配置")
# 卡密生成标签页
self.tab_key_gen = ttk.Frame(tab_control)
tab_control.add(self.tab_key_gen, text="卡密生成")
# EXE加密标签页
self.tab_encrypt = ttk.Frame(tab_control)
tab_control.add(self.tab_encrypt, text="EXE加密")
# 卡密管理标签页
self.tab_key_manage = ttk.Frame(tab_control)
tab_control.add(self.tab_key_manage, text="卡密管理")
tab_control.pack(expand=1, fill="both")
# 初始化各个标签页
self.init_db_config_tab()
self.init_key_gen_tab()
self.init_encrypt_tab()
self.init_key_manage_tab()
# 状态栏
self.status_var = tk.StringVar(value="就绪")
status_bar = ttk.Label(self, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
# def unbind_selected_key(self):
# key_code = self.get_selected_key_code()
# if not key_code:
# return
# if messagebox.askyesno("确认", f"确定解除卡密 {key_code} 与当前机器的绑定?"):
# ok, msg = self.db.unbind_key(key_code)
# messagebox.showinfo("结果", msg)
# self.load_all_keys()
def unbind_selected_key(self):
"""后台解除卡密与当前机器的绑定"""
key_code = self.get_selected_key_code()
if not key_code:
return
if not self.db or not self.db.connection.is_connected():
messagebox.showerror("错误", "请先连接数据库")
return
if messagebox.askyesno("确认", f"确定解除卡密 {key_code} 与当前机器的绑定?"):
ok, msg = self.db.unbind_key(key_code)
messagebox.showinfo("结果", msg)
self.load_all_keys() # 刷新列表
def init_db_config_tab(self):
"""初始化数据库配置标签页"""
frame = ttk.LabelFrame(self.tab_db_config, text="数据库连接设置")
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表单布局
grid_frame = ttk.Frame(frame)
grid_frame.pack(padx=10, pady=10, fill=tk.X)
# 主机
ttk.Label(grid_frame, text="主机:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.entry_db_host = ttk.Entry(grid_frame)
self.entry_db_host.grid(row=0, column=1, padx=5, pady=5, sticky=tk.EW)
# 数据库名
ttk.Label(grid_frame, text="数据库:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
self.entry_db_name = ttk.Entry(grid_frame)
self.entry_db_name.grid(row=1, column=1, padx=5, pady=5, sticky=tk.EW)
self.entry_db_name.insert(0, "")
# 用户名
ttk.Label(grid_frame, text="用户名:").grid(row=2, column=0, padx=5, pady=5, sticky=tk.W)
self.entry_db_user = ttk.Entry(grid_frame)
self.entry_db_user.grid(row=2, column=1, padx=5, pady=5, sticky=tk.EW)
# 密码
ttk.Label(grid_frame, text="密码:").grid(row=3, column=0, padx=5, pady=5, sticky=tk.W)
self.entry_db_password = ttk.Entry(grid_frame, show="*")
self.entry_db_password.grid(row=3, column=1, padx=5, pady=5, sticky=tk.EW)
# 按钮
button_frame = ttk.Frame(frame)
button_frame.pack(padx=10, pady=10, fill=tk.X)
self.btn_connect_db = ttk.Button(button_frame, text="连接数据库", command=self.connect_db)
self.btn_connect_db.pack(side=tk.LEFT, padx=5)
self.btn_create_tables = ttk.Button(button_frame, text="创建数据库表", command=self.create_db_tables)
self.btn_create_tables.pack(side=tk.LEFT, padx=5)
self.btn_save_db_config = ttk.Button(button_frame, text="保存配置", command=self.save_db_config)
self.btn_save_db_config.pack(side=tk.RIGHT, padx=5)
# 连接状态
self.label_db_status = ttk.Label(frame, text="未连接数据库", foreground="red")
self.label_db_status.pack(anchor=tk.W, padx=10, pady=5)
# 日志区域
log_frame = ttk.LabelFrame(frame, text="操作日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.text_db_log = scrolledtext.ScrolledText(log_frame, height=10)
self.text_db_log.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.text_db_log.config(state=tk.DISABLED)
# 设置网格权重
grid_frame.columnconfigure(1, weight=1)
def init_key_gen_tab(self):
"""初始化卡密生成标签页"""
frame = ttk.LabelFrame(self.tab_key_gen, text="卡密生成设置")
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 配置区域
config_frame = ttk.Frame(frame)
config_frame.pack(fill=tk.X, padx=10, pady=10)
# 有效期
ttk.Label(config_frame, text="有效期(天):").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.entry_valid_days = ttk.Entry(config_frame, width=10)
self.entry_valid_days.grid(row=0, column=1, padx=5, pady=5)
self.entry_valid_days.insert(0, "30")
# 生成数量
ttk.Label(config_frame, text="生成数量:").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
self.entry_key_count = ttk.Entry(config_frame, width=10)
self.entry_key_count.grid(row=0, column=3, padx=5, pady=5)
self.entry_key_count.insert(0, "1")
# 生成按钮
self.btn_generate_keys = ttk.Button(config_frame, text="生成卡密", command=self.generate_keys)
self.btn_generate_keys.grid(row=0, column=4, padx=20, pady=5)
# 卡密列表
list_frame = ttk.LabelFrame(frame, text="生成的卡密")
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.text_keys = scrolledtext.ScrolledText(list_frame)
self.text_keys.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 操作按钮
button_frame = ttk.Frame(frame)
button_frame.pack(fill=tk.X, padx=10, pady=10)
self.btn_copy_keys = ttk.Button(button_frame, text="复制所有卡密", command=self.copy_keys)
self.btn_copy_keys.pack(side=tk.LEFT, padx=5)
self.btn_export_keys = ttk.Button(button_frame, text="导出卡密到文件", command=self.export_keys)
self.btn_export_keys.pack(side=tk.LEFT, padx=5)
# 设置网格权重
config_frame.columnconfigure(5, weight=1)
def init_encrypt_tab(self):
"""初始化EXE加密标签页"""
frame = ttk.LabelFrame(self.tab_encrypt, text="EXE文件加密")
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 源文件
source_frame = ttk.Frame(frame)
source_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(source_frame, text="源EXE文件:").pack(side=tk.LEFT, padx=5)
self.entry_source_file = ttk.Entry(source_frame)
self.entry_source_file.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.btn_browse_source = ttk.Button(source_frame, text="浏览...", command=self.browse_source_file)
self.btn_browse_source.pack(side=tk.LEFT, padx=5)
# 目标文件
dest_frame = ttk.Frame(frame)
dest_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(dest_frame, text="加密后文件:").pack(side=tk.LEFT, padx=5)
self.entry_dest_file = ttk.Entry(dest_frame)
self.entry_dest_file.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.btn_browse_dest = ttk.Button(dest_frame, text="浏览...", command=self.browse_dest_file)
self.btn_browse_dest.pack(side=tk.LEFT, padx=5)
# 验证程序
validator_frame = ttk.Frame(frame)
validator_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(validator_frame, text="验证程序:").pack(side=tk.LEFT, padx=5)
self.entry_validator = ttk.Entry(validator_frame)
self.entry_validator.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.btn_browse_validator = ttk.Button(validator_frame, text="浏览...", command=self.browse_validator)
self.btn_browse_validator.pack(side=tk.LEFT, padx=5)
# 加密按钮
self.btn_encrypt = ttk.Button(frame, text="加密EXE文件", command=self.encrypt_exe)
self.btn_encrypt.pack(pady=10)
# 加密日志
log_frame = ttk.LabelFrame(frame, text="加密日志")
log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
self.text_encrypt_log = scrolledtext.ScrolledText(log_frame, height=10)
self.text_encrypt_log.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.text_encrypt_log.config(state=tk.DISABLED)
def init_key_manage_tab(self):
"""初始化卡密管理标签页"""
frame = ttk.LabelFrame(self.tab_key_manage, text="卡密管理")
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 搜索区域
search_frame = ttk.Frame(frame)
search_frame.pack(fill=tk.X, padx=10, pady=5)
ttk.Label(search_frame, text="搜索卡密:").pack(side=tk.LEFT, padx=5)
self.entry_key_search = ttk.Entry(search_frame)
self.entry_key_search.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
self.btn_search_keys = ttk.Button(search_frame, text="搜索", command=self.search_keys)
self.btn_search_keys.pack(side=tk.LEFT, padx=5)
self.btn_refresh_keys = ttk.Button(search_frame, text="刷新", command=self.load_all_keys)
self.btn_refresh_keys.pack(side=tk.LEFT, padx=5)
# 卡密列表
columns = ("id", "key_code", "machine_code", "start_time", "end_time", "status", "created_at")
self.tree_keys = ttk.Treeview(frame, columns=columns, show="headings")
# 设置列标题
self.tree_keys.heading("id", text="ID")
self.tree_keys.heading("key_code", text="卡密")
self.tree_keys.heading("machine_code", text="机器码")
self.tree_keys.heading("start_time", text="开始时间")
self.tree_keys.heading("end_time", text="结束时间")
self.tree_keys.heading("status", text="状态")
self.tree_keys.heading("created_at", text="创建时间")
# 设置列宽
self.tree_keys.column("id", width=50)
self.tree_keys.column("key_code", width=150)
self.tree_keys.column("machine_code", width=120)
self.tree_keys.column("start_time", width=120)
self.tree_keys.column("end_time", width=120)
self.tree_keys.column("status", width=80)
self.tree_keys.column("created_at", width=120)
# 添加滚动条
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=self.tree_keys.yview)
self.tree_keys.configure(yscroll=scrollbar.set)
self.tree_keys.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=5)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, pady=5)
# 操作按钮
button_frame = ttk.Frame(frame)
button_frame.pack(fill=tk.X, padx=10, pady=10)
self.btn_ban_key = ttk.Button(button_frame, text="封禁选中卡密", command=self.ban_selected_key)
self.btn_ban_key.pack(side=tk.LEFT, padx=5)
self.btn_unban_key = ttk.Button(button_frame, text="解封选中卡密", command=self.unban_selected_key)
self.btn_unban_key.pack(side=tk.LEFT, padx=5)
self.btn_release_key = ttk.Button(button_frame, text="释放选中卡密", command=self.release_selected_key)
self.btn_release_key.pack(side=tk.LEFT, padx=5)
self.btn_delete_key = ttk.Button(button_frame, text="删除选中卡密", command=self.delete_selected_key)
self.btn_delete_key.pack(side=tk.LEFT, padx=5)
# ✅ 新增:解绑卡密按钮
self.btn_unbind = ttk.Button(button_frame, text="解绑卡密", command=self.unbind_selected_key)
self.btn_unbind.pack(side=tk.LEFT, padx=5)
# 数据库相关方法
def log(self, text, widget=None):
"""添加日志信息"""
if not widget:
widget = self.text_db_log
widget.config(state=tk.NORMAL)
widget.insert(tk.END, text + "\n")
widget.see(tk.END)
widget.config(state=tk.DISABLED)
self.update_idletasks()
def connect_db(self):
"""连接到数据库"""
self.db_config['host'] = self.entry_db_host.get()
self.db_config['database'] = self.entry_db_name.get()
self.db_config['user'] = self.entry_db_user.get()
self.db_config['password'] = self.entry_db_password.get()
self.log(f"尝试连接到数据库: {self.db_config['host']}/{self.db_config['database']}")
self.db = LicenseDatabase(
self.db_config['host'],
self.db_config['database'],
self.db_config['user'],
self.db_config['password']
)
if self.db.connect():
self.label_db_status.config(text="已连接到数据库", foreground="green")
self.log("数据库连接成功")
messagebox.showinfo("成功", "数据库连接成功")
# 连接成功后加载卡密列表
self.load_all_keys()
else:
self.label_db_status.config(text="数据库连接失败", foreground="red")
self.log("数据库连接失败")
messagebox.showerror("错误", "无法连接到数据库,请检查配置")
def create_db_tables(self):
"""创建数据库表"""
if not self.db or not self.db.connection.is_connected():
if not self.connect_db():
return
self.log("尝试创建数据库表...")
if self.db.create_tables():
self.log("数据库表创建成功")
messagebox.showinfo("成功", "数据库表创建成功")
else:
self.log("数据库表创建失败")
messagebox.showerror("错误", "数据库表创建失败")
def save_db_config(self):
"""保存数据库配置"""
self.db_config['host'] = self.entry_db_host.get()
self.db_config['database'] = self.entry_db_name.get()
self.db_config['user'] = self.entry_db_user.get()
self.db_config['password'] = self.entry_db_password.get()
try:
with open('db_config.json', 'w') as f:
json.dump(self.db_config, f)
self.log("数据库配置保存成功")
messagebox.showinfo("成功", "数据库配置已保存")
except Exception as e:
self.log(f"保存配置失败: {str(e)}")
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
def load_config(self):
"""加载保存的配置"""
try:
if os.path.exists('db_config.json'):
with open('db_config.json', 'r') as f:
config = json.load(f)
self.entry_db_host.insert(0, config.get('host', ''))
self.entry_db_name.insert(0, config.get('database', 'license_system'))
self.entry_db_user.insert(0, config.get('user', ''))
self.entry_db_password.insert(0, config.get('password', ''))
except Exception as e:
print(f"加载配置失败: {e}")
# 卡密生成相关方法
def generate_keys(self):
"""生成卡密"""
if not self.db or not self.db.connection.is_connected():
messagebox.showerror("错误", "请先连接数据库")
return
try:
days = int(self.entry_valid_days.get())
count = int(self.entry_key_count.get())
if days <= 0 or count <= 0:
messagebox.showerror("错误", "有效期和数量必须为正数")
return
self.text_keys.delete(1.0, tk.END)
self.log(f"开始生成 {count} 个有效期为 {days} 天的卡密...", self.text_keys)
keys = []
for i in range(count):
key = self.db.generate_key(days)
if key:
keys.append(key)
self.text_keys.insert(tk.END, key + "\n")
self.text_keys.see(tk.END)
self.update_idletasks()
self.log(f"\n成功生成 {len(keys)} 个卡密", self.text_keys)
messagebox.showinfo("成功", f"成功生成 {len(keys)} 个卡密")
# 刷新卡密列表
self.load_all_keys()
except ValueError:
messagebox.showerror("错误", "请输入有效的数字")
except Exception as e:
messagebox.showerror("错误", f"生成卡密失败: {str(e)}")
def copy_keys(self):
"""复制卡密到剪贴板"""
keys = self.text_keys.get(1.0, tk.END).strip()
if keys:
pyperclip.copy(keys)
messagebox.showinfo("成功", "卡密已复制到剪贴板")
else:
messagebox.showinfo("提示", "没有可复制的卡密")
def export_keys(self):
"""导出卡密到文件"""
keys = self.text_keys.get(1.0, tk.END).strip()
if not keys:
messagebox.showinfo("提示", "没有可导出的卡密")
return
file_path = filedialog.asksaveasfilename(
defaultextension=".txt",
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
)
if file_path:
try:
with open(file_path, 'w') as f:
f.write(keys)
messagebox.showinfo("成功", f"卡密已导出到 {file_path}")
except Exception as e:
messagebox.showerror("错误", f"导出失败: {str(e)}")
# EXE加密相关方法
def browse_source_file(self):
"""浏览源文件"""
file_path = filedialog.askopenfilename(
filetypes=[("EXE文件", "*.exe"), ("所有文件", "*.*")]
)
if file_path:
self.entry_source_file.delete(0, tk.END)
self.entry_source_file.insert(0, file_path)
# 自动填充目标文件路径
dir_name, file_name = os.path.split(file_path)
name, ext = os.path.splitext(file_name)
dest_file = os.path.join(dir_name, f"{name}_encrypted{ext}")
self.entry_dest_file.delete(0, tk.END)
self.entry_dest_file.insert(0, dest_file)
# 自动填充验证器路径使用新的validator_wrapper.py
validator_path = os.path.join(os.path.dirname(__file__), "validator_wrapper.py")
self.entry_validator.delete(0, tk.END)
self.entry_validator.insert(0, validator_path)
def browse_dest_file(self):
"""浏览目标文件"""
file_path = filedialog.asksaveasfilename(
defaultextension=".exe",
filetypes=[("EXE文件", "*.exe"), ("文本文件", "*.py"), ("所有文件", "*.*")]
)
if file_path:
self.entry_dest_file.delete(0, tk.END)
self.entry_dest_file.insert(0, file_path)
def browse_validator(self):
"""浏览验证程序"""
file_path = filedialog.askopenfilename(
filetypes=[("Python程序", "*.*"), ("EXE文件", "*.exe"), ("所有文件", "*.*")]
)
if file_path:
self.entry_validator.delete(0, tk.END)
self.entry_validator.insert(0, file_path)
def encrypt_exe(self):
"""加密EXE文件"""
source_path = self.entry_source_file.get()
dest_path = self.entry_dest_file.get()
validator_path = self.entry_validator.get()
# 检查文件路径
if not source_path or not os.path.exists(source_path):
messagebox.showerror("错误", "请选择有效的源文件")
return
if not dest_path:
messagebox.showerror("错误", "请选择目标文件路径")
return
if not validator_path or not os.path.exists(validator_path):
messagebox.showerror("错误", "请选择有效的验证程序")
return
# 检查数据库连接
if not self.db or not self.db.connection.is_connected():
if not self.connect_db():
return
# 执行加密
self.log("开始加密文件...", self.text_encrypt_log)
self.log(f"源文件: {source_path}", self.text_encrypt_log)
try:
encryptor = EXEEncryptor()
success, msg = encryptor.encrypt_file(
source_path,
dest_path,
validator_path,
self.db_config
)
self.log(msg, self.text_encrypt_log)
if success:
self.log(f"加密后的文件已保存到: {dest_path}", self.text_encrypt_log)
messagebox.showinfo("成功", f"文件加密成功\n保存到: {dest_path}")
else:
messagebox.showerror("错误", msg)
except Exception as e:
error_msg = f"加密过程出错: {str(e)}"
self.log(error_msg, self.text_encrypt_log)
messagebox.showerror("错误", error_msg)
# 卡密管理相关方法
def load_all_keys(self):
"""加载所有卡密"""
if not self.db or not self.db.connection.is_connected():
return
# 清空现有列表
for item in self.tree_keys.get_children():
self.tree_keys.delete(item)
try:
keys = self.db.get_all_keys()
for key in keys:
# 格式化日期时间
start_time = key['start_time'].strftime("%Y-%m-%d") if key['start_time'] else ""
end_time = key['end_time'].strftime("%Y-%m-%d") if key['end_time'] else ""
created_at = key['created_at'].strftime("%Y-%m-%d") if key['created_at'] else ""
self.tree_keys.insert("", tk.END, values=(
key['id'],
key['key_code'],
key['machine_code'] or "",
start_time,
end_time,
key['status'],
created_at
))
# 根据状态设置行颜色
item = self.tree_keys.get_children()[-1]
if key['status'] == 'active':
self.tree_keys.item(item, tags=('active',))
elif key['status'] == 'expired':
self.tree_keys.item(item, tags=('expired',))
elif key['status'] == 'banned':
self.tree_keys.item(item, tags=('banned',))
# 设置标签样式
self.tree_keys.tag_configure('active', foreground='green')
self.tree_keys.tag_configure('expired', foreground='gray')
self.tree_keys.tag_configure('banned', foreground='red')
except Exception as e:
print(f"加载卡密列表失败: {e}")
def search_keys(self):
"""搜索卡密"""
if not self.db or not self.db.connection.is_connected():
return
search_text = self.entry_key_search.get().strip().lower()
if not search_text:
self.load_all_keys()
return
# 清空现有列表
for item in self.tree_keys.get_children():
self.tree_keys.delete(item)
try:
keys = self.db.get_all_keys()
for key in keys:
# 检查是否匹配搜索文本
if (search_text in key['key_code'].lower() or
search_text in key['status'].lower() or
(key['machine_code'] and search_text in key['machine_code'].lower())):
# 格式化日期时间
start_time = key['start_time'].strftime("%Y-%m-%d") if key['start_time'] else ""
end_time = key['end_time'].strftime("%Y-%m-%d") if key['end_time'] else ""
created_at = key['created_at'].strftime("%Y-%m-%d") if key['created_at'] else ""
self.tree_keys.insert("", tk.END, values=(
key['id'],
key['key_code'],
key['machine_code'] or "",
start_time,
end_time,
key['status'],
created_at
))
except Exception as e:
print(f"搜索卡密失败: {e}")
def get_selected_key_code(self):
"""获取选中的卡密"""
selected_items = self.tree_keys.selection()
if not selected_items:
messagebox.showinfo("提示", "请先选择一个卡密")
return None
item = selected_items[0]
key_code = self.tree_keys.item(item, "values")[1]
return key_code
def ban_selected_key(self):
"""封禁选中的卡密"""
key_code = self.get_selected_key_code()
if not key_code:
return
if messagebox.askyesno("确认", f"确定要封禁卡密 {key_code} 吗?"):
if self.db.update_key_status(key_code, 'banned'):
messagebox.showinfo("成功", "卡密已封禁")
self.load_all_keys()
else:
messagebox.showerror("错误", "封禁卡密失败")
def unban_selected_key(self):
"""解封选中的卡密"""
key_code = self.get_selected_key_code()
if not key_code:
return
if messagebox.askyesno("确认", f"确定要解封卡密 {key_code} 吗?"):
# 检查原状态是未使用还是已激活
try:
selected_items = self.tree_keys.selection()
item = selected_items[0]
original_status = self.tree_keys.item(item, "values")[5]
new_status = 'unused' if original_status == 'banned' and not self.tree_keys.item(item, "values")[
2] else 'active'
if self.db.update_key_status(key_code, new_status):
messagebox.showinfo("成功", "卡密已解封")
self.load_all_keys()
else:
messagebox.showerror("错误", "解封卡密失败")
except Exception as e:
messagebox.showerror("错误", f"操作失败: {str(e)}")
def release_selected_key(self):
"""释放选中的已使用激活码"""
key_code = self.get_selected_key_code()
if not key_code:
return
# 获取选择项的状态信息
selected_items = self.tree_keys.selection()
if selected_items:
item = selected_items[0]
status = self.tree_keys.item(item, "values")[5]
if status != 'active':
messagebox.showwarning("警告", "只能释放处于'激活'状态的激活码")
return
if messagebox.askyesno("确认释放",
f"确定要释放激活码 '{key_code}' 吗?\n\n释放后:\n1. 该激活码将变为未使用状态\n2. 机器码将被清空\n3. 可以重新在任何机器上使用\n\n此操作不可撤销!"):
if not self.db or not self.db.connection.is_connected():
if not self.connect_db():
return
success, msg = self.db.release_key(key_code)
if success:
messagebox.showinfo("成功", f"激活码 '{key_code}' 已释放成功\n{msg}")
self.load_all_keys() # 刷新列表
else:
messagebox.showerror("失败", msg)
def delete_selected_key(self):
"""删除选中的卡密"""
key_code = self.get_selected_key_code()
if not key_code:
return
if messagebox.askyesno("确认", f"确定要删除卡密 {key_code} 吗?\n此操作不可恢复!"):
# 实际项目中应该实现delete_key方法
messagebox.showinfo("提示", "为安全起见,当前版本不允许删除激活码,建议使用封禁或释放功能")
if __name__ == "__main__":
app = EXEEncryptionTool()
app.mainloop()

38
main.spec Normal file
View File

@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='main',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

656
validator_wrapper.py Normal file
View File

@ -0,0 +1,656 @@
#!/usr/bin/env python3
"""
EXE Wrapper Validator - 外层验证程序
用于验证卡密后启动原始exe文件包含防护措施
已整合自动本地缓存验证 + UI 美化 + 修复本地缓存解密
"""
import os
import sys
import struct
import json
import hashlib
import tempfile
import subprocess
import mysql.connector
from datetime import datetime
import tkinter as tk
from tkinter import messagebox, ttk
import threading
import time
import psutil
import ctypes
import win32con
import os, json, base64
from cryptography.fernet import Fernet # pip install cryptography
from machine_code import get_machine_code
LICENSE_FILE = os.path.join(tempfile.gettempdir(), '.lic_cache')
def _get_fernet() -> Fernet:
"""用机器码作为密钥,保证同一机器才能解密"""
machine_key = get_machine_code().encode()[:32].ljust(32, b'0')
return Fernet(base64.urlsafe_b64encode(machine_key))
def save_license(key: str):
"""加密保存卡密"""
data = {'key': key, 'machine': get_machine_code()}
with open(LICENSE_FILE, 'wb') as f:
f.write(_get_fernet().encrypt(json.dumps(data).encode()))
def load_license() -> str | None:
"""解密读取卡密,若文件不存在/被篡改返回 None"""
try:
with open(LICENSE_FILE, 'rb') as f:
data = json.loads(_get_fernet().decrypt(f.read()).decode())
return data['key'] if data.get('machine') == get_machine_code() else None
except Exception:
return None
# RESOURCE_DATA_PLACEHOLDER
RESOURCE_DATA = None
class EXEWrapperValidator:
"""EXE外层验证器 - 验证卡密后启动原始程序"""
def __init__(self):
self.temp_dir = None
self.original_exe_path = None
self.process_handle = None
self.anti_debug_enabled = True
self.anti_vm_enabled = True
self.process_monitoring = True
def show_license_dialog(self):
"""显示许可证激活对话框(美化版)"""
def style_button(btn, bg_color, hover_color):
def on_enter(e): btn['bg'] = hover_color
def on_leave(e): btn['bg'] = bg_color
btn.bind("<Enter>", on_enter)
btn.bind("<Leave>", on_leave)
def on_activate():
key = key_entry.get().strip()
if not key:
messagebox.showerror("错误", "请输入激活码")
return
activate_btn.config(state=tk.DISABLED, text="验证中...")
status_label.config(text="正在验证激活码...", fg="blue")
root.update()
def validate_thread():
success, msg = self.validate_license(key, self.get_machine_code())
root.after(0, lambda: self.handle_validation_result(success, msg, root, key))
threading.Thread(target=validate_thread, daemon=True).start()
machine_code = self.get_machine_code()
root = tk.Tk()
root.title("软件许可证验证")
root.geometry("520x460")
root.configure(bg="#ffffff")
root.resizable(False, False)
# 居中窗口
root.update_idletasks()
x = (root.winfo_screenwidth() - root.winfo_width()) // 2
y = (root.winfo_screenheight() - root.winfo_height()) // 2
root.geometry(f"520x460+{x}+{y}")
# 标题
tk.Label(root, text="🔐 软件许可证验证",
font=("Microsoft YaHei", 18, "bold"),
bg="#ffffff", fg="#2c3e50").pack(pady=12)
# 机器码框架
machine_frame = tk.Frame(root, bg="#ffffff")
machine_frame.pack(fill=tk.X, padx=30, pady=6)
tk.Label(machine_frame, text="机器码:", bg="#ffffff",
font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
machine_entry = tk.Entry(machine_frame, state="readonly", width=45,
font=("Consolas", 10), bg="#f2f4f7", relief=tk.SOLID, bd=1)
machine_entry.insert(0, machine_code)
machine_entry.pack(fill=tk.X, pady=6)
# 复制机器码按钮
copy_btn = tk.Button(machine_frame, text="复制机器码",
command=lambda: self.copy_to_clipboard(machine_code),
bg="#3498db", fg="white", font=("Microsoft YaHei", 9),
relief=tk.FLAT, padx=8, cursor="hand2")
style_button(copy_btn, "#3498db", "#5dade2")
copy_btn.pack(anchor=tk.E, pady=2)
# 激活码框架
key_frame = tk.Frame(root, bg="#ffffff")
key_frame.pack(fill=tk.X, padx=30, pady=10)
tk.Label(key_frame, text="激活码:", bg="#ffffff",
font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
key_entry = tk.Entry(key_frame, width=40, font=("Consolas", 12),
bg="#fbfbfb", relief=tk.SOLID, bd=2,
highlightcolor="#3498db", highlightthickness=2)
key_entry.pack(fill=tk.X, pady=6)
# 说明文字
info_label = tk.Label(root, text="请输入激活码格式XXXXX-XXXXX-XXXXX-XXXXX",
bg="#ffffff", fg="#7f8c8d", font=("Microsoft YaHei", 9))
info_label.pack(pady=4)
status_label = tk.Label(root, text="", bg="#ffffff", font=("Microsoft YaHei", 9))
status_label.pack(pady=6)
# 按钮框架
button_frame = tk.Frame(root, bg="#ffffff")
button_frame.pack(fill=tk.X, padx=30, pady=12)
activate_btn = tk.Button(button_frame, text="验证并启动", command=on_activate,
bg="#27ae60", fg="white", padx=26, pady=8,
font=("Microsoft YaHei", 11, "bold"),
relief=tk.FLAT, cursor="hand2")
style_button(activate_btn, "#27ae60", "#2ecc71")
activate_btn.pack(side=tk.LEFT, padx=8)
cancel_btn = tk.Button(button_frame, text="退出",
command=lambda: [root.destroy(), sys.exit(0)],
bg="#e74c3c", fg="white", padx=26, pady=8,
font=("Microsoft YaHei", 11, "bold"),
relief=tk.FLAT, cursor="hand2")
style_button(cancel_btn, "#e74c3c", "#ff6b6b")
cancel_btn.pack(side=tk.RIGHT, padx=8)
key_entry.focus()
root.bind('<Return>', lambda e: on_activate())
root.bind('<Escape>', lambda e: [root.destroy(), sys.exit(0)])
root.protocol("WM_DELETE_WINDOW", lambda: [root.destroy(), sys.exit(0)])
root.mainloop()
def handle_validation_result(self, success, msg, root,key):
"""处理验证结果"""
if success:
messagebox.showinfo("验证成功", "激活码验证成功!正在启动程序...")
save_license(key) # <== 新增
root.destroy()
# 提取并运行原始程序
extracted_path = self.extract_original_program()
if extracted_path:
self.launch_program_with_protection(extracted_path)
else:
messagebox.showerror("错误", "无法提取原始程序")
sys.exit(1)
else:
messagebox.showerror("验证失败", msg)
# 重置按钮状态
for widget in root.winfo_children():
if isinstance(widget, tk.Frame):
for child in widget.winfo_children():
if isinstance(child, tk.Button) and "验证" in child.cget("text"):
child.config(state=tk.NORMAL, text="验证并启动")
break
def copy_to_clipboard(self, text):
"""复制文本到剪贴板"""
try:
import pyperclip
pyperclip.copy(text)
messagebox.showinfo("成功", "机器码已复制到剪贴板")
except ImportError:
# 使用tkinter的回退方法
root = tk.Tk()
root.withdraw()
root.clipboard_clear()
root.clipboard_append(text)
root.update()
root.destroy()
messagebox.showinfo("成功", "机器码已复制到剪贴板")
except Exception as e:
messagebox.showwarning("警告", f"无法复制到剪贴板: {str(e)}")
def get_machine_code(self):
"""生成唯一机器码"""
try:
if os.name == 'nt': # Windows
return self.get_windows_machine_code()
else: # Linux/Mac
return self.get_unix_machine_code()
except Exception as e:
# 回退方法
import platform
import uuid
unique = f"{platform.node()}{uuid.getnode()}{platform.processor()}"
return hashlib.md5(unique.encode("utf-8")).hexdigest()[:16].upper()
def get_windows_machine_code(self):
"""获取Windows机器码"""
try:
# 尝试获取主板序列号
try:
result = subprocess.check_output(
'wmic baseboard get serialnumber',
shell=True, stderr=subprocess.STDOUT
).decode().strip()
if "SerialNumber" in result:
serial = result.split("\n")[1].strip()
if serial and serial != "To Be Filled By O.E.M.":
return hashlib.md5(serial.encode()).hexdigest()[:16].upper()
except:
pass
# 尝试获取CPU ID
try:
result = subprocess.check_output(
'wmic cpu get processorid',
shell=True, stderr=subprocess.STDOUT
).decode().strip()
if "ProcessorId" in result:
cpu_id = result.split("\n")[1].strip()
if cpu_id:
return hashlib.md5(cpu_id.encode()).hexdigest()[:16].upper()
except:
pass
# 回退
import platform
import uuid
unique = f"{platform.node()}{uuid.getnode()}"
return hashlib.md5(unique.encode()).hexdigest()[:16].upper()
except Exception as e:
import platform
import uuid
unique = f"{platform.node()}{uuid.getnode()}"
return hashlib.md5(unique.encode()).hexdigest()[:16].upper()
def get_unix_machine_code(self):
"""获取Unix/Linux机器码"""
try:
import subprocess
import uuid
# 尝试获取MAC地址
mac = uuid.getnode()
mac_str = ':'.join(['{:02x}'.format((mac >> elements) & 0xff)
for elements in range(0, 2 * 6, 2)][::-1])
# 尝试获取machine-id或hostname
try:
with open('/etc/machine-id', 'r') as f:
machine_id = f.read().strip()
unique = f"{mac_str}{machine_id}"
except:
import platform
unique = f"{mac_str}{platform.node()}"
return hashlib.md5(unique.encode()).hexdigest()[:16].upper()
except Exception as e:
import platform
import uuid
unique = f"{platform.node()}{uuid.getnode()}"
return hashlib.md5(unique.encode()).hexdigest()[:16].upper()
def load_resource_data(self):
"""从嵌入的 RESOURCE_DATA 中加载原始EXE和配置"""
try:
if RESOURCE_DATA is None:
return False, "未找到嵌入的资源数据", None
# 提取原始exe内容
original_hex = RESOURCE_DATA['original_exe']
original_content = bytes.fromhex(original_hex)
# 提取配置
config = {
'original_size': RESOURCE_DATA['original_size'],
'file_hash': RESOURCE_DATA['file_hash'],
'db_config': RESOURCE_DATA['db_config']
}
return True, config, original_content
except Exception as e:
return False, f"加载资源失败: {str(e)}", None
def validate_license(self, license_key, machine_code):
"""验证许可证"""
try:
# 验证密钥格式
parts = license_key.split('-')
if len(parts) != 4:
return False, "激活码格式错误应为XXXXX-XXXXX-XXXXX-XXXXX"
for part in parts:
if len(part) != 5:
return False, "激活码格式错误每段应为5个字符"
# 加载嵌入的资源数据
success, config, original_content = self.load_resource_data()
if not success:
return False, f"无法读取配置:{config}"
# 提取数据库配置
db_config = config.get('db_config', {})
host = db_config.get('host', 'localhost')
database = db_config.get('database', 'license_system')
user = db_config.get('user', '')
password = db_config.get('password', '')
# 连接MySQL数据库
try:
conn = mysql.connector.connect(
host=host,
user=user,
password=password,
database=database,
connection_timeout=10,
autocommit=True
)
cursor = conn.cursor(dictionary=True)
except mysql.connector.Error as e:
return False, f"数据库连接失败:{str(e)}"
try:
# 检查许可证是否存在且有效
query = "SELECT * FROM license_keys WHERE key_code = %s"
cursor.execute(query, (license_key,))
result = cursor.fetchone()
if not result:
return False, "激活码不存在"
if result['status'] == 'banned':
return False, "激活码已被封禁"
if result['end_time'] < datetime.now():
cursor.execute(
"UPDATE license_keys SET status = 'expired' WHERE key_code = %s",
(license_key,)
)
return False, "激活码已过期"
if result['status'] == 'active':
if result['machine_code'] != machine_code:
return False, f"此激活码已在其他设备上使用设备ID{result['machine_code'][:8]}..."
else:
return True, "激活验证成功"
if result['status'] == 'unused':
cursor.execute("""
UPDATE license_keys
SET status = 'active', machine_code = %s, start_time = NOW()
WHERE key_code = %s AND status = 'unused'
""", (machine_code, license_key))
if cursor.rowcount == 0:
return False, "激活码已被其他用户使用"
return True, "激活成功"
return False, f"激活码状态异常:{result['status']}"
finally:
cursor.close()
conn.close()
except Exception as e:
return False, f"验证过程出错:{str(e)}"
def extract_original_program(self):
"""从嵌入资源中提取原始exe到临时文件"""
try:
success, config, original_content = self.load_resource_data()
if not success:
messagebox.showerror("错误", config)
return None
# 验证完整性
expected_hash = config['file_hash']
actual_hash = hashlib.sha256(original_content).hexdigest()
if expected_hash != actual_hash:
messagebox.showerror("错误", "原始文件校验失败")
return None
# 创建临时文件
temp_dir = tempfile.gettempdir()
temp_file_path = os.path.join(temp_dir, f"original_program_{os.getpid()}.exe")
with open(temp_file_path, 'wb') as f:
f.write(original_content)
# 设置可执行权限
if os.name != 'nt':
os.chmod(temp_file_path, 0o755)
self.original_exe_path = temp_file_path
return temp_file_path
except Exception as e:
messagebox.showerror("错误", f"提取原始程序失败: {str(e)}")
return None
def _simple_decrypt(self, data):
"""简化版XOR解密"""
key = b'EXEProtector#2024'
decrypted = bytearray(data)
key_len = len(key)
for i in range(len(decrypted)):
decrypted[i] ^= key[i % key_len]
return bytes(decrypted)
def _decompress_data(self, data):
"""解压缩数据"""
import zlib
return zlib.decompress(data)
def launch_program_with_protection(self, program_path):
"""启动程序并应用防护措施"""
try:
# 应用防护措施
self.apply_protection_measures()
# 启动程序
if os.name == 'nt': # Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = win32con.SW_SHOW # ✅ 正确来源
process = subprocess.Popen([program_path], startupinfo=startupinfo)
# process = subprocess.Popen([program_path], startupinfo=startupinfo)
self.process_handle = process.pid
# 启动监控线程
if self.process_monitoring:
threading.Thread(target=self.monitor_process,
args=(process.pid,), daemon=True).start()
# 等待程序启动
time.sleep(1)
# 退出验证器
sys.exit(0)
else: # Linux/Mac
subprocess.Popen([program_path])
sys.exit(0)
except Exception as e:
messagebox.showerror("错误", f"启动程序失败: {str(e)}")
sys.exit(1)
def apply_protection_measures(self):
"""应用防护措施"""
if self.anti_debug_enabled:
self.enable_anti_debug()
if self.anti_vm_enabled:
self.enable_anti_vm()
def enable_anti_debug(self):
"""启用反调试保护"""
try:
if os.name == 'nt': # Windows
# 检查调试器
if ctypes.windll.kernel32.IsDebuggerPresent():
sys.exit(0)
# 设置调试标志
ctypes.windll.kernel32.SetProcessDEPPolicy(0x00000001)
except Exception:
pass
def enable_anti_vm(self):
"""启用反虚拟机保护"""
try:
if os.name == 'nt': # Windows
# 检查常见的虚拟机进程
vm_processes = [
'vmsrvc.exe', 'vmusrvc.exe', 'vmtoolsd.exe',
'vboxservice.exe', 'vboxtray.exe', 'vboxcontrol.exe',
'vmwaretray.exe', 'vmwareuser.exe', 'vmusrvc.exe'
]
for proc in psutil.process_iter(['name']):
try:
if proc.info['name'].lower() in vm_processes:
sys.exit(0)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
except Exception:
pass
def monitor_process(self, pid):
"""监控进程状态"""
try:
while True:
time.sleep(5) # 每5秒检查一次
# 检查进程是否还在运行
try:
process = psutil.Process(pid)
if not process.is_running():
break
except psutil.NoSuchProcess:
break
# 检查是否有调试器附加
if self.anti_debug_enabled:
try:
if ctypes.windll.kernel32.IsDebuggerPresent():
process.terminate()
break
except:
pass
except Exception:
pass
finally:
# 清理临时文件
self.cleanup_temp_files()
def cleanup_temp_files(self):
"""清理临时文件"""
try:
if self.original_exe_path and os.path.exists(self.original_exe_path):
os.remove(self.original_exe_path)
except:
pass
import requests, socket, os, json, tempfile, base64, time
from cryptography.fernet import Fernet
CACHE_FILE = LICENSE_FILE
def online_check(host='taiyiagi.xyz', port=3306, timeout=3):
"""简单检测能否连上数据库服务器"""
try:
socket.create_connection((host, port), timeout)
return True
except:
return False
def verify_with_fallback(validator, key):
"""有网:联网复检;无网:本地缓存兜底"""
db_config = validator.load_resource_data()[1]['db_config']
host = db_config.get('host', 'localhost')
port = int(db_config.get('port', 3306))
if online_check(host, port):
# 有网:强制联网验证
return validator.validate_license(key, validator.get_machine_code())
else:
# 无网尝试使用本地缓存LICENSE_FILE
try:
if os.path.exists(LICENSE_FILE):
with open(LICENSE_FILE, 'rb') as f:
data = json.loads(_get_fernet().decrypt(f.read()).decode())
cached_key = data.get('key')
if cached_key:
return validator.validate_license(cached_key, validator.get_machine_code())
except Exception:
pass
return False, "网络不可用且本地缓存失效"
def main():
validator = EXEWrapperValidator()
# 控制台调试模式(方便调试)
if len(sys.argv) > 1 and sys.argv[1] == '--console':
print("调试模式")
machine_code = validator.get_machine_code()
print(f"机器码: {machine_code}")
license_key = input("请输入激活码: ").strip()
if license_key:
success, msg = validator.validate_license(license_key, machine_code)
print(f"验证结果: {success} - {msg}")
if success:
save_license(license_key)
extracted = validator.extract_original_program()
if extracted:
validator.launch_program_with_protection(extracted)
return
# 1) 启动时优先加载本地缓存卡密(自动验证)
cached_key = load_license()
if cached_key:
success, msg = validator.validate_license(cached_key, validator.get_machine_code())
if success:
extracted = validator.extract_original_program()
if extracted:
validator.launch_program_with_protection(extracted)
return
else:
# 本地缓存不可用,删除并提示
try:
os.remove(LICENSE_FILE)
except:
pass
try:
# 尝试用GUI提示如果可用
tmp = tk.Tk(); tmp.withdraw()
messagebox.showwarning("提示", f"本地授权失效:{msg}")
tmp.destroy()
except:
print("本地授权失效:", msg)
# 2) 无有效授权,弹出 GUI 输入框
validator.show_license_dialog()
if __name__ == "__main__":
main()

50
validator_wrapper.spec Normal file
View File

@ -0,0 +1,50 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(
['validator_wrapper.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='validator_wrapper',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='validator_wrapper',
)