Skip to content

feat(db): make at-rest field encryption optional via DB_CIPHER - #71

Merged
PIKACHUIM merged 2 commits into
mainfrom
feat/db-cipher-optional
Sep 21, 2026
Merged

PIKACHUIM merged 2 commits into
mainfrom
feat/db-cipher-optional

Conversation

@PIKACHUIM

@PIKACHUIM PIKACHUIM commented Sep 20, 2026 •

Copy link
Copy Markdown
Member

feat(db): make at-rest field encryption optional via DB_CIPHER

PR 分支:feat/db-cipher-optional(基于 main @ 3cd23e5)


Summary / 摘要

  1. 把「数据库敏感字段静态加密」从强制行为改为可选能力(新变量 DB_CIPHER,默认 none 不加密);
  2. 在 AES 之外补齐非 AES 算法:ChaCha20-Poly1305、DES、3DES(均为兼容/可选,不改变默认);
  3. 按 fix(storage): reduce worker CPU usage #69 的方向继续降低加解密开销:密钥派生进程内缓存、写时复制、未变化字段跳过重新加密。

用户可感知的行为变化

  • 新增 DB_CIPHER,取值(大小写无关,支持别名):
取值 密文前缀 实现 说明
none(默认) — — 明文落盘(敏感字段与普通 JSON 一致)
aes-256-gcm enc:v2: WebCrypto HKDF+AES-GCM 既有加密部署的形态,建议开启加密时使用
aes-256-gcm-pbkdf2 enc:v1: WebCrypto PBKDF2(10 万次) 历史 envelope;每字段约 27 ms
aes-256-cbc-hmac enc:v3: WebCrypto AES-CBC + HMAC-SHA256 Encrypt-then-MAC
chacha20-poly1305 enc:v4: 自带纯 JS(RFC 8439) WebCrypto 无 ChaCha20,故自带实现
des-cbc-hmac enc:v5: crypto-js + HMAC-SHA256 仅兼容:单 DES 56-bit,可暴力破解
3des-cbc-hmac enc:v6: crypto-js + HMAC-SHA256 仅兼容:3DES 已被 NIST SP 800-131A 弃用

别名示例: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-gcm ~30 µs WebCrypto 原生;小字段下调用开销占主导
chacha20-poly1305 ~18 µs 纯 JS,小字段下比 WebCrypto AES-GCM 更快
aes-256-cbc-hmac ~60 µs 加密 + HMAC 两次 WebCrypto 调用
des/3des-cbc-hmac ~0.26 ms crypto-js 纯 JS
aes-256-gcm-pbkdf2 ~27 ms 每字段 10 万次 PBKDF2 —— 历史版本真正的 CPU 大头

结论:密码算法本身不是瓶颈,KDF 与「重复加解密次数」才是。 因此本次做了三项无功能风险的优化:

  1. 密钥派生进程内缓存(crypto.ts 的 cachedDerive):同一 (算法, 密钥) 在 isolate 内只派生一次;
    fix(storage): reduce worker CPU usage #69 是「每次 save/load 派生一次」,这里再降一级到「每个 isolate 一次」。
    纯 memoization:输入相同输出必然相同,派生失败不写缓存(避免瞬时故障被固化)。
  2. sealDb 写时复制:不再对整库做 JSON.parse(JSON.stringify(...)),只复制真正被修改的数组/实体,
    键顺序与深拷贝版本一致(测试断言落盘内容不变)。
  3. 未变化字段跳过重新加密:load 时记录「明文 ↔ 密文」,save 时若明文相同 && 算法/密钥指纹相同 && 密文算法 == 当前写入算法 则直接复用原密文,跳过整个密码学运算。
    • 效果:改一个设置不再触发全库重新加密;对 PBKDF2(27 ms/字段)与 DES(0.26 ms/字段)尤为明显。
    • 安全性:复用仅在「同一明文 + 同一密钥 + 同一算法」下发生,不会出现「nonce/IV 用于不同明文」;
      任一条件不满足都走正常加密路径,因此换算法/关加密/轮换密钥/改值的行为与优化前完全一致。

