""" services.py 业务服务模块:封装授权文件加密解密、核心逻辑。 加密方案说明(简单但有效): 1. 将授权字典序列化为 JSON 字符串 2. 使用 XOR 异或加密(密钥混合 SN,实现一机一密) 3. 将加密后的字节进行 Base64 编码,方便传输 为什么用 XOR + SN 混合密钥: - 轻量级,不依赖额外加密库 - 绑定设备 SN,即使授权文件被拷贝到其他设备也无法解密 - 对于 Demo 和内部系统足够安全;生产环境可替换为 AES """ import json import base64 from datetime import datetime, timedelta from typing import Optional from models import LicenseData def _derive_key(device_sn: str) -> bytes: """ 根据设备 SN 派生加密密钥。 将 SN 字符串的每个字符 ASCII 码叠加后,扩展为 32 字节密钥。 这样每台设备的密钥都不同,实现"一机一密"。 """ seed = sum(ord(c) for c in device_sn) % 256 # 用 seed 生成 32 字节的密钥序列 key = bytes((seed + i * 7) % 256 for i in range(32)) return key def encrypt_license(license_data: LicenseData) -> str: """ 加密授权文件: LicenseData → JSON 字符串 → XOR 加密 → Base64 编码 参数: license_data: 明文授权数据对象 返回: base64 编码的加密字符串,可直接写入文件或传给 APP """ # 1. 转为 JSON 字节 json_bytes = license_data.model_dump_json().encode("utf-8") # 2. 派生密钥 key = _derive_key(license_data.device_sn) # 3. XOR 循环加密:密文 = 明文 ^ 密钥(循环使用) encrypted = bytearray() for i, byte in enumerate(json_bytes): encrypted.append(byte ^ key[i % len(key)]) # 4. Base64 编码,变成可打印字符串 return base64.b64encode(encrypted).decode("ascii") def decrypt_license(encrypted_b64: str, device_sn: str) -> Optional[dict]: """ 解密授权文件: Base64 解码 → XOR 解密 → JSON 反序列化为字典 参数: encrypted_b64: encrypt_license 返回的 Base64 字符串 device_sn: 设备 SN(用于派生解密密钥,必须与加密时一致) 返回: 解密后的授权字典;如果 SN 不匹配或数据损坏则返回 None """ try: # 1. Base64 解码 encrypted = base64.b64decode(encrypted_b64) # 2. 派生相同密钥 key = _derive_key(device_sn) # 3. XOR 解密(XOR 的逆运算还是 XOR) decrypted = bytearray() for i, byte in enumerate(encrypted): decrypted.append(byte ^ key[i % len(key)]) # 4. JSON 反序列化 return json.loads(decrypted.decode("utf-8")) except Exception: # 解密失败(SN 不匹配、Base64 错误、JSON 损坏等) return None def build_license_data( device_sn: str, modules: list[str], valid_days: int = 365 ) -> LicenseData: """ 构造明文授权数据对象。 根据当前时间计算有效期截止时间。 """ now = datetime.utcnow() valid_until = now + timedelta(days=valid_days) return LicenseData( version="1.0", device_sn=device_sn, generated_at=now.isoformat() + "Z", valid_until=valid_until.isoformat() + "Z", modules=modules, status="active" )