feat(db): make at-rest field encryption optional via DB_CIPHER - #71
Merged
Merged
Conversation
- DB_CIPHER 新增:none(默认,不加密)/ aes-256-gcm(HKDF,enc:v2:)/ aes-256-gcm-pbkdf2(历史 enc:v1:)/ aes-256-cbc-hmac(enc:v3:) - 解密改为按密文前缀自动识别算法,与当前配置无关:换算法或关掉加密都不会 让既有数据不可读,旧密文会在下一次保存时自动迁移为明文 - DB_CIPHER=none 只表示不加密数据库字段:未提供 JWT_SECRET 时仍会生成并 持久化共享密钥,保证 JWT 令牌在多实例/冷启动间一致 - /api/public/env_check 暴露 config.db_cipher,并同步更新 README、wrangler vars、Deploy 按钮绑定说明与 .env.example 文案 - 修复 db_cache.test.ts 的既有失败:storage 缺少 driver/mount_path 会被 ensureDefaultStorages 过滤掉,导致断言在 storages[0] 上抛错 Co-Authored-By: CodeBuddy <noreply@codebuddy.ai>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 80ff38e | Sep 20 2026, 09:10 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | 80ff38e | Sep 20 2026, 09:10 AM |
4 tasks
- DB_CIPHER 新增:chacha20-poly1305(enc:v4:,RFC 8439,纯 JS 实现)、 des-cbc-hmac / 3des-cbc-hmac(enc:v5:/enc:v6:,crypto-js + HMAC-SHA256)。 后两者仅作兼容用途:单 DES 有效密钥 56-bit 可被暴力破解、3DES 已被 NIST SP 800-131A 弃用,选用时打印一次性告警 - 新增 pkg/chacha20.ts:ChaCha20-Poly1305 纯 JS 实现(WebCrypto 全平台无此算法), 已通过 RFC 8439 §2.5.2/§2.8.2 官方向量并与 Node 原生实现交叉验证 - 新增 pkg/legacy-ciphers.ts:DES/3DES-CBC(WebCrypto 无 DES/3DES), 已与 OpenSSL des-ede3-cbc 交叉验证(单 DES 用等价的 EDE(K,K,K) 比对) - 性能①:密钥派生结果在进程内缓存,同一 isolate 只派生一次 (#69 已把「逐字段派生」降为「每次操作派生一次」,这里再降一级) - 性能②:sealDb 改为写时复制,不再对整库做 JSON 深拷贝 - 性能③:新增「未变化字段跳过重新加密」——明文、算法、密钥都没变时直接复用 上次的密文(三重校验保证换算法/关加密/换密钥/改值的行为完全不变), 因此改一个设置不再触发全库重新加密(对 PBKDF2/DES 一类昂贵算法尤为明显) - 测试:新增 pkg/cipher_algorithms.test.ts(RFC 向量 + 原生互操作), db_cipher.test.ts 扩展到 15 例(含缓存复用与写时复制不变量) - 已在仓库根目录验证边缘构建可正常打包新模块(node scripts/build-edge.mjs, 产物 +16.3KB;构建产物按 #69 的做法不随本 PR 提交) Co-Authored-By: CodeBuddy <noreply@codebuddy.ai>
Closed
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(db): make at-rest field encryption optional via DB_CIPHER
Summary / 摘要
DB_CIPHER,默认none不加密);用户可感知的行为变化
DB_CIPHER,取值(大小写无关,支持别名):none(默认)aes-256-gcmenc:v2:aes-256-gcm-pbkdf2enc:v1:aes-256-cbc-hmacenc:v3:chacha20-poly1305enc:v4:des-cbc-hmacenc:v5:3des-cbc-hmacenc:v6:别名示例:
gcm/hkdf/v2、pbkdf2/v1、cbc/v3、chacha20/v4、des/v5、3des/tripledes/v6、off/plain。取值无法识别时告警并回退
none(不静默启用任何算法);选用 DES/3DES 时打印一次性安全告警。enc:v1:~enc:v6:密文仍会被自动解密、可正常登录;并在下一次配置保存时自动转写为明文(逐字段迁移,无需手动步骤)。若希望继续加密,显式设置
DB_CIPHER。none只表示「不加密数据库字段」,不影响其它任何行为:JWT 签名仍需一把跨实例一致的共享密钥,未通过环境变量
JWT_SECRET提供时,安装向导仍会生成并持久化到openlist_encryption_secret。/api/public/env_check新增config.db_cipher;scripts/env-check.mjs同步打印。重要实现变化
pkg/crypto.ts:DbCipher注册表(resolveDbCipher/cipherPrefix/detectCipherPrefix/isSealedCiphertext/createFieldCipher),FieldCipher增加fingerprint(算法+密钥指纹)。pkg/chacha20.ts(新):ChaCha20-Poly1305 纯 JS 实现(含 BigInt Poly1305、长度前缀 AEAD 编码、定长标签比较)。pkg/legacy-ciphers.ts(新):DES/3DES-CBC 纯 JS 实现(crypto-js,已有依赖)。internal/model/db.ts:DB_CIPHER、读取按密文前缀(换算法/关加密都不丢数据);hasSealedValues():明文数据 +none时完全不解析密钥;resolveFieldCipher(env, { needDecrypt }):none但存在历史密文时仍构造解密器(迁移前提);sealDb改为写时复制;新增未变化字段复用密文缓存(sealedCache);none」时提示下次保存将转为明文;缺密钥时提示明文落盘/密文保持原样。internal/model/store/backend.ts:新增readCipher(env)(与readDriver/readFormat正交)。wrangler.jsonc、package.json(Deploy 按钮绑定说明)、README.md、.env.example、.dev.vars.example。db_cache.test.ts:既有 v2 envelope 用例显式加上DB_CIPHER=aes-256-gcm,并新增「默认不加密」用例;同时修复该文件在
main上即失败的用例(storage 缺driver/mount_path被ensureDefaultStorages()过滤成空数组,断言在
storages[0]上抛TypeError)。package.json新增test:model并纳入test:all。CPU 特性与本次优化(实测,120 字节字段,Node 22)
aes-256-gcmchacha20-poly1305aes-256-cbc-hmacdes/3des-cbc-hmacaes-256-gcm-pbkdf2结论:密码算法本身不是瓶颈,KDF 与「重复加解密次数」才是。 因此本次做了三项无功能风险的优化:
crypto.ts的cachedDerive):同一 (算法, 密钥) 在 isolate 内只派生一次;fix(storage): reduce worker CPU usage #69 是「每次 save/load 派生一次」,这里再降一级到「每个 isolate 一次」。
纯 memoization:输入相同输出必然相同,派生失败不写缓存(避免瞬时故障被固化)。
sealDb写时复制:不再对整库做JSON.parse(JSON.stringify(...)),只复制真正被修改的数组/实体,键顺序与深拷贝版本一致(测试断言落盘内容不变)。
明文相同 && 算法/密钥指纹相同 && 密文算法 == 当前写入算法则直接复用原密文,跳过整个密码学运算。任一条件不满足都走正常加密路径,因此换算法/关加密/轮换密钥/改值的行为与优化前完全一致。
惰性解密(需求 3)评估结论:暂不实现
storages[].addition(网盘凭据)是同步消费的热点:internal/op/storage.ts有 101 处引用,约 60 个 driver 文件里都是同步读取/解析(
parseAddition等),总量上千处。要做到惰性解密就必须把 async 传导到所有 driver 调用点,改动面与回归风险都过大。
users[].password/otp_secret的消费点相对集中(auth.ts/user.ts/password.ts约 20 处),但收益有限:一次加载需要解密的用户密码数量 = 用户数(通常 1~3 个),
真正多的是
storages[].addition(每个挂载 1 个,且每次请求几乎都会用到)。dbCache(请求内 TTL 缓存)+ 本次的“未变化字段跳过加密”已经消除了绝大部分重复开销;相比之下惰性解密能省的是「一次加载里那些本请求根本用不到的字段」,收益很小而风险最高。
如果你确实要,我可以单独做「仅
users[].password的惰性解密」(约 5 个调用点,可测),但建议先观察本次优化后的实际 CPU 数据再决定。
兼容性 / 迁移矩阵
DB_CIPHERnoneenc:v1:~enc:v6:noneenc:v1:~enc:v6:配置 checklist
→ 未勾选:读取侧完全向后兼容(旧密文自动解密并自动迁移),API/存储结构不变;
变化仅在于「默认不再加密」这一行为默认值。若维护者认为「默认关闭加密」需按破坏性/安全变更处理,请在评审中指出。
→ 新增
DB_CIPHER(含 6 种算法);/api/public/env_check返回体新增config.db_cipher;敏感字段落盘形态与迁移时机变化;新增
enc:v4:~enc:v6:三种密文格式。Related repository PRs / 关联仓库 PR:
DB_CIPHER环境变量与各算法说明)Related Issues / 关联 Issue
enc:v2:HKDF envelope 与本 PR 的aes-256-gcm完全一致,可互相解密)none不影响共享密钥的生成与持久化)Testing / 测试
go test ./...→ 不适用(本仓库为 TypeScript Worker 实现,非 Go 仓库)npx tsx --test "src/backend/internal/model/*.test.ts"db_cipher.test.ts15 例)npx tsx --test "src/backend/pkg/*.test.ts"cipher_algorithms.test.ts6 例)npx tsx --test src/backend/internal/model/store/store.test.tsnpx tsx --test "src/backend/server/*.test.ts"main上的既有失败(default_credentials.test.ts3 项 +seed.test.tsCAS codec 1 项),已用git stash在未改动的main上复现node scripts/build-edge.mjs(仓库根目录)算法正确性验证(
cipher_algorithms.test.ts):与 Node 原生
chacha20-poly1305双向互操作;篡改密文/标签/nonce 必须失败。des-ede3-cbc双向完全一致(单 DES 用数学等价的EDE(K,K,K)作参照,因 OpenSSL 3 默认禁用
des-cbc);含空串/多字节/超长明文与 PKCS#7 填充校验。手动验证场景(本地 memory 驱动):
DB_CIPHER→ 落盘明文;DB_CIPHER=chacha20-poly1305→ 落盘enc:v4:,冷启动读回明文;aes-256-gcm写入后改为不配置DB_CIPHER→ 仍能读回明文,再次保存后存储变明文;DB_CIPHER=3des-cbc-hmac读enc:v1:/enc:v2:历史数据 → 正常解密并迁移;其余说明:
tsc -p tsconfig.json --noEmit)在本机无法得到有效结论:本地node_modules未安装@types/node,报错全部是Cannot find name 'process'/Cannot find module 'node:test'这类环境依赖缺失,没有指向本次新增代码;IDE/LSP 诊断为空。
Checklist / 检查清单
gofmt,go fmt, orprettierwhere applicable.→ 遵循仓库风格(
semi: false、2 空格缩进)。未执行prettier --write:本机工作区为 CRLF(
core.autocrlf=true),全量重排会产生整文件行尾噪音。AI Disclosure / AI 使用声明
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution.I can reproduce all AI-assisted content included in this PR without any AI tools.
评审关注点(Reviewer Notes)
DB_CIPHER默认none(即默认不再加密敏感字段)。这是「可选加密」需求的直接结果,也让「直接用 SQL 读库 / 与 Go 后端共享库」更方便,但降低了静态保护。若希望保守默认,
把
DEFAULT_DB_CIPHER改成aes-256-gcm即可(一行)。des-cbc-hmac/3des-cbc-hmac是应需求加入的「非 AES 选项」,已实现 Encrypt-then-MAC 完整性保护并附带一次性告警,但强度不足。如果维护者认为不应提供,
删掉
WEAK_DB_CIPHERS中的两项 +createFieldCipher的两个分支即可,其余代码无需改动。pkg/chacha20.ts是自带实现(WebCrypto 全平台无 ChaCha20,且本仓库边缘构建把
node:crypto换成抛错 shim)。已用 RFC 官方向量 + Node 原生实现双重验证;实现刻意选择 BigInt(简洁、易审计)而非位运算优化。请重点 review 认证数据的构造顺序
(aad‖pad(aad)‖ct‖pad(ct)‖len(aad)‖len(ct))与定长标签比较。
aes-256-cbc-hmac用 4 字节长度前缀包裹明文,规避 Node 与 CF Workers在 WebCrypto AES-CBC padding 行为上的差异(见
wrapWithLength/unwrapWithLength)。cloud-functions/[[default]].js;EdgeOne 部署前需运行
node scripts/build-edge.mjs(已在本机验证可成功打包)。