encrypt.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. """
  2. RSA+AES混合加密工具 - 支持密钥版本管理
  3. 支持生成带签名的安全响应
  4. """
  5. import base64
  6. import json
  7. import time
  8. import hmac
  9. import hashlib
  10. import secrets
  11. from datetime import datetime
  12. from typing import Dict, Any, Optional, Tuple
  13. from sqlalchemy.orm import Session
  14. from cryptography.hazmat.primitives import serialization
  15. from cryptography.hazmat.primitives.asymmetric import rsa, padding
  16. from cryptography.hazmat.primitives import hashes
  17. from cryptography.hazmat.backends import default_backend
  18. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  19. from cryptography.hazmat.primitives.padding import PKCS7
  20. from app.config import settings
  21. from app.core.logger import logger
  22. from app.database import get_db
  23. from app.models.api_key import APIKey
  24. from app.utils.timestamp_validator import TimestampValidator
  25. from app.utils.timezone import beijing_now_naive
  26. class RSAEncryptor:
  27. """RSA+AES混合加密器 - 支持多版本密钥"""
  28. def __init__(self):
  29. self.private_key: Optional[rsa.RSAPrivateKey] = None
  30. self.public_key: Optional[rsa.RSAPublicKey] = None
  31. self.current_key_version: int = 0
  32. self.key_cache: Dict[int, Tuple[str, str]] = {} # version -> (private_key, public_key)
  33. def generate_key_pair(self) -> tuple[str, str]:
  34. """生成密钥对"""
  35. private_key = rsa.generate_private_key(
  36. public_exponent=65537,
  37. key_size=2048,
  38. backend=default_backend()
  39. )
  40. public_key = private_key.public_key()
  41. # 序列化为PEM格式
  42. private_pem = private_key.private_bytes(
  43. encoding=serialization.Encoding.PEM,
  44. format=serialization.PrivateFormat.PKCS8,
  45. encryption_algorithm=serialization.NoEncryption()
  46. ).decode('utf-8')
  47. public_pem = public_key.public_bytes(
  48. encoding=serialization.Encoding.PEM,
  49. format=serialization.PublicFormat.SubjectPublicKeyInfo
  50. ).decode('utf-8')
  51. return private_pem, public_pem
  52. def load_private_key(self, private_key_pem: str, key_version: int = 0):
  53. """加载私钥"""
  54. try:
  55. self.private_key = serialization.load_pem_private_key(
  56. private_key_pem.encode(),
  57. password=None,
  58. backend=default_backend()
  59. )
  60. self.public_key = self.private_key.public_key()
  61. self.current_key_version = key_version
  62. logger.info(f"私钥加载成功,版本: {key_version}")
  63. except Exception as e:
  64. logger.exception("私钥加载失败")
  65. raise
  66. def load_key_by_version(self, db: Session, key_version: int, allow_inactive: bool = False) -> bool:
  67. """
  68. 根据版本号加载密钥
  69. Args:
  70. db: 数据库会话
  71. key_version: 密钥版本号
  72. allow_inactive: 是否允许加载已过期/已停用的密钥(用于兼容旧密钥版本)
  73. Returns:
  74. bool: 是否加载成功
  75. """
  76. try:
  77. # 查找指定版本的密钥
  78. query = db.query(APIKey).filter(
  79. APIKey.key_version == key_version,
  80. APIKey.is_deleted == 0
  81. )
  82. if not allow_inactive:
  83. # 严格模式:必须激活且未过期
  84. query = query.filter(
  85. APIKey.is_active == 1,
  86. APIKey.expire_at > beijing_now_naive()
  87. )
  88. api_key = query.first()
  89. if not api_key:
  90. logger.warning(
  91. f"未找到密钥版本 {key_version} "
  92. f"(激活={not allow_inactive},已过期={not allow_inactive})"
  93. )
  94. return False
  95. # 检查缓存是否仍然有效(激活状态可能已改变)
  96. if key_version in self.key_cache:
  97. cached_private_pem, cached_public_pem = self.key_cache[key_version]
  98. # 验证缓存内容是否与数据库完全一致
  99. if cached_private_pem == api_key.private_key and cached_public_pem == api_key.public_key:
  100. # 缓存有效,直接使用
  101. self.load_private_key(cached_private_pem, key_version)
  102. return True
  103. else:
  104. # 缓存已过期或不一致,清除旧缓存
  105. del self.key_cache[key_version]
  106. logger.info(f"清除过期缓存: 版本 {key_version}")
  107. self.load_private_key(api_key.private_key, key_version)
  108. self.key_cache[key_version] = (api_key.private_key, api_key.public_key)
  109. return True
  110. except Exception as e:
  111. logger.exception(f"加载密钥版本 {key_version} 失败")
  112. return False
  113. def get_current_public_key(self, db: Session) -> Tuple[str, int]:
  114. """获取当前激活的公钥(主is_active,辅expire_at)"""
  115. try:
  116. api_key = db.query(APIKey).filter(
  117. APIKey.is_active == 1, # 必须是激活状态
  118. APIKey.is_deleted == 0,
  119. APIKey.expire_at > beijing_now_naive() # 且未过期
  120. ).order_by(APIKey.key_version.desc()).first()
  121. if api_key:
  122. return api_key.public_key, api_key.key_version
  123. logger.warning("未找到激活的有效公钥")
  124. return "", 0
  125. except Exception as e:
  126. logger.exception("获取当前公钥失败")
  127. return "", 0
  128. def encrypt_for_client(self, data: Dict[str, Any], db: Session) -> Tuple[str, int]:
  129. """
  130. 为客户端加密数据
  131. Args:
  132. data: 要加密的数据
  133. db: 数据库会话
  134. Returns:
  135. tuple: (加密后的数据, 使用的密钥版本)
  136. """
  137. try:
  138. # 获取当前激活的公钥
  139. public_key_pem, key_version = self.get_current_public_key(db)
  140. if not public_key_pem:
  141. raise ValueError("未找到可用的公钥")
  142. # 如果当前加载的不是最新的密钥,则重新加载
  143. if self.current_key_version != key_version:
  144. self.load_key_by_version(db, key_version)
  145. # 生成AES密钥
  146. aes_key = self._generate_aes_key()
  147. # 使用AES加密数据
  148. encrypted_payload = self._aes_encrypt(json.dumps(data, ensure_ascii=False).encode('utf-8'), aes_key)
  149. # 使用RSA加密AES密钥
  150. encrypted_aes_key = self._rsa_encrypt_key(aes_key, public_key_pem)
  151. # 组合:encrypted_aes_key(256) + iv(16) + encrypted_payload
  152. iv = aes_key[16:32] # 从AES密钥中提取IV
  153. result = encrypted_aes_key + iv + encrypted_payload
  154. # Base64编码
  155. encrypted_data = base64.b64encode(result).decode('utf-8')
  156. logger.debug(f"客户端数据加密成功,版本: {key_version}")
  157. return encrypted_data, key_version
  158. except Exception as e:
  159. logger.exception("客户端数据加密失败")
  160. raise
  161. def decrypt_from_client(self, encrypted_data: str, key_version: int, db: Session) -> Dict[str, Any]:
  162. """
  163. 解密客户端发送的数据(单 blob:混合加密或纯 RSA)
  164. Args:
  165. encrypted_data: 加密的数据
  166. key_version: 使用的密钥版本
  167. db: 数据库会话
  168. Returns:
  169. dict: 解密后的数据
  170. 密钥兼容:当 key_version 对应的密钥已过期时,自动尝试加载该版本的密钥
  171. (已过期但未删除的密钥仍可用于解密,仅用于兼容客户端缓存的旧密钥)
  172. """
  173. try:
  174. # 确保使用正确的密钥版本
  175. if self.current_key_version != key_version:
  176. # 1. 首先尝试严格模式(激活且未过期)
  177. if not self.load_key_by_version(db, key_version, allow_inactive=False):
  178. # 2. 严格模式失败,尝试宽松模式
  179. logger.info(f"密钥版本 {key_version} 严格模式失败,尝试宽松模式兼容旧密钥")
  180. if not self.load_key_by_version(db, key_version, allow_inactive=True):
  181. raise ValueError(f"无法加载密钥版本 {key_version}")
  182. return self.decrypt(encrypted_data)
  183. except Exception as e:
  184. logger.exception("客户端数据解密失败")
  185. raise
  186. def decrypt_from_client_separate(
  187. self,
  188. encrypted_params_b64: str,
  189. encrypted_aes_key_b64: str,
  190. key_version: int,
  191. db: Session,
  192. ) -> Dict[str, Any]:
  193. """
  194. 解密客户端按「分离格式」发送的数据(与 client-tauri 一致):
  195. - encrypted_params = base64(IV(16) + AES-256-CTR 密文)
  196. - aes_key = base64(RSA-OAEP 加密的 32 字节 AES 密钥)
  197. 密钥兼容:当 key_version 对应的密钥已过期时,自动尝试加载该版本的密钥
  198. (已过期但未删除的密钥仍可用于解密,仅用于兼容客户端缓存的旧密钥)
  199. """
  200. if not encrypted_params_b64 or not encrypted_aes_key_b64:
  201. raise ValueError("encrypted_params 与 aes_key 均不能为空")
  202. last_error = None
  203. # 1. 首先尝试使用严格模式加载当前密钥版本(激活且未过期)
  204. if self.current_key_version != key_version:
  205. if self.load_key_by_version(db, key_version, allow_inactive=False):
  206. last_error = None
  207. else:
  208. # 2. 严格模式失败,尝试宽松模式(允许已过期但未删除的密钥)
  209. logger.info(f"密钥版本 {key_version} 严格模式失败,尝试宽松模式兼容旧密钥")
  210. if self.load_key_by_version(db, key_version, allow_inactive=True):
  211. logger.info(f"使用旧密钥版本 {key_version} 解密(兼容模式)")
  212. last_error = None
  213. else:
  214. last_error = f"无法加载密钥版本 {key_version}"
  215. if not self.private_key:
  216. raise ValueError(last_error or "私钥未加载")
  217. # 3. 执行解密
  218. try:
  219. # 1) RSA-OAEP 解密得到 32 字节 AES 密钥
  220. enc_aes = base64.b64decode(encrypted_aes_key_b64)
  221. aes_key = self.private_key.decrypt(
  222. enc_aes,
  223. padding.OAEP(
  224. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  225. algorithm=hashes.SHA256(),
  226. label=None,
  227. ),
  228. )
  229. if len(aes_key) != 32:
  230. raise ValueError(f"AES 密钥长度必须为 32,实际为 {len(aes_key)}")
  231. # 2) Base64 解码请求体:前 16 字节 IV,其余为 AES-CTR 密文
  232. params_bytes = base64.b64decode(encrypted_params_b64)
  233. if len(params_bytes) < 16:
  234. raise ValueError("encrypted_params 长度不足(需要至少 16 字节 IV)")
  235. iv = params_bytes[:16]
  236. ciphertext = params_bytes[16:]
  237. # 3) AES-256-CTR 解密(与客户端 Ctr64BE::<Aes256> 对应)
  238. cipher = Cipher(
  239. algorithms.AES(aes_key),
  240. modes.CTR(iv),
  241. backend=default_backend(),
  242. )
  243. decryptor = cipher.decryptor()
  244. decrypted_bytes = decryptor.update(ciphertext) + decryptor.finalize()
  245. return json.loads(decrypted_bytes.decode("utf-8"))
  246. except Exception as e:
  247. logger.exception("客户端数据解密失败(分离格式)")
  248. raise
  249. def _generate_aes_key(self) -> bytes:
  250. """生成AES密钥和IV"""
  251. import os
  252. return os.urandom(32) # 256位AES密钥
  253. def _aes_encrypt(self, data: bytes, key: bytes) -> bytes:
  254. """AES加密"""
  255. iv = key[16:32] # 使用密钥的后16字节作为IV
  256. cipher = Cipher(algorithms.AES(key[:16]), modes.CBC(iv), backend=default_backend())
  257. encryptor = cipher.encryptor()
  258. # PKCS7填充
  259. padder = PKCS7(128).padder()
  260. padded_data = padder.update(data) + padder.finalize()
  261. return encryptor.update(padded_data) + encryptor.finalize()
  262. def _rsa_encrypt_key(self, aes_key: bytes, public_key_pem: str) -> bytes:
  263. """使用RSA加密AES密钥"""
  264. public_key = serialization.load_pem_public_key(
  265. public_key_pem.encode(),
  266. backend=default_backend()
  267. )
  268. return public_key.encrypt(
  269. aes_key,
  270. padding.OAEP(
  271. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  272. algorithm=hashes.SHA256(),
  273. label=None
  274. )
  275. )
  276. def encrypt_response_with_client_key(
  277. self,
  278. response_data: Dict[str, Any],
  279. encrypted_aes_key_b64: str,
  280. key_version: int,
  281. db: Session,
  282. ) -> Tuple[str, int]:
  283. """
  284. 使用请求中的客户端 AES 密钥加密响应(与 client-tauri 约定一致)。
  285. 格式:Base64( IV(16) + AES-256-CTR(JSON) ),客户端用同一密钥解密。
  286. """
  287. if not encrypted_aes_key_b64:
  288. raise ValueError("encrypted_aes_key 为空")
  289. if self.current_key_version != key_version:
  290. if not self.load_key_by_version(db, key_version):
  291. raise ValueError(f"无法加载密钥版本 {key_version}")
  292. if not self.private_key:
  293. raise ValueError("私钥未加载")
  294. try:
  295. enc_aes = base64.b64decode(encrypted_aes_key_b64)
  296. aes_key = self.private_key.decrypt(
  297. enc_aes,
  298. padding.OAEP(
  299. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  300. algorithm=hashes.SHA256(),
  301. label=None,
  302. ),
  303. )
  304. if len(aes_key) != 32:
  305. raise ValueError(f"AES 密钥长度必须为 32,实际为 {len(aes_key)}")
  306. payload = json.dumps(response_data, ensure_ascii=False).encode("utf-8")
  307. iv = secrets.token_bytes(16)
  308. cipher = Cipher(
  309. algorithms.AES(aes_key),
  310. modes.CTR(iv),
  311. backend=default_backend(),
  312. )
  313. encryptor = cipher.encryptor()
  314. ciphertext = encryptor.update(payload) + encryptor.finalize()
  315. result = iv + ciphertext
  316. return base64.b64encode(result).decode("utf-8"), key_version
  317. except Exception as e:
  318. logger.exception("使用客户端密钥加密响应失败")
  319. raise
  320. def decrypt(self, encrypted_data: str) -> Dict[str, Any]:
  321. """解密数据(支持RSA+AES混合加密和纯RSA加密)"""
  322. if not settings.ENCRYPT_ENABLED:
  323. return json.loads(encrypted_data)
  324. if not self.private_key:
  325. raise ValueError("私钥未加载")
  326. try:
  327. # Base64解码
  328. encrypted_bytes = base64.b64decode(encrypted_data)
  329. # 判断是混合加密还是纯RSA加密
  330. # 混合加密:加密的AES密钥(256字节) + IV(16字节) + 加密的数据
  331. # 纯RSA加密:直接是RSA加密的数据(通常小于256字节)
  332. # 如果数据长度大于256字节,认为是混合加密
  333. if len(encrypted_bytes) > 256:
  334. # 混合加密模式
  335. # 提取加密的AES密钥(前256字节)
  336. encrypted_aes_key = encrypted_bytes[:256]
  337. # 提取IV(接下来16字节)
  338. iv = encrypted_bytes[256:272]
  339. # 提取加密的数据(剩余部分)
  340. encrypted_payload = encrypted_bytes[272:]
  341. # 记录调试信息:各部分长度
  342. try:
  343. logger.debug(f"Encrypted total bytes: {len(encrypted_bytes)}, "
  344. f"enc_key_len: {len(encrypted_aes_key)}, iv_len: {len(iv)}, payload_len: {len(encrypted_payload)}")
  345. except Exception:
  346. pass
  347. # 使用RSA解密AES密钥
  348. aes_key = self.private_key.decrypt(
  349. encrypted_aes_key,
  350. padding.OAEP(
  351. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  352. algorithm=hashes.SHA256(),
  353. label=None
  354. )
  355. )
  356. # 使用AES解密数据
  357. cipher = Cipher(algorithms.AES(aes_key), modes.CBC(iv), backend=default_backend())
  358. decryptor = cipher.decryptor()
  359. padded_data = decryptor.update(encrypted_payload) + decryptor.finalize()
  360. # 去除PKCS7填充
  361. unpadder = PKCS7(128).unpadder()
  362. decrypted_bytes = unpadder.update(padded_data) + unpadder.finalize()
  363. # 记录解密后字节预览,便于调试(限制长度,避免日志过大)
  364. try:
  365. preview = decrypted_bytes[:300]
  366. # 尝试以utf-8安全显示
  367. try:
  368. preview_text = preview.decode('utf-8', errors='replace')
  369. except Exception:
  370. preview_text = str(preview)
  371. logger.debug(f"Decrypted bytes preview (first 300): {preview_text}")
  372. logger.debug(f"Decrypted total length: {len(decrypted_bytes)}")
  373. except Exception:
  374. pass
  375. # JSON解析
  376. return json.loads(decrypted_bytes.decode('utf-8'))
  377. else:
  378. # 纯RSA加密模式(向后兼容)
  379. decrypted = self.private_key.decrypt(
  380. encrypted_bytes,
  381. padding.OAEP(
  382. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  383. algorithm=hashes.SHA256(),
  384. label=None
  385. )
  386. )
  387. # JSON解析
  388. return json.loads(decrypted.decode('utf-8'))
  389. except Exception as e:
  390. logger.exception("数据解密失败")
  391. raise
  392. # 全局加密器实例
  393. encryptor = RSAEncryptor()
  394. # ============ 响应签名相关 ============
  395. # 服务端签名密钥:未设置环境变量时默认 buff-server-secret-{当前年},每次调用取当前年,跨年无需重启
  396. def get_server_secret() -> bytes:
  397. """获取服务端签名密钥"""
  398. import os
  399. default = f"buff-server-secret-{datetime.now().year}"
  400. return os.getenv("BUFF_API_SERVER_SECRET", default).encode()
  401. def generate_response_signature(
  402. encrypted_data: str,
  403. key_version: int,
  404. server_secret: bytes = None
  405. ) -> Tuple[str, int, str]:
  406. """
  407. 生成响应签名
  408. Args:
  409. encrypted_data: 加密后的响应数据 (Base64)
  410. key_version: 密钥版本
  411. server_secret: 服务端密钥 (可选,默认从环境变量获取)
  412. Returns:
  413. (signature, timestamp, nonce)
  414. """
  415. if server_secret is None:
  416. server_secret = get_server_secret()
  417. # 生成时间戳和nonce
  418. timestamp = int(time.time())
  419. nonce = secrets.token_hex(16)
  420. # 构建签名字符串: timestamp:nonce:encrypted_data:key_version
  421. sign_string = f"{timestamp}:{nonce}:{encrypted_data}:{key_version}"
  422. # HMAC-SHA256签名
  423. signature = hmac.new(
  424. server_secret,
  425. sign_string.encode(),
  426. hashlib.sha256
  427. ).digest()
  428. return base64.b64encode(signature).decode(), timestamp, nonce
  429. def create_secure_response(
  430. data: Any,
  431. db: Session,
  432. code: int = 200,
  433. msg: str = "成功"
  434. ) -> Dict[str, Any]:
  435. """
  436. 创建带签名的安全响应 (符合技术方案文档要求)
  437. 响应格式:
  438. {
  439. "code": 200,
  440. "msg": "成功",
  441. "encrypted_data": "base64(AES加密响应数据)",
  442. "timestamp": 1704105600,
  443. "nonce": "h8g7f6e5d4c3b2a1...",
  444. "signature": "base64(HMAC-SHA256签名)",
  445. "key_version": 1
  446. }
  447. Args:
  448. data: 响应数据
  449. db: 数据库会话
  450. code: 状态码
  451. msg: 消息
  452. Returns:
  453. 带签名的安全响应字典
  454. """
  455. try:
  456. # 构建响应数据
  457. response_data = {
  458. "code": code,
  459. "msg": msg,
  460. "data": data
  461. }
  462. # 加密响应数据
  463. encrypted_data, key_version = encryptor.encrypt_for_client(response_data, db)
  464. # 生成响应签名
  465. signature, timestamp, nonce = generate_response_signature(encrypted_data, key_version)
  466. # 返回完整的带签名响应
  467. return {
  468. "code": 200,
  469. "msg": "成功",
  470. "encrypted_data": encrypted_data,
  471. "timestamp": timestamp,
  472. "nonce": nonce,
  473. "signature": signature,
  474. "key_version": key_version
  475. }
  476. except Exception as e:
  477. logger.exception("创建安全响应失败")
  478. # 如果加密失败,返回错误信息(不加密)
  479. return {
  480. "code": 500,
  481. "msg": "系统异常,请稍后重试",
  482. "encrypted_data": None,
  483. "timestamp": int(time.time()),
  484. "nonce": secrets.token_hex(16),
  485. "signature": None,
  486. "key_version": 0
  487. }
  488. def verify_response_signature(
  489. encrypted_data: str,
  490. signature: str,
  491. timestamp: int,
  492. nonce: str,
  493. key_version: int,
  494. server_secret: bytes = None
  495. ) -> bool:
  496. """
  497. 验证响应签名 (客户端使用)
  498. Args:
  499. encrypted_data: 加密的响应数据
  500. signature: 响应签名
  501. timestamp: 响应时间戳
  502. nonce: 响应nonce
  503. key_version: 密钥版本
  504. server_secret: 服务端密钥
  505. Returns:
  506. 签名是否有效
  507. """
  508. try:
  509. if server_secret is None:
  510. server_secret = get_server_secret()
  511. # 构建签名字符串
  512. sign_string = f"{timestamp}:{nonce}:{encrypted_data}:{key_version}"
  513. # 计算期望的签名
  514. expected_signature = hmac.new(
  515. server_secret,
  516. sign_string.encode(),
  517. hashlib.sha256
  518. ).digest()
  519. # 常数时间比较 (防止时序攻击)
  520. return hmac.compare_digest(
  521. base64.b64decode(signature),
  522. expected_signature
  523. )
  524. except Exception as e:
  525. logger.exception("验证响应签名失败")
  526. return False