- 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>
88 lines
2.9 KiB
Python
88 lines
2.9 KiB
Python
"""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
|