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