license_manager.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. # license_manager.py
  2. import os
  3. import json
  4. import base64
  5. import hashlib
  6. import uuid
  7. import platform
  8. import subprocess
  9. import sys
  10. from datetime import datetime
  11. try:
  12. from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  13. except ImportError:
  14. raise ImportError("请先安装依赖: pip install cryptography")
  15. ENCRYPTION_KEY = b"0123456789abcdef0123456789abcdef"
  16. def get_app_dir():
  17. if getattr(sys, 'frozen', False):
  18. return os.path.dirname(sys.executable)
  19. return os.path.dirname(os.path.abspath(__file__))
  20. LICENSE_FILE = os.path.join(get_app_dir(), "license.lic")
  21. def get_machine_guid():
  22. system = platform.system()
  23. if system == "Windows":
  24. try:
  25. import winreg
  26. with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Cryptography") as key:
  27. return winreg.QueryValueEx(key, "MachineGuid")[0]
  28. except Exception:
  29. pass
  30. elif system == "Linux":
  31. for path in ["/etc/machine-id", "/var/lib/dbus/machine-id"]:
  32. if os.path.exists(path):
  33. with open(path, "r", encoding="utf-8") as f:
  34. return f.read().strip()
  35. elif system == "Darwin":
  36. try:
  37. result = subprocess.run(
  38. ["ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
  39. capture_output=True, text=True
  40. )
  41. for line in result.stdout.split("\n"):
  42. if "IOPlatformUUID" in line:
  43. return line.split('"')[-2]
  44. except Exception:
  45. pass
  46. return str(uuid.getnode())
  47. def get_machine_code():
  48. guid = get_machine_guid()
  49. h = hashlib.sha256()
  50. h.update(b"LicenseSystem")
  51. h.update(guid.encode("utf-8"))
  52. return h.hexdigest()
  53. def decrypt_key(encrypted_key: str):
  54. try:
  55. data = base64.b64decode(encrypted_key)
  56. except Exception:
  57. raise ValueError("Base64 解码失败")
  58. if len(data) < 12:
  59. raise ValueError("密文太短")
  60. aesgcm = AESGCM(ENCRYPTION_KEY)
  61. nonce = data[:12]
  62. ciphertext = data[12:]
  63. try:
  64. plaintext = aesgcm.decrypt(nonce, ciphertext, None)
  65. except Exception:
  66. raise ValueError("解密失败,密钥无效或已被篡改")
  67. try:
  68. return json.loads(plaintext.decode("utf-8"))
  69. except Exception:
  70. raise ValueError("许可证数据解析失败")
  71. def validate_key(key: str):
  72. machine_code = get_machine_code()
  73. license_info = decrypt_key(key)
  74. if license_info.get("machine_code") != machine_code:
  75. raise ValueError("机器码不匹配,该密钥无法在本机使用")
  76. expire_str = license_info.get("expire_time")
  77. if not expire_str:
  78. raise ValueError("许可证信息不完整")
  79. expire_time = datetime.fromisoformat(expire_str)
  80. if datetime.now() > expire_time:
  81. raise ValueError("许可证已过期")
  82. return license_info
  83. def save_license(license_info: dict):
  84. with open(LICENSE_FILE, "w", encoding="utf-8") as f:
  85. json.dump(license_info, f, indent=2, ensure_ascii=False)
  86. def load_license():
  87. if not os.path.exists(LICENSE_FILE):
  88. return None
  89. try:
  90. with open(LICENSE_FILE, "r", encoding="utf-8") as f:
  91. return json.load(f)
  92. except Exception:
  93. return None
  94. def verify_license(root):
  95. """
  96. 直接在传入的 root 窗口上显示验证界面。
  97. 验证通过返回 True,关闭窗口返回 False。
  98. """
  99. import tkinter as tk
  100. from tkinter import messagebox
  101. # 1. 先检查本地缓存,如果有效直接通过
  102. cached = load_license()
  103. if cached:
  104. try:
  105. if cached.get("machine_code") != get_machine_code():
  106. raise ValueError("机器码不匹配")
  107. if datetime.now() > datetime.fromisoformat(cached.get("expire_time")):
  108. raise ValueError("许可证已过期")
  109. return True
  110. except Exception:
  111. if os.path.exists(LICENSE_FILE):
  112. os.remove(LICENSE_FILE)
  113. # 2. 没有缓存或缓存无效,显示验证界面
  114. machine_code = get_machine_code()
  115. # 清空 root 里所有内容(防止有残留)
  116. for widget in root.winfo_children():
  117. widget.destroy()
  118. # 设置验证窗口样式
  119. root.title("软件激活")
  120. root.geometry("420x280")
  121. root.resizable(False, False)
  122. # 窗口居中
  123. root.update_idletasks()
  124. w, h = 420, 280
  125. x = (root.winfo_screenwidth() - w) // 2
  126. y = (root.winfo_screenheight() - h) // 2
  127. root.geometry(f"{w}x{h}+{x}+{y}")
  128. # 机器码(可复制)
  129. tk.Label(root, text="您的机器码(可复制):", font=("微软雅黑", 10)).pack(pady=(15, 5))
  130. mc_entry = tk.Entry(root, font=("Consolas", 11), justify="center", width=50)
  131. mc_entry.insert(0, machine_code)
  132. mc_entry.config(state="readonly")
  133. mc_entry.pack(padx=20, pady=5)
  134. tk.Label(root, text="请将机器码发给管理员获取密钥", font=("微软雅黑", 9), fg="gray").pack(pady=5)
  135. # 密钥输入
  136. tk.Label(root, text="许可证密钥:", font=("微软雅黑", 10)).pack(pady=(10, 5))
  137. key_entry = tk.Entry(root, font=("Consolas", 11), show="*", width=50)
  138. key_entry.pack(padx=20, pady=5)
  139. key_entry.focus()
  140. error_label = tk.Label(root, text="", font=("微软雅黑", 9), fg="red")
  141. error_label.pack(pady=5)
  142. # 结果标志
  143. result = [False]
  144. def on_verify():
  145. key = key_entry.get().strip()
  146. if not key:
  147. error_label.config(text="密钥不能为空")
  148. return
  149. try:
  150. info = validate_key(key)
  151. save_license(info)
  152. expire = datetime.fromisoformat(info["expire_time"])
  153. days_left = (expire - datetime.now()).days
  154. messagebox.showinfo(
  155. "激活成功",
  156. f"✓ 验证通过!\n有效期至:{expire.strftime('%Y-%m-%d %H:%M:%S')}\n剩余天数:{days_left} 天"
  157. )
  158. result[0] = True
  159. root.quit() # 退出事件循环,不销毁窗口
  160. except Exception as e:
  161. error_label.config(text=str(e))
  162. key_entry.delete(0, tk.END)
  163. key_entry.focus()
  164. def on_close():
  165. result[0] = False
  166. root.quit()
  167. # 按钮
  168. btn_frame = tk.Frame(root)
  169. btn_frame.pack(pady=15)
  170. tk.Button(btn_frame, text="确认激活", command=on_verify, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
  171. tk.Button(btn_frame, text="关闭", command=on_close, width=12, font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=10)
  172. # 回车确认,X 按钮关闭
  173. root.bind("<Return>", lambda e: on_verify())
  174. root.protocol("WM_DELETE_WINDOW", on_close)
  175. # 显示窗口并阻塞等待用户操作
  176. root.deiconify()
  177. root.mainloop()
  178. # 清理验证界面,把 root 交还给主程序
  179. for widget in root.winfo_children():
  180. widget.destroy()
  181. return result[0]