enterprise-saa-s-dashboard-.../python_backend/services.py

113 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"
)