FinBuddy SQLCipher加密与前端重构
· FinBuddy
今日进展
- 数据库加密改造:新增 SQLCipher 加密引擎(encrypted_engine.py),替换原来的 aiosqlite 方案,db.py 从异步引擎改为同步加密引擎 + asyncio.to_thread 异步封装
- 密钥管理重构:新增 db_crypto.py(KeyHolder 单例)和 db_keystore.py(密钥存储),密钥从用户密码 + salt 通过 PBKDF2 派生,仅存内存
- EncryptedStorage 密钥链路改造:security.py 不再从文件读取密钥,改为从 KeyHolder 获取,KeyHolder 不可用时降级为文件读取(兼容未加密启动)
- 新增数据库引导和迁移:bootstrap_db.py(首次启动初始化)和 migration.py(加密迁移脚本)
- 自选股排序功能:watchlist_service.py 新增 sort_orders 参数,支持批量添加时指定排序
- 知识库分类增加 sample_queries 字段:schemas.py 新增列,用于存储分类示例查询
- 前端全面优化:Chat 组件(消息列表/新闻分析/步骤进度)、Credit 面板(支付弹窗)、通知系统、策略回测、自选股面板等 20+ 组件修改
- Skills 更新:TDX 系列(K线/指标/板块/选股)、新闻分析(东方财富/新浪/同花顺源)、研究技能(财务分析/编排器)均有修改
- 新增文档:EventBus 架构设计、工作流元能力设计、LLM 元能力与工具清单、数据库定时备份设计
关键代码/伪代码
SQLCipher 加密引擎 — 替换 aiosqlite
# 之前:aiosqlite 异步引擎(明文数据库)
# 问题:aiosqlite 忽略 creator 参数,无法传入 SQLCipher 连接
# 方案:同步 SQLCipher 引擎 + asyncio.to_thread 异步封装
# db.py 改造前
async_engine = create_async_engine(f"sqlite+aiosqlite:///{db_path}")
AsyncSessionLocal = async_sessionmaker(async_engine, class_=AsyncSession)
# db.py 改造后
from finclaw.data.encrypted_engine import get_encrypted_engine
sync_engine = get_encrypted_engine(db_path) # SQLCipher 引擎
SyncSessionLocal = sessionmaker(sync_engine, expire_on_commit=False)
# 异步封装:同步引擎 + to_thread
async def get_sync_session():
session = SyncSessionLocal()
try:
yield session
finally:
session.close()
# 引擎延迟初始化:首次调用时才创建
# 避免模块导入时就要求密钥
KeyHolder — 密钥派生与管理
# 核心思路:用户密码 → PBKDF2 → SQLCipher 密钥
# 密钥只存在内存中,不落盘
CLASS KeyHolder:
"""密钥持有者 — 单例模式"""
_instance = None
@classmethod
DEF get_instance(cls) -> KeyHolder:
IF cls._instance IS None:
RAISE RuntimeError("KeyHolder 未初始化")
RETURN cls._instance
@classmethod
DEF initialize(cls, password: str, salt: bytes):
# PBKDF2 派生密钥
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations=600000 # OWASP 推荐
)
cls._instance = cls(key_hex=key.hex())
DEF get_sqlcipher_key(self) -> str:
# 返回 SQLCipher 格式的密钥
RETURN f"x'{self._key_hex}'"
DEF get_fernet_key(self) -> bytes:
# 从 SQLCipher 密钥派生 Fernet 密钥(不同用途,不同密钥)
fernet_raw = hashlib.sha256(
self._key_hex.encode("utf-8") + b"_fernet"
).digest()
RETURN base64.urlsafe_b64encode(fernet_raw)
EncryptedStorage — 密钥链路改造
# 改造前:密钥从文件读取
DEF _load_key(self):
RETURN open(self.key_path, "rb").read()
# 改造后:优先从 KeyHolder 获取,降级为文件读取
DEF _load_key(self):
TRY:
FROM finclaw.infra.db_crypto IMPORT KeyHolder
key_holder = KeyHolder.get_instance()
# 从 SQLCipher 密钥派生 Fernet 密钥
RETURN key_holder.get_fernet_key()
EXCEPT Exception:
# 降级:兼容未加密启动场景
IF os.path.exists(self.key_path):
RETURN open(self.key_path, "rb").read()
RAISE
遇到的问题
- aiosqlite 不支持 creator 参数:验证了 aiosqlite 会忽略传入的 creator,无法用它连接 SQLCipher。最终放弃异步引擎,改用同步引擎 + asyncio.to_thread,性能差异可忽略(本地 SQLite 本身就是文件 IO)
- 密钥链路循环依赖:KeyHolder 在 db_crypto.py,EncryptedStorage 在 security.py,db.py 又依赖两者。解决方案:延迟导入(try: from finclaw.infra.db_crypto import KeyHolder),避免模块级循环引用
- 引擎延迟初始化的时序问题:如果模块导入时就创建引擎,密钥还没准备好。解决方案:引擎延迟到首次 get_sync_session 或 init_db 调用时才创建
- 前端 20+ 组件同步修改:Chat/Credit/Notification/Strategy/Watchlist 全动了,回归测试压力大。目前靠手动验证核心流程
明日计划
- SQLCipher 加密端到端测试:验证加密数据库的创建、读写、迁移流程
- 前端组件回归测试:重点验证 Chat 流式对话、Credit 支付、Watchlist 排序
- 密钥丢失恢复方案:用户忘记密码时的数据恢复策略