375 lines
12 KiB
Python
375 lines
12 KiB
Python
|
|
"""
|
|||
|
|
安全的验证器 - 通过API验证,不包含数据库密码
|
|||
|
|
作者:太一
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import json
|
|||
|
|
import hashlib
|
|||
|
|
import tempfile
|
|||
|
|
import subprocess
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import messagebox
|
|||
|
|
import threading
|
|||
|
|
import time
|
|||
|
|
import hmac
|
|||
|
|
import base64
|
|||
|
|
from datetime import datetime, timedelta
|
|||
|
|
from cryptography.fernet import Fernet
|
|||
|
|
import requests
|
|||
|
|
|
|||
|
|
# ========== 配置区(这些可以公开) ==========
|
|||
|
|
API_BASE_URL = "https://your-server.com/api" # 替换为你的服务器地址
|
|||
|
|
API_KEY = "your-secure-api-key-here" # 从环境变量读取更安全
|
|||
|
|
SOFTWARE_NAME = "SOFTWARE_NAME_PLACEHOLDER" # 打包时替换
|
|||
|
|
|
|||
|
|
# 本地缓存配置
|
|||
|
|
CACHE_FILE = os.path.join(tempfile.gettempdir(), '.lic_cache')
|
|||
|
|
CACHE_VALIDITY_HOURS = 24 # 缓存有效期
|
|||
|
|
|
|||
|
|
# ========== 机器码生成 ==========
|
|||
|
|
def get_machine_code():
|
|||
|
|
"""生成机器码(简化且稳定的版本)"""
|
|||
|
|
import platform
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if platform.system() == 'Windows':
|
|||
|
|
# 优先使用主板序列号
|
|||
|
|
try:
|
|||
|
|
import subprocess
|
|||
|
|
result = subprocess.check_output(
|
|||
|
|
'wmic baseboard get serialnumber',
|
|||
|
|
shell=True, stderr=subprocess.DEVNULL, timeout=5
|
|||
|
|
).decode().strip()
|
|||
|
|
|
|||
|
|
lines = [line.strip() for line in result.split('\n') if line.strip()]
|
|||
|
|
if len(lines) > 1 and lines[1] not in ['To Be Filled By O.E.M.', '']:
|
|||
|
|
return hashlib.md5(lines[1].encode()).hexdigest()[:16].upper()
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# 备用方案:使用MAC地址
|
|||
|
|
mac = uuid.getnode()
|
|||
|
|
mac_str = ':'.join(['{:02x}'.format((mac >> i) & 0xff) for i in range(0, 48, 8)][::-1])
|
|||
|
|
return hashlib.md5(mac_str.encode()).hexdigest()[:16].upper()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
# 最终备用
|
|||
|
|
fallback = f"{platform.node()}{uuid.getnode()}"
|
|||
|
|
return hashlib.md5(fallback.encode()).hexdigest()[:16].upper()
|
|||
|
|
|
|||
|
|
# ========== 加密缓存 ==========
|
|||
|
|
def _get_cache_key():
|
|||
|
|
"""获取缓存加密密钥(基于机器码)"""
|
|||
|
|
machine_key = get_machine_code().encode()[:32].ljust(32, b'0')
|
|||
|
|
return base64.urlsafe_b64encode(machine_key)
|
|||
|
|
|
|||
|
|
def save_cache(license_key, token, expires_at):
|
|||
|
|
"""保存验证缓存"""
|
|||
|
|
try:
|
|||
|
|
data = {
|
|||
|
|
'license_key': license_key,
|
|||
|
|
'token': token,
|
|||
|
|
'expires_at': expires_at,
|
|||
|
|
'machine_code': get_machine_code()
|
|||
|
|
}
|
|||
|
|
fernet = Fernet(_get_cache_key())
|
|||
|
|
encrypted = fernet.encrypt(json.dumps(data).encode())
|
|||
|
|
with open(CACHE_FILE, 'wb') as f:
|
|||
|
|
f.write(encrypted)
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"保存缓存失败: {e}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def load_cache():
|
|||
|
|
"""加载验证缓存"""
|
|||
|
|
try:
|
|||
|
|
if not os.path.exists(CACHE_FILE):
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
with open(CACHE_FILE, 'rb') as f:
|
|||
|
|
encrypted = f.read()
|
|||
|
|
|
|||
|
|
fernet = Fernet(_get_cache_key())
|
|||
|
|
decrypted = fernet.decrypt(encrypted)
|
|||
|
|
data = json.loads(decrypted.decode())
|
|||
|
|
|
|||
|
|
# 验证机器码
|
|||
|
|
if data.get('machine_code') != get_machine_code():
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
# 验证过期时间
|
|||
|
|
expires_at = datetime.fromisoformat(data.get('expires_at'))
|
|||
|
|
if datetime.now() > expires_at:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"加载缓存失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def clear_cache():
|
|||
|
|
"""清除缓存"""
|
|||
|
|
try:
|
|||
|
|
if os.path.exists(CACHE_FILE):
|
|||
|
|
os.remove(CACHE_FILE)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ========== API通信 ==========
|
|||
|
|
def create_signature(license_key, machine_code, software_name):
|
|||
|
|
"""创建请求签名"""
|
|||
|
|
message = f"{license_key}{machine_code}{software_name}"
|
|||
|
|
return hmac.new(API_KEY.encode(), message.encode(), hashlib.sha256).hexdigest()
|
|||
|
|
|
|||
|
|
def validate_license_online(license_key, machine_code):
|
|||
|
|
"""在线验证卡密"""
|
|||
|
|
try:
|
|||
|
|
signature = create_signature(license_key, machine_code, SOFTWARE_NAME)
|
|||
|
|
|
|||
|
|
response = requests.post(
|
|||
|
|
f"{API_BASE_URL}/validate",
|
|||
|
|
json={
|
|||
|
|
'license_key': license_key,
|
|||
|
|
'machine_code': machine_code,
|
|||
|
|
'software_name': SOFTWARE_NAME,
|
|||
|
|
'signature': signature
|
|||
|
|
},
|
|||
|
|
headers={'X-API-Key': API_KEY},
|
|||
|
|
timeout=10
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if response.status_code == 200:
|
|||
|
|
data = response.json()
|
|||
|
|
if data.get('success'):
|
|||
|
|
# 保存缓存
|
|||
|
|
save_cache(
|
|||
|
|
license_key,
|
|||
|
|
data.get('token', ''),
|
|||
|
|
data.get('expires_at', (datetime.now() + timedelta(hours=24)).isoformat())
|
|||
|
|
)
|
|||
|
|
return True, data.get('message', '验证成功')
|
|||
|
|
else:
|
|||
|
|
return False, data.get('message', '验证失败')
|
|||
|
|
else:
|
|||
|
|
return False, f"服务器错误: {response.status_code}"
|
|||
|
|
|
|||
|
|
except requests.exceptions.ConnectionError:
|
|||
|
|
return False, "无法连接到服务器,请检查网络"
|
|||
|
|
except requests.exceptions.Timeout:
|
|||
|
|
return False, "服务器响应超时"
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"验证过程出错: {str(e)}"
|
|||
|
|
|
|||
|
|
def validate_license_offline(cache_data):
|
|||
|
|
"""离线验证(使用缓存)"""
|
|||
|
|
try:
|
|||
|
|
# 验证token(可选:发送到服务器验证)
|
|||
|
|
token = cache_data.get('token', '')
|
|||
|
|
if not token:
|
|||
|
|
return False, "缓存数据不完整"
|
|||
|
|
|
|||
|
|
# 这里可以添加本地token验证逻辑
|
|||
|
|
# 或者在有网络时向服务器验证token
|
|||
|
|
|
|||
|
|
return True, "离线验证成功(使用缓存)"
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"离线验证失败: {str(e)}"
|
|||
|
|
|
|||
|
|
# ========== 资源解密 ==========
|
|||
|
|
# ENCRYPTED_RESOURCE_PLACEHOLDER
|
|||
|
|
ENCRYPTED_RESOURCE = None
|
|||
|
|
|
|||
|
|
def decrypt_resource():
|
|||
|
|
"""解密嵌入的资源"""
|
|||
|
|
try:
|
|||
|
|
if not ENCRYPTED_RESOURCE:
|
|||
|
|
return None, "未找到嵌入资源"
|
|||
|
|
|
|||
|
|
from cryptography.fernet import Fernet
|
|||
|
|
import base64
|
|||
|
|
|
|||
|
|
# 重建完整密钥
|
|||
|
|
# key_hint包含前半部分,key_part2包含后半部分
|
|||
|
|
key_part1_bytes = base64.b64decode(ENCRYPTED_RESOURCE['key_hint'].encode())
|
|||
|
|
key_part2_bytes = base64.b64decode(ENCRYPTED_RESOURCE['key_part2'].encode())
|
|||
|
|
|
|||
|
|
# 合并密钥
|
|||
|
|
full_key = key_part1_bytes + key_part2_bytes
|
|||
|
|
|
|||
|
|
# 解密
|
|||
|
|
fernet = Fernet(full_key)
|
|||
|
|
encrypted_data = base64.b64decode(ENCRYPTED_RESOURCE['data'].encode())
|
|||
|
|
decrypted = fernet.decrypt(encrypted_data)
|
|||
|
|
|
|||
|
|
# 验证哈希
|
|||
|
|
if hashlib.sha256(decrypted).hexdigest() != ENCRYPTED_RESOURCE['hash']:
|
|||
|
|
return None, "文件完整性校验失败"
|
|||
|
|
|
|||
|
|
return decrypted, "解密成功"
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return None, f"解密失败: {str(e)}"
|
|||
|
|
|
|||
|
|
def extract_and_run():
|
|||
|
|
"""提取并运行原始程序"""
|
|||
|
|
try:
|
|||
|
|
exe_content, msg = decrypt_resource()
|
|||
|
|
if not exe_content:
|
|||
|
|
return False, msg
|
|||
|
|
|
|||
|
|
# 写入临时文件
|
|||
|
|
temp_path = os.path.join(tempfile.gettempdir(), f"app_{os.getpid()}.exe")
|
|||
|
|
with open(temp_path, 'wb') as f:
|
|||
|
|
f.write(exe_content)
|
|||
|
|
|
|||
|
|
# 设置权限
|
|||
|
|
if hasattr(os, 'chmod'):
|
|||
|
|
os.chmod(temp_path, 0o755)
|
|||
|
|
|
|||
|
|
# 启动程序
|
|||
|
|
if os.name == 'nt':
|
|||
|
|
subprocess.Popen([temp_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
|
|||
|
|
else:
|
|||
|
|
subprocess.Popen([temp_path])
|
|||
|
|
|
|||
|
|
time.sleep(2) # 等待程序启动
|
|||
|
|
|
|||
|
|
return True, "程序启动成功"
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return False, f"启动失败: {str(e)}"
|
|||
|
|
|
|||
|
|
# ========== GUI ==========
|
|||
|
|
def show_activation_dialog():
|
|||
|
|
"""显示激活对话框"""
|
|||
|
|
|
|||
|
|
def on_activate():
|
|||
|
|
license_key = entry_key.get().strip()
|
|||
|
|
if not license_key:
|
|||
|
|
messagebox.showerror("错误", "请输入激活码")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
btn_activate.config(state=tk.DISABLED, text="验证中...")
|
|||
|
|
status_label.config(text="正在验证激活码...", fg="blue")
|
|||
|
|
root.update()
|
|||
|
|
|
|||
|
|
def validate_thread():
|
|||
|
|
machine_code = get_machine_code()
|
|||
|
|
success, msg = validate_license_online(license_key, machine_code)
|
|||
|
|
|
|||
|
|
root.after(0, lambda: handle_result(success, msg))
|
|||
|
|
|
|||
|
|
threading.Thread(target=validate_thread, daemon=True).start()
|
|||
|
|
|
|||
|
|
def handle_result(success, msg):
|
|||
|
|
if success:
|
|||
|
|
messagebox.showinfo("成功", "激活成功!正在启动程序...")
|
|||
|
|
root.destroy()
|
|||
|
|
|
|||
|
|
# 启动程序
|
|||
|
|
ok, err = extract_and_run()
|
|||
|
|
if ok:
|
|||
|
|
sys.exit(0)
|
|||
|
|
else:
|
|||
|
|
messagebox.showerror("错误", err)
|
|||
|
|
sys.exit(1)
|
|||
|
|
else:
|
|||
|
|
messagebox.showerror("失败", msg)
|
|||
|
|
btn_activate.config(state=tk.NORMAL, text="验证并启动")
|
|||
|
|
status_label.config(text="", fg="black")
|
|||
|
|
|
|||
|
|
machine_code = get_machine_code()
|
|||
|
|
|
|||
|
|
root = tk.Tk()
|
|||
|
|
root.title("软件激活验证")
|
|||
|
|
root.geometry("500x400")
|
|||
|
|
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"500x400+{x}+{y}")
|
|||
|
|
|
|||
|
|
# 标题
|
|||
|
|
tk.Label(root, text="🔐 软件许可证验证",
|
|||
|
|
font=("Microsoft YaHei", 16, "bold")).pack(pady=20)
|
|||
|
|
|
|||
|
|
# 机器码
|
|||
|
|
frame_machine = tk.Frame(root)
|
|||
|
|
frame_machine.pack(fill=tk.X, padx=30, pady=10)
|
|||
|
|
tk.Label(frame_machine, text="机器码:", font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
|
|||
|
|
entry_machine = tk.Entry(frame_machine, state="readonly", font=("Consolas", 10))
|
|||
|
|
entry_machine.insert(0, machine_code)
|
|||
|
|
entry_machine.pack(fill=tk.X, pady=5)
|
|||
|
|
|
|||
|
|
def copy_machine_code():
|
|||
|
|
root.clipboard_clear()
|
|||
|
|
root.clipboard_append(machine_code)
|
|||
|
|
messagebox.showinfo("成功", "机器码已复制")
|
|||
|
|
|
|||
|
|
tk.Button(frame_machine, text="复制机器码", command=copy_machine_code).pack(anchor=tk.E)
|
|||
|
|
|
|||
|
|
# 激活码
|
|||
|
|
frame_key = tk.Frame(root)
|
|||
|
|
frame_key.pack(fill=tk.X, padx=30, pady=10)
|
|||
|
|
tk.Label(frame_key, text="激活码:", font=("Microsoft YaHei", 10, "bold")).pack(anchor=tk.W)
|
|||
|
|
entry_key = tk.Entry(frame_key, font=("Consolas", 12))
|
|||
|
|
entry_key.pack(fill=tk.X, pady=5)
|
|||
|
|
entry_key.focus()
|
|||
|
|
|
|||
|
|
# 状态
|
|||
|
|
status_label = tk.Label(root, text="", font=("Microsoft YaHei", 9))
|
|||
|
|
status_label.pack(pady=10)
|
|||
|
|
|
|||
|
|
# 按钮
|
|||
|
|
frame_buttons = tk.Frame(root)
|
|||
|
|
frame_buttons.pack(pady=20)
|
|||
|
|
|
|||
|
|
btn_activate = tk.Button(frame_buttons, text="验证并启动", command=on_activate,
|
|||
|
|
bg="#27ae60", fg="white", font=("Microsoft YaHei", 11, "bold"),
|
|||
|
|
padx=20, pady=8)
|
|||
|
|
btn_activate.pack(side=tk.LEFT, padx=10)
|
|||
|
|
|
|||
|
|
tk.Button(frame_buttons, text="退出", command=sys.exit,
|
|||
|
|
bg="#e74c3c", fg="white", font=("Microsoft YaHei", 11, "bold"),
|
|||
|
|
padx=20, pady=8).pack(side=tk.LEFT, padx=10)
|
|||
|
|
|
|||
|
|
root.bind('<Return>', lambda e: on_activate())
|
|||
|
|
root.protocol("WM_DELETE_WINDOW", sys.exit)
|
|||
|
|
root.mainloop()
|
|||
|
|
|
|||
|
|
# ========== 主函数 ==========
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
|
|||
|
|
# 1. 尝试加载缓存
|
|||
|
|
cache_data = load_cache()
|
|||
|
|
if cache_data:
|
|||
|
|
print("找到有效缓存,尝试离线验证...")
|
|||
|
|
success, msg = validate_license_offline(cache_data)
|
|||
|
|
if success:
|
|||
|
|
print(f"离线验证成功: {msg}")
|
|||
|
|
ok, err = extract_and_run()
|
|||
|
|
if ok:
|
|||
|
|
sys.exit(0)
|
|||
|
|
else:
|
|||
|
|
print(f"启动失败: {err}")
|
|||
|
|
clear_cache()
|
|||
|
|
else:
|
|||
|
|
print(f"离线验证失败: {msg}")
|
|||
|
|
clear_cache()
|
|||
|
|
|
|||
|
|
# 2. 需要在线验证
|
|||
|
|
show_activation_dialog()
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
main()
|
|||
|
|
|