Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

# ── 必填 ────────────────────────────────────────────────────────────────────

# JWT 签名密钥,同时用于字段加密(网盘凭据、2FA 密钥)与定时任务鉴权。
# 要求 >=16 字符;推荐用 `openssl rand -hex 32` 生成。
# JWT 签名密钥,同时用作可选的字段加密密钥(DB_CIPHER 非 none 时,见 wrangler.jsonc)
# 与定时任务鉴权。要求 >=16 字符;推荐用 `openssl rand -hex 32` 生成。
JWT_SECRET=

# ── 选填 ────────────────────────────────────────────────────────────────────
Expand Down
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

# ── 必填 ────────────────────────────────────────────────────────────────────

# JWT 签名密钥,同时用于字段加密(网盘凭据、2FA 密钥)与定时任务鉴权。
# 要求 >=16 字符;推荐用 `openssl rand -hex 32` 生成。
# JWT 签名密钥,同时用作可选的字段加密密钥(DB_CIPHER 非 none 时,见 wrangler.jsonc)
# 与定时任务鉴权。要求 >=16 字符;推荐用 `openssl rand -hex 32` 生成。
JWT_SECRET=

# ── 选填 ────────────────────────────────────────────────────────────────────
Expand Down
59 changes: 58 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,37 @@ pnpm run deploy:worker
- `do`:Cloudflare Durable Objects(SQLite)
- `mysql`:MySQL(仅 Node.js 容器)

**DB_CIPHER**(敏感字段落盘算法,**默认不加密**)
- `none`(默认):不加密,敏感字段与普通 JSON 一样明文落盘
- `aes-256-gcm`:HKDF-SHA256 派生一把 AES-256-GCM 密钥(`enc:v2:`)
—— 既有加密部署写入的形态,开销最低,开启加密时**推荐**
- `aes-256-gcm-pbkdf2`:AES-256-GCM,密钥由 PBKDF2-SHA256(10 万次迭代)逐字段派生
(历史 `enc:v1:` envelope,抗弱口令但每次读写都很慢,仅建议用于兼容)
- `aes-256-cbc-hmac`:AES-256-CBC + HMAC-SHA256(`enc:v3:`,Encrypt-then-MAC)
- `chacha20-poly1305`:ChaCha20-Poly1305(`enc:v4:`,RFC 8439,纯 JS 实现;
WebCrypto 全平台都没有 ChaCha20,故自带实现并通过 RFC 官方向量验证)
- `des-cbc-hmac` / `3des-cbc-hmac`:DES / 3DES-CBC + HMAC-SHA256(`enc:v5:` / `enc:v6:`)
—— **仅用于兼容/互操作**:单 DES 有效密钥只有 56-bit(可被暴力破解),3DES 已被
NIST SP 800-131A 弃用(64-bit 分组 + Sweet32)。选用时后端会打印一次性告警,
请勿用它们保护真实数据。
- 别名(大小写无关):`gcm`/`hkdf`/`v2`、`pbkdf2`/`v1`、`cbc`/`v3`、`chacha20`/`v4`、
`des`/`v5`、`3des`/`tripledes`/`v6`、`off`/`plain`;无法识别时告警并回退 `none`

**几种算法的 CPU 特性(实测,120 字节字段,Node 22)**

| 算法 | 单字段耗时 | 说明 |
| :-- | --: | :-- |
| `aes-256-gcm` | ~30 µs | WebCrypto 原生(调用开销为主) |
| `chacha20-poly1305` | ~18 µs | 纯 JS,小字段下反而更快 |
| `aes-256-cbc-hmac` | ~60 µs | 两次 WebCrypto 调用(加密 + HMAC) |
| `des-cbc-hmac` / `3des-cbc-hmac` | ~0.26 ms | 纯 JS(crypto-js) |
| `aes-256-gcm-pbkdf2` | ~27 ms | 每字段 10 万次 PBKDF2(历史包袱) |

为此后端内置两项优化(对功能无影响):① 密钥派生结果在**进程内缓存**,同一
isolate 只派生一次;② **未变化字段跳过重新加密** —— 明文、算法、密钥都没变时
直接复用上次的密文,因此「改一个设置」不会触发全库重新加密(对 PBKDF2/DES
这类昂贵算法尤为明显)。

