这是什么
「理科计算器·验证器」是一份约 950 KB 的单 HTML 文件应用,加密逻辑与计算逻辑混在一起,谁都用不了、也没法单独审。这里把密码学部分完整抽出来,重写成两个不依赖宿主代码的包——一个纯算法层,一个带会话生命周期的完整安全库。
还嫌麻烦?core 打包产物 / lib 打包产物 直接 npm install ./xxx.tgz。
密码学设计
| 环节 | 算法 / 参数 |
|---|---|
| 口令派生 | PBKDF2-HMAC-SHA256,600,000 次迭代(OWASP 2023 建议值) |
| 密钥分离 | HKDF-SHA256,按用途分域(会话 / 文件 v3 / v4 / v5 / TOTP / Argon2id) |
| 对称加密 | AES-256-GCM,96 位 IV,128 位认证标签,元数据走 AAD 认证 |
| 会话密钥 | 不可导出的 CryptoKey,JS 拿不到原始字节 |
| 校验口令 | 与加密密钥分离派生,验证值不可反推密钥 |
| 可选 KDF | Argon2id(内存密集,抗 GPU/ASIC),需宿主注入 WASM 实现 |
信封是自描述的
iterations、salt、iv、version 都写在密文头部,所以升级 KDF 强度不会让旧数据失效——旧信封按它自己记录的迭代数解开,新信封用新强度。
| 版本 | 密钥来源 | 说明 |
|---|---|---|
| v2 | 口令直派生 | 最早期格式,仍可解 |
| v3 | 口令派生 + HKDF | 引入文件密钥分域 |
| v4 | 会话密钥 + AAD | 元数据参与认证,防篡改 |
| v5 | Argon2id | 内存密集,需注入实现 |
快速开始
// 纯算法层
import * as sci from './core/sci-secure-core.js'
const env = await sci.encryptWithPassword({ name: '张三' }, '我的密码')
const back = await sci.decryptWithPassword(env, '我的密码')
// 完整安全库
import { SecureVault } from './lib/index.js'
const vault = new SecureVault()
await vault.register('alice', '密码123')
const blob = await vault.encrypt({ note: '机密内容' })
vault.lock()
用别的语言解开
密文结构写在 FORMAT-SPEC.md 里,附实测向量;reference-impl-python.py 是照规范写的 Python 实现,已经和 JS 版互相解通过。也就是说,数据不锁死在浏览器里。
安全边界(请务必读)
这套东西保护的是拿到密文的人,不是拿到你设备的人。下面这些它做不到:
- JS 字符串清不掉。
zeroize()只能清Uint8Array;口令一旦变成字符串就交给 GC 了。语言限制,不是偷懒。 - 本机被控则无解。内存里有明文和会话密钥,扩展、注入脚本、调试端口都能读。
- 登录退避可绕过。退避记录在 localStorage,清掉或直接对着密文爆破即可跳过;真正的下限由 KDF 强度决定。
- Argon2id 默认没生效。WebCrypto 没有 Argon2,必须自己注入 WASM 实现,否则走 PBKDF2。
- 口令强度仍是最弱一环。迭代数翻倍只是把单次猜测成本提高约一倍,弱口令照样被字典打穿。
- 需要安全上下文。
crypto.subtle只在 HTTPS 或 localhost 下可用。
把一个加密模块公开,不会削弱它——安全性建立在密钥和口令上,不在代码保密上(Kerckhoffs 原则)。公开的价值是能被审查,而不是让实现烂在文件里。这个仓库里没有任何私钥、口令或真实用户数据。
版本
| 版本 | 变更 |
|---|---|
| 1.1.0 | PBKDF2 默认 310,000 → 600,000(向后兼容);新增 v5/Argon2id 信封、防钓鱼安全短语、zeroize;lib 增加登录退避、自动锁定、disposeTotp、Trusted Types 策略 |
| 1.0.0 | 从主程序剥离,core 与 lib 双包成型 |