| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- # license_manager.py
- import os
- import json
- import base64
- import hashlib
- import uuid
- import platform
- import subprocess
- import sys
- from datetime import datetime
- try:
- from cryptography.hazmat.primitives.ciphers.aead import AESGCM
- except ImportError:
- raise ImportError("请先安装依赖: pip install cryptography")
- ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef"
- def get_app_dir():
- if getattr(sys, 'frozen', False):
- return os.path.dirname(sys.executable)
- return os.path.dirname(os.path.abspath(__file__))
- LICENSE_FILE = os.path.join(get_app_dir(), "license.lic")
- def get_machine_guid():
- system = platform.system()
- if system == "Windows":
- try:
- import winreg
- with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key:
- return winreg.QueryValueEx(key, "MachineGuid")[0]
- except Exception:
- pass
- elif system == "Linux":
- for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
- if os.path.exists(path):
- with open(path, "r", encoding="utf-8") as f:
- return f.read().strip()
- elif system == "Darwin":
- try:
- result = subprocess.run(
- ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
- capture_output=True, text=True
- )
- for line in result.stdout.split("\n"):
- if "IOPlatformUUID" in line:
- return line.split('"')[-2]
- except Exception:
- pass
- return str(uuid.getnode())
- def get_machine_code():
- guid = get_machine_guid()
- h = hashlib.sha256()
- h.update(b"LicenseSystem")
- h.update(guid.encode("utf-8"))
- return h.hexdigest()
- def decrypt_key(encrypted_key: str):
- try:
- data = base64.b64decode(encrypted_key)
- except Exception:
- raise ValueError("Base64 解码失败")
- if len(data) < 12:
- raise ValueError("密文太短")
- aesgcm = AESGCM(ENCRYPTION_KEY)
- nonce = data[:12]
- ciphertext = data[12:]
- try:
- plaintext = aesgcm.decrypt(nonce, ciphertext, None)
- except Exception:
- raise ValueError("解密失败,密钥无效或已被篡改")
- try:
- return json.loads(plaintext.decode("utf-8"))
- except Exception:
- raise ValueError("许可证数据解析失败")
- def validate_key(key: str):
- machine_code = get_machine_code()
- license_info = decrypt_key(key)
- if license_info.get("machine_code") != machine_code:
- raise ValueError("机器码不匹配,该密钥无法在本机使用")
- expire_str = license_info.get("expire_time")
- if not expire_str:
- raise ValueError("许可证信息不完整")
- expire_time = datetime.fromisoformat(expire_str)
- if datetime.now() > expire_time:
- raise ValueError("许可证已过期")
- return license_info
- def save_license(license_info: dict):
- with open(LICENSE_FILE, "w", encoding="utf-8") as f:
- json.dump(license_info, f, indent=2, ensure_ascii=False)
- def load_license():
- if not os.path.exists(LICENSE_FILE):
- return None
- try:
- with open(LICENSE_FILE, "r", encoding="utf-8") as f:
- return json.load(f)
- except Exception:
- return None
- def verify_license(root):
- """
- 直接在传入的 root 窗口上显示验证界面。
- 验证通过返回 True,关闭窗口返回 False。
- """
- import tkinter as tk
- from tkinter import messagebox
- # 1. 先检查本地缓存,如果有效直接通过
- cached = load_license()
- if cached:
- try:
- if cached.get("machine_code") != get_machine_code():
- raise ValueError("机器码不匹配")
- if datetime.now() > datetime.fromisoformat(cached.get("expire_time")):
- raise ValueError("许可证已过期")
- return True
- except Exception:
- if os.path.exists(LICENSE_FILE):
- os.remove(LICENSE_FILE)
- # 2. 没有缓存或缓存无效,显示验证界面
- machine_code = get_machine_code()
- # 清空 root 里所有内容(防止有残留)
- for widget in root.winfo_children():
- widget.destroy()
- # 设置验证窗口样式
- root.title("软件激活")
- root.geometry("420x280")
- root.resizable(False, False)
- # 窗口居中
- root.update_idletasks()
- w, h = 420, 280
- x = (root.winfo_screenwidth() - w) // 2
- y = (root.winfo_screenheight() - h) // 2
- root.geometry(f"{w}x{h}+{x}+{y}")
- # 机器码(可复制)
- tk.Label(root, text="您的机器码(可复制):", font=("微软雅黑", 10)).pack(pady=(15, 5))
- mc_entry = tk.Entry(root, font=("Consolas", 11), justify="center", width=50)
- mc_entry.insert(0, machine_code)
- mc_entry.config(state="readonly")
- mc_entry.pack(padx=20, pady=5)
- tk.Label(root, text="请将机器码发给管理员获取密钥", font=("微软雅黑", 9), fg="gray").pack(pady=5)
- # 密钥输入
- tk.Label(root, text="许可证密钥:", font=("微软雅黑", 10)).pack(pady=(10, 5))
- key_entry = tk.Entry(root, font=("Consolas", 11), show="*", width=50)
- key_entry.pack(padx=20, pady=5)
- key_entry.focus()
- error_label = tk.Label(root, text="", font=("微软雅黑", 9), fg="red")
- error_label.pack(pady=5)
- # 结果标志
- result = [False]
- def on_verify():
- key = key_entry.get().strip()
- if not key:
- error_label.config(text="密钥不能为空")
- return
- try:
- info = validate_key(key)
- save_license(info)
- expire = datetime.fromisoformat(info["expire_time"])
- days_left = (expire - datetime.now()).days
- messagebox.showinfo(
- "激活成功",
- f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天"
- )
- result[0] = True
- root.quit() # 退出事件循环,不销毁窗口
- except Exception as e:
- error_label.config(text=str(e))
- key_entry.delete(0, tk.END)
- key_entry.focus()
- def on_close():
- result[0] = False
- root.quit()
- # 按钮
- btn_frame = tk.Frame(root)
- btn_frame.pack(pady=15)
- tk.Button(btn_frame, text="确认激活", command=on_verify, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
- tk.Button(btn_frame, text="关闭", command=on_close, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
- # 回车确认,X 按钮关闭
- root.bind("<Return>", lambda e: on_verify())
- root.protocol("WM_DELETE_WINDOW", on_close)
- # 显示窗口并阻塞等待用户操作
- root.deiconify()
- root.mainloop()
- # 清理验证界面,把 root 交还给主程序
- for widget in root.winfo_children():
- widget.destroy()
- return result[0]
|