Files
PlantAI-rag-new/vstore/crypto.py
T
jerryandClaude Fable 5 5db3ead73e feat(docgen): doc_writer 能力全链——vstore 独立服务 + BFF docgen 路由 + 前端文档工作台
- vstore 独立安装包(8094):解析/嵌入/加密 vault/faiss 检索,存储路径支持本地 file 与 http(s):// URL,启动握手校验,集群化预留(embed 独立模块)
- BFF docgen 路由:上传入库/generate/编辑/导出/下载/主库管理,意图护栏(与前端同词典),vstore 失败兜底,quota 507 与 TTL 惰性清理
- 前端:自然触发意图路由、拖拽上传 chips、能力徽标、右侧文档 TAB(WYSIWYG 编辑/prompt 修改/PDF+DOCX 导出下载)、右栏拖宽+手柄仿左栏
- 批量导入工具 docgen_import.py(并发 8、导入即加密+删源明文)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 21:13:37 +08:00

88 lines
2.9 KiB
Python
Raw 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.
"""crypto.py —— Fernet 对称加密封装(模版/附件密文落盘,明文纪律)。
- encrypt_bytes / decrypt_bytes:bytes 进 bytes 出
- dump_encrypted / load_encrypted:.enc 原子落盘(临时文件 + os.replace)
- secure_delete:3 遍覆写(0x00/0xFF/0xAA)+ fsync 后删除,尽力而为
"""
from __future__ import annotations
import os
from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
from .settings import get_fernet_key
class CryptoError(Exception):
"""加解密失败(密钥不匹配/数据损坏/输入为空)。"""
def _cipher(key: bytes | None = None) -> Fernet:
return Fernet(key if key is not None else get_fernet_key())
def encrypt_bytes(data: bytes, key: bytes | None = None) -> bytes:
"""明文 bytes → Fernet token(URL-safe base64,可直接落盘 .enc)。"""
if not data:
raise CryptoError("加密内容为空")
try:
return _cipher(key).encrypt(data)
except (ValueError, TypeError) as exc:
raise CryptoError(f"加密失败:{exc}") from exc
def decrypt_bytes(token: bytes, key: bytes | None = None) -> bytes:
"""Fernet token → 明文 bytes。密钥不符/数据损坏抛 CryptoError。"""
if not token:
raise CryptoError("密文为空")
try:
return _cipher(key).decrypt(token)
except InvalidToken as exc:
raise CryptoError("解密失败:密钥不匹配或数据已损坏") from exc
def dump_encrypted(path: str | Path, data: bytes, key: bytes | None = None) -> Path:
"""密文落盘(.enc)。原子写:先临时文件再 os.replace。"""
target = Path(path)
target.parent.mkdir(parents=True, exist_ok=True)
token = encrypt_bytes(data, key=key)
tmp = target.with_name(target.name + ".tmp")
tmp.write_bytes(token)
os.replace(tmp, target)
return target
def load_encrypted(path: str | Path, key: bytes | None = None) -> bytes:
"""读取 .enc 并解密为明文 bytes。"""
target = Path(path)
if not target.exists():
raise CryptoError(f"密文不存在:{target}")
return decrypt_bytes(target.read_bytes(), key=key)
def secure_delete(path: str | Path) -> None:
"""尽力安全删除:3 遍覆写 + fsync 后删除。
说明:对 SSD 无法保证物理擦除,但可防常规恢复;失败时仍尽力 unlink。
"""
target = Path(path)
if not target.exists():
return
try:
size = target.stat().st_size
if size > 0:
with open(target, "r+b") as f:
for pattern in (b"\x00", b"\xff", b"\xaa"):
f.seek(0)
f.write(pattern * size)
f.flush()
os.fsync(f.fileno())
target.unlink()
except OSError:
# 覆写失败也绝不残留明文(本路径本就只存密文)
try:
target.unlink()
except OSError:
pass