File size: 3,383 Bytes
0e6887b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | """
对称加密工具(Fernet):用于把用户自配的 LLM 密钥**加密后**持久化。
=================================================================
用户在「模型设置」面板填写的 API 密钥属于敏感数据。若要跨会话保留(落到本地
SQLite 并经 :mod:`utils.hf_storage` 同步到私有 HF Dataset),**绝不能明文存储**。
本模块用 ``cryptography.fernet.Fernet`` 做对称加密:
- 加密密钥材料来自环境变量 ``PHARMAK_SECRET_KEY``(放 HF Space Secrets)。
为方便使用,任意长度的字符串都可作为 secret —— 内部用 SHA-256 派生出
Fernet 所需的 32 字节、URL-safe base64 密钥(故 secret 不必是合法 Fernet key)。
- **优雅降级**:未安装 ``cryptography`` 或未设置 secret 时,``is_available()``
返回 ``False``,调用方应退回"仅会话存储、不持久化密钥"的行为,避免明文落库。
- ``decrypt`` 在 token 损坏 / secret 轮换导致无法解密时返回 ``None``(不抛出),
调用方据此当作"无持久配置"处理。
安全说明:密文与 secret 永不进入日志;密文随 DB 同步到**私有** Dataset。
secret 一旦轮换,旧密文将无法解密(视为失效,用户需重新填写密钥)。
"""
from __future__ import annotations
import base64
import hashlib
import logging
import os
from typing import Optional
logger = logging.getLogger(__name__)
#: 加密密钥材料的环境变量名(放 HF Space Secrets)。
ENV_SECRET = "PHARMAK_SECRET_KEY"
def _secret() -> str:
return (os.environ.get(ENV_SECRET) or "").strip()
def _derive_key(secret: str) -> bytes:
"""从任意 secret 字符串派生 Fernet 所需的 32 字节 URL-safe base64 密钥。"""
digest = hashlib.sha256(secret.encode("utf-8")).digest()
return base64.urlsafe_b64encode(digest)
def is_available() -> bool:
"""是否可用加密:``cryptography`` 可导入且已配置 ``PHARMAK_SECRET_KEY``。"""
if not _secret():
return False
try:
from cryptography.fernet import Fernet # noqa: F401
except Exception:
logger.info("未安装 cryptography,用户密钥将不持久化(退回仅会话存储)。")
return False
return True
def _fernet():
from cryptography.fernet import Fernet
return Fernet(_derive_key(_secret()))
def encrypt(plaintext: str) -> Optional[str]:
"""加密明文,返回密文 token 字符串。不可用或入参为空时返回 ``None``。"""
if not plaintext or not is_available():
return None
try:
return _fernet().encrypt(plaintext.encode("utf-8")).decode("ascii")
except Exception as exc: # noqa: BLE001 - 加密失败不得影响主流程
logger.warning("密钥加密失败:%s", exc)
return None
def decrypt(token: str) -> Optional[str]:
"""解密密文 token,返回明文。无法解密(损坏 / secret 轮换)时返回 ``None``。"""
if not token or not is_available():
return None
try:
return _fernet().decrypt(token.encode("ascii")).decode("utf-8")
except Exception: # noqa: BLE001 - token 失效视为无配置
logger.info("密钥解密失败(token 失效或 secret 已轮换),按无持久配置处理。")
return None
__all__ = ["is_available", "encrypt", "decrypt", "ENV_SECRET"]
|