api_crypto.py 1.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. """Web API 请求/响应加解密工具。"""
  2. from __future__ import annotations
  3. import base64
  4. import hashlib
  5. import json
  6. import os
  7. import time
  8. from typing import Any, Dict
  9. from cryptography.hazmat.primitives.ciphers.aead import AESGCM
  10. class ApiCryptoError(Exception):
  11. """API 加解密错误。"""
  12. def derive_key(fixed_secret: str, user_id: str, ts: int, nonce: str) -> bytes:
  13. seed = f"{fixed_secret}:{user_id}:{ts}:{nonce}"
  14. return hashlib.sha256(seed.encode("utf-8")).digest()
  15. def _b64url_encode(data: bytes) -> str:
  16. return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
  17. def _b64url_decode(data: str) -> bytes:
  18. pad = "=" * (-len(data) % 4)
  19. return base64.urlsafe_b64decode((data + pad).encode("utf-8"))
  20. def encrypt_json(payload: Dict[str, Any], key: bytes) -> str:
  21. iv = os.urandom(12)
  22. aes = AESGCM(key)
  23. raw = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
  24. cipher = aes.encrypt(iv, raw, None)
  25. return _b64url_encode(iv + cipher)
  26. def decrypt_json(cipher_text: str, key: bytes) -> Dict[str, Any]:
  27. try:
  28. data = _b64url_decode(cipher_text)
  29. if len(data) < 13:
  30. raise ApiCryptoError("密文长度非法")
  31. iv = data[:12]
  32. cipher = data[12:]
  33. aes = AESGCM(key)
  34. plain = aes.decrypt(iv, cipher, None)
  35. obj = json.loads(plain.decode("utf-8"))
  36. if not isinstance(obj, dict):
  37. raise ApiCryptoError("解密后报文格式非法")
  38. return obj
  39. except ApiCryptoError:
  40. raise
  41. except Exception as exc:
  42. raise ApiCryptoError("密文解析失败") from exc
  43. def gen_nonce_hex() -> str:
  44. return os.urandom(12).hex()
  45. def is_ts_in_window(ts: int, skew_sec: int) -> bool:
  46. now_ms = int(time.time() * 1000)
  47. return abs(now_ms - int(ts)) <= max(30, int(skew_sec)) * 1000