Files
writer-work-flow/apps/api/ww_api/security/credentials.py
Yaojia Wang b523b4fd21 feat: M1 — 立项→写章草稿(SSE)→自动保存;连一家 provider
- 薄自建 LLM 网关:OpenAI 兼容适配器(DeepSeek) + instructor 结构化输出 + usage_ledger 记账 + 档位路由
- 记忆服务 assemble:确定性选择(显式+主角+近况) + 渲染卡 + 缓存断点(中性文本)
- LangGraph 写章节点 + Postgres checkpointer + SSE 归一(token/done/error)
- API:立项 + 写章 draft(SSE) + PUT 自动保存 + 提供商凭据(Fernet 加密/测试连接)
- 前端:AppShell + 作品库 + 5 步立项向导 + 写作工作台(流式打字机+自动保存) + 设置页
- M1 E2E:真实 DB + mock 网关零 token 走通闭环
2026-06-18 11:38:28 +02:00

54 lines
2.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""提供商 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:]}"