"""提供商 API Key 的对称加解密与掩码(ARCH §4.7)。 - 加密用 `cryptography` 的 Fernet,密钥取 `settings.credential_enc_key`。 - 缺失/非法密钥 → 立即失败(`CredentialKeyError`),绝不静默。 - `mask_api_key` 产出可安全展示/落库回显的掩码;明文永不出响应、永不进日志。 """ from __future__ import annotations from cryptography.fernet import Fernet, InvalidToken # 掩码常量:前缀 + 省略号 + 末四位。短 key 用 bullet 占位,绝不泄露字符。 _MASK_PREFIX = "sk-…" _MASK_PLACEHOLDER = "••••" _LAST_N = 4 class CredentialKeyError(RuntimeError): """加密密钥缺失或非法——快速失败,调用方负责映射为 5xx/配置错误。""" def _fernet(key: str) -> Fernet: if not key: raise CredentialKeyError( "credential_enc_key 未配置;无法加解密提供商凭据。请设置环境变量。" ) try: return Fernet(key.encode()) except (ValueError, TypeError) as exc: # 不回显 key 内容 raise CredentialKeyError( "credential_enc_key 非法(需为 32 字节 url-safe base64 Fernet key)。" ) from exc def encrypt_api_key(plaintext: str, *, key: str) -> bytes: """加密明文 API Key,返回可入库的密文字节(`api_key_enc`)。""" return _fernet(key).encrypt(plaintext.encode()) def decrypt_api_key(blob: bytes, *, key: str) -> str: """解密密文字节回明文。仅供探测/调用使用,绝不进响应或日志。""" try: return _fernet(key).decrypt(blob).decode() except InvalidToken as exc: raise CredentialKeyError("凭据密文无法解密(密钥不匹配或数据损坏)。") from exc def mask_api_key(plaintext: str) -> str: """掩码:仅露末四位,如 `sk-…1234`;过短/空则全隐。""" if len(plaintext) < _LAST_N: return f"{_MASK_PREFIX}{_MASK_PLACEHOLDER}" return f"{_MASK_PREFIX}{plaintext[-_LAST_N:]}"