diff --git a/main.py b/main.py new file mode 100644 index 0000000..e05dd63 --- /dev/null +++ b/main.py @@ -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() \ No newline at end of file diff --git a/main.spec b/main.spec new file mode 100644 index 0000000..1ccbe3c --- /dev/null +++ b/main.spec @@ -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, +) diff --git a/validator_wrapper.py b/validator_wrapper.py new file mode 100644 index 0000000..4323511 --- /dev/null +++ b/validator_wrapper.py @@ -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("", on_enter) + btn.bind("", 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('', lambda e: on_activate()) + root.bind('', 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() diff --git a/validator_wrapper.spec b/validator_wrapper.spec new file mode 100644 index 0000000..d19e71b --- /dev/null +++ b/validator_wrapper.spec @@ -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', +)