Files
ThearchyHelios dead51ae56 数据目录里的文件一律先写临时文件再改名:新增 core/atomic(同目录、带随机后缀的临时文件,写完 os.replace;只该生成一次的用 os.link 建,已有就不动)
- 工件(内容寻址):以前共用 <id>.tmp,两个进程同时写同一份内容时后到的会截断先到的半成品、先到的改名又让后到的改名失败;现在各写各的临时文件
- 加密密钥 .secret_key、断言签名私钥:几个进程同时第一次启动时只建出一把,后到的读先到的那一把(以前各生成一把,各自加密、签名的东西对方认不得),文件从建出来起就是 0600
- 附件抽出文字的缓存:临时文件名带随机后缀
- 文件工具(file_write)的覆盖和追加、三种系统沙箱写进工作目录的代码和输入文件、seatbelt 的策略文件:先写临时文件再改名
- 上传表格的版本库、原件存档、记录表本来就是这样写的,不动
2026-10-19 10:01:20 +08:00

61 lines
1.9 KiB
Python

from __future__ import annotations
import base64
import hashlib
import os
from cryptography.fernet import Fernet, InvalidToken
from app.core import atomic
from app.core.config import settings
_PREFIX = "enc::"
def _load_key() -> bytes:
"""取加密密钥:优先用配置里的 secret_key,否则在 data_dir 生成一个持久化的。"""
raw = settings.secret_key
if not raw:
key_file = settings.data_dir / ".secret_key"
if not key_file.exists():
# 只建一次(core/atomic.create_text):两个进程同时第一次启动,后到的读先到的那一把,不会各拿一把、
# 各自加密的东西对方解不开
atomic.create_text(key_file, base64.urlsafe_b64encode(os.urandom(32)).decode(), mode=0o600)
raw = key_file.read_text().strip()
# 任意字符串都归一化成 32 字节 Fernet key
digest = hashlib.sha256(raw.encode()).digest()
return base64.urlsafe_b64encode(digest)
_fernet = Fernet(_load_key())
def encrypt(value: str | None) -> str | None:
"""加密明文。已经是密文的原样返回,方便重复保存同一条记录。"""
if not value:
return value
if value.startswith(_PREFIX):
return value
return _PREFIX + _fernet.encrypt(value.encode()).decode()
def decrypt(value: str | None) -> str | None:
if not value:
return value
if not value.startswith(_PREFIX):
return value # 兼容历史明文
try:
return _fernet.decrypt(value[len(_PREFIX) :].encode()).decode()
except InvalidToken:
return None
def mask(value: str | None) -> str:
"""给前端展示用的掩码,永远不把明文 key 发回浏览器。"""
if not value:
return ""
plain = decrypt(value) or ""
if len(plain) <= 8:
return "••••"
return f"{plain[:4]}••••{plain[-4:]}"