**推荐配置组合:**
```bash
# Cloudflare Workers + D1(推荐)
Expand Down Expand Up @@ -205,8 +236,31 @@ CF_API_KEY=your_api_token
> 非法「驱动 × 格式」组合(如 `DB_FORMAT=sql` + `DB_DRIVER=kv`)同样只报错,
> 不会自动改驱动或格式。

**关于 DB_CIPHER 的补充说明:**
- 加密只作用于 `storages[].addition`(网盘凭据)、敏感 `settings`、`users[].password`、
`users[].otp_secret`;内存中始终为明文,其余逻辑(驱动、路径解析、管理接口)不受影响。
- **`none` 只表示「不加密数据库字段」,不影响其它任何行为**:JWT 令牌签名仍需一把
跨实例/跨冷启动一致的共享密钥,若未通过环境变量 `JWT_SECRET` 提供,安装向导仍会
自动生成并持久化 `openlist_encryption_secret`(与加密是否启用无关)。
- 密文带版本前缀(`enc:v1:` ~ `enc:v6:`),**读取时按前缀自动识别算法**,
与当前配置无关。因此:
- 从加密切回 `none`(或升级后不再配置 `DB_CIPHER`):既有密文仍能正常解密,
并在**下一次配置保存**时自动转为明文(逐字段迁移,无需任何手动步骤);
- 更换算法:既有密文按旧算法解开,下次写入按新算法落盘;
- 既有的明文数据(无前缀)原样返回,升级不会丢数据。
- 加密算法选择是**正交的一维**,不改变 `DB_DRIVER` / `DB_FORMAT` 的语义。
- 取值无法识别时回退 `none` 并在日志告警(不会静默启用某个算法);
`/api/public/env_check` 的 `config.db_cipher` 会回显当前生效值。
- AES 三种算法基于 WebCrypto(各平台原生);`chacha20-poly1305` 与 `des/3des` 因
WebCrypto 不提供对应算法而使用纯 JS 实现 —— 全部在 Cloudflare Workers /
EdgeOne Node 云函数 / ESA / Node.js 上行为一致(不依赖 `node:crypto`)。

**向后兼容:**
- `DB_DRIVER=json` 自动转换为 `DB_FORMAT=map` + 自动检测驱动
- 未配置 `DB_CIPHER` 时**不再对敏感字段加密**(旧版本默认加密)。升级已有部署时:
存储中的 `enc:v1:`(PBKDF2)与 `enc:v2:`(HKDF)密文仍会按前缀自动解密、保持可读,
并在**下一次配置保存**时自动转为明文;如需继续加密,显式设置 `DB_CIPHER` 即可
(`DB_CIPHER=aes-256-gcm` 与既有加密部署的写入形态一致)。

**表名对齐(仅 SQL 格式):**
`sql` 格式采用列式表,命名策略与 Go 后端的 GORM 一致(snake_case + 复数表名 + 前缀):
Expand All @@ -224,7 +278,10 @@ CF_API_KEY=your_api_token

#### 安全配置

- `JWT_SECRET`:JWT 令牌签名密钥(必填),**同时用于数据加密与定时任务鉴权**
- `JWT_SECRET`:JWT 令牌签名密钥(必填),**同时用作可选的字段加密密钥**与定时任务鉴权
- `DB_CIPHER`:敏感字段落盘算法(可选,默认 `none` 不加密):`none` / `aes-256-gcm`
(推荐)/ `aes-256-gcm-pbkdf2` / `aes-256-cbc-hmac` / `chacha20-poly1305` /
`des-cbc-hmac` / `3des-cbc-hmac`(后两者仅兼容用途,不安全)
- `ADMIN_PASS`:初始管理员密码(可选,设置后跳过安装向导自动初始化 admin)

#### 其他配置
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
},
"DB_DRIVER": {
"description": "Storage driver: `auto` (default), `blob`, `cfkv`, `kv`, `d1`, `do`, `mysql`. Leave as `auto` unless you need to force one. An explicitly configured driver is never replaced automatically: if it is unavailable, the app reports a configuration error (including which driver auto-detection would have picked) instead of writing to another backend. Invalid driver/format pairs are reported the same way."
},
"DB_CIPHER": {
"description": "At-rest cipher for sensitive fields (drive credentials, 2FA secrets, password hashes): `none` (default, no encryption), `aes-256-gcm` (HKDF-derived AES-256-GCM key — the `enc:v2:` envelope written by existing encrypted deployments, recommended), `aes-256-gcm-pbkdf2` (legacy `enc:v1:` envelope, PBKDF2 per field, slow), `aes-256-cbc-hmac` (`enc:v3:`), `chacha20-poly1305` (`enc:v4:`, RFC 8439, pure-JS implementation), `des-cbc-hmac` / `3des-cbc-hmac` (`enc:v5:`/`enc:v6:`, compatibility only — single DES is 56-bit and 3DES is deprecated, do not use for real data). Ciphertexts carry an `enc:vN:` prefix and are decrypted by prefix on read, so switching back to `none` (or changing algorithms) never makes existing data unreadable — it only changes how newly written data is stored (legacy ciphertext is migrated to plaintext on the next save). Key derivation is cached per isolate, and unchanged fields are not re-encrypted on save. This setting does NOT affect the shared secret: with `none`, a secret is still generated and persisted during setup when JWT_SECRET is not provided, because JWT signing needs it."
}
}
},
Expand Down Expand Up @@ -51,10 +54,11 @@
"test:drivers": "tsx --test \"src/backend/drivers/**/*.test.ts\"",
"test:server": "tsx --test \"src/backend/server/*.test.ts\"",
"test:store": "tsx --test src/backend/internal/model/store/store.test.ts",
"test:model": "tsx --test \"src/backend/internal/model/*.test.ts\"",
"test:regress": "tsx scripts/_regress.mjs",
"test:deploy": "node scripts/test-deploy.js",
"env:check": "tsx scripts/env-check.mjs",
"test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:regress",
"test:all": "npm run test:189 && npm run test:drivers && npm run test:server && npm run test:store && npm run test:model && npm run test:regress",
"sls:deploy": "serverless deploy",
"sls:package": "serverless package",
"sls:info": "serverless info",
Expand Down
3 changes: 2 additions & 1 deletion scripts/env-check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ async function probe(name, env) {
)
console.log(
` config : format=${d.config.db_format}->${d.config.resolved_format} ` +
`driver=${d.config.db_driver}->${d.config.resolved_driver}`,
`driver=${d.config.db_driver}->${d.config.resolved_driver} ` +
`cipher=${d.config.db_cipher}`,
)
console.log(
` storage : available=${d.storage.available} memory=${d.storage.memory} connected=${d.storage.connected}`,
Expand Down
Loading
Loading