惰性解密(需求 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_CIPHER 读取行为 下次保存后
明文(无前缀) none 原样返回 明文
enc:v1:~enc:v6: none 按前缀解密(需能取到密钥) 明文(自动迁移)
enc:v1:~enc:v6: 任一算法 按前缀解密 按新算法加密
明文(无前缀) 任一算法 原样返回 加密

取密钥顺序不变:env.JWT_SECRET → 持久化 openlist_encryption_secret,因此「原来能解密的部署升级后一定能解密」。

配置 checklist

  • This PR has breaking changes. / 此 PR 包含破坏性变更。
    → 未勾选:读取侧完全向后兼容(旧密文自动解密并自动迁移),API/存储结构不变;
    变化仅在于「默认不再加密」这一行为默认值。若维护者认为「默认关闭加密」需按破坏性/安全变更处理,请在评审中指出。
  • This PR changes public API, config, storage format, or migration behavior.
    → 新增 DB_CIPHER(含 6 种算法);/api/public/env_check 返回体新增 config.db_cipher;
    敏感字段落盘形态与迁移时机变化;新增 enc:v4:~enc:v6: 三种密文格式。
  • This PR requires corresponding changes in related repositories.

Related repository PRs / 关联仓库 PR:

  • OpenList:
  • OpenList-Docs: (建议补充 DB_CIPHER 环境变量与各算法说明)

Related Issues / 关联 Issue

Testing / 测试

  • go test ./... → 不适用(本仓库为 TypeScript Worker 实现,非 Go 仓库)
  • 自动化测试:
命令 结果
npx tsx --test "src/backend/internal/model/*.test.ts" 39 / 39 通过(含 db_cipher.test.ts 15 例)
npx tsx --test "src/backend/pkg/*.test.ts" 11 / 11 通过(含新增 cipher_algorithms.test.ts 6 例)
npx tsx --test src/backend/internal/model/store/store.test.ts 12 / 12 通过
npx tsx --test "src/backend/server/*.test.ts" 101 / 105:4 项为 main 上的既有失败(default_credentials.test.ts 3 项 + seed.test.ts CAS codec 1 项),已用 git stash 在未改动的 main 上复现
node scripts/build-edge.mjs(仓库根目录) ✅ 通过,产物 1,902,615 → 1,918,932 字节(+16.3 KB / +0.86%,crypto-js 本就在产物内)

算法正确性验证(cipher_algorithms.test.ts):

  • ChaCha20-Poly1305:RFC 8439 §2.5.2(Poly1305)与 §2.8.2(AEAD)官方测试向量全部匹配;
    与 Node 原生 chacha20-poly1305 双向互操作;篡改密文/标签/nonce 必须失败。
  • DES / 3DES:与 OpenSSL des-ede3-cbc 双向完全一致(单 DES 用数学等价的 EDE(K,K,K) 作参照,
    因 OpenSSL 3 默认禁用 des-cbc);含空串/多字节/超长明文与 PKCS#7 填充校验。
  • 既有 v1/v2(fix(storage): reduce worker CPU usage #69)密文与新算法互不干扰:任意写入算法都能解开任意前缀的密文(测试覆盖)。

手动验证场景(本地 memory 驱动):

  1. 不配置 DB_CIPHER → 落盘明文;
  2. DB_CIPHER=chacha20-poly1305 → 落盘 enc:v4:,冷启动读回明文;
  3. 用 aes-256-gcm 写入后改为不配置 DB_CIPHER → 仍能读回明文,再次保存后存储变明文;
  4. DB_CIPHER=3des-cbc-hmac 读 enc:v1:/enc:v2: 历史数据 → 正常解密并迁移;
  5. 连续两次保存同一份配置 → 落盘密文逐字节不变;只改一个字段 → 仅该字段密文变化。

其余说明:

  • 类型检查(tsc -p tsconfig.json --noEmit)在本机无法得到有效结论:本地 node_modules 未安装
    @types/node,报错全部是 Cannot find name 'process' / Cannot find module 'node:test' 这类环境依赖缺失,
    没有指向本次新增代码;IDE/LSP 诊断为空。

Checklist / 检查清单

  • I have read CONTRIBUTING.
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    → 遵循仓库风格(semi: false、2 空格缩进)。未执行 prettier --write:本机工作区为 CRLF
    (core.autocrlf=true),全量重排会产生整文件行尾噪音。
  • I have requested review from relevant maintainers or code owners where applicable.

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): CodeBuddy(DeepSeek-V4.1-Flash)

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-By attribution.

  • I can reproduce all AI-assisted content included in this PR without any AI tools.


评审关注点(Reviewer Notes)

  1. 默认值取舍:DB_CIPHER 默认 none(即默认不再加密敏感字段)。这是「可选加密」需求的直接结果,
    也让「直接用 SQL 读库 / 与 Go 后端共享库」更方便,但降低了静态保护。若希望保守默认,
    把 DEFAULT_DB_CIPHER 改成 aes-256-gcm 即可(一行)。
  2. 弱算法是否应该收:des-cbc-hmac / 3des-cbc-hmac 是应需求加入的「非 AES 选项」,
    已实现 Encrypt-then-MAC 完整性保护并附带一次性告警,但强度不足。如果维护者认为不应提供,
    删掉 WEAK_DB_CIPHERS 中的两项 + createFieldCipher 的两个分支即可,其余代码无需改动。
  3. 自研密码学代码:pkg/chacha20.ts 是自带实现(WebCrypto 全平台无 ChaCha20,且本仓库边缘构建
    把 node:crypto 换成抛错 shim)。已用 RFC 官方向量 + Node 原生实现双重验证;
    实现刻意选择 BigInt(简洁、易审计)而非位运算优化。请重点 review 认证数据的构造顺序
    (aad‖pad(aad)‖ct‖pad(ct)‖len(aad)‖len(ct))与定长标签比较。
  4. 跨运行时 padding:aes-256-cbc-hmac 用 4 字节长度前缀包裹明文,规避 Node 与 CF Workers
    在 WebCrypto AES-CBC padding 行为上的差异(见 wrapWithLength/unwrapWithLength)。
  5. DES/3DES 的填充:crypto-js 是纯 JS,在所有运行时行为一致,不存在上述差异。
  6. 构建产物:与 fix(storage): reduce worker CPU usage #69 一致,本 PR 不包含 cloud-functions/[[default]].js;
    EdgeOne 部署前需运行 node scripts/build-edge.mjs(已在本机验证可成功打包)。

- 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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-tsworkers 80ff38e Sep 20 2026, 09:10 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-work 80ff38e Sep 20 2026, 09:10 AM

@PIKACHUIM PIKACHUIM linked an issue Sep 20, 2026 that may be closed by this pull request
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>
@PIKACHUIM PIKACHUIM mentioned this pull request Sep 20, 2026
4 tasks
@PIKACHUIM PIKACHUIM linked an issue Sep 21, 2026 that may be closed by this pull request
4 tasks
@PIKACHUIM
PIKACHUIM merged commit a355867 into main Sep 21, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 备份还原功能卡住。 [BUG] Cloudflare Workers 免费版极易触发 exceededCpu 10ms 限制

1 participant