de-otio/crypto-envelope
GitHub: de-otio/crypto-envelope
为 TypeScript 应用提供带版本号的自描述认证加密信封,让开发者无需精通密码学细节即可安全地加密解密结构化数据。
Stars: 0 | Forks: 0
# @de-otio/crypto-envelope
**专用的 TypeScript 认证加密信封。** 让应用开发者能够轻松使用最佳实践的密码学技术,同时防止常见的实现错误(重用 nonce、跳过 AAD、弱 KDF、静默解密失败等)。
## 它是什么
一个位于加密原语(`@noble/*`、libsodium)之上,并在应用协议(Signal、TLS、JOSE)之下的**信封层**。接收明文负载 + 主密钥,生成具有合理默认值且可验证的带版本认证信封。该过程可逆。
该包体积小巧且具有强主见。它只做一件事:加密和解密自描述的数据块。分层密钥管理(SSH 包装、派生自密码的恢复密钥、操作系统钥匙串集成、TOFU 固定)是一个独立的关注点,将作为 [`@de-otio/keyring`](https://github.com/de-otio/keyring) 发布——在撰写本文时尚未发布。
## 它不是什么
- 不是原语库——请使用 [`@noble/*`](https://github.com/paulmillr/noble-ciphers),本包正是依赖于它。
- 不是协议库——如需完整的会话、群组或文件加密,请使用 `libsignal`、`mls` 或 `age`。
- 不是 KMS 包装器——如果你需要 KMS 支持的主密钥,请使用 `aws-encryption-sdk-js`。
- 不是 JWT/JWE 令牌库——请使用 [`jose`](https://github.com/panva/jose)。
- 不是密钥管理框架——如果你想要分层的 SSH/密码解锁、恢复 UX 或操作系统钥匙串集成,请使用 `@de-otio/keyring`(即将推出)。
## 安装
```
npm install @de-otio/crypto-envelope@alpha
```
**支持的运行时:** Node ≥22、现代浏览器(MV3 扩展和页面)、Deno ≥2、Bun ≥1、Cloudflare Workers、Vercel Edge。在 Node 上,本包使用 `sodium-native` 来实现基于 `mlock` 的安全内存(预编译二进制文件;无需额外工具链)。在浏览器和其他仅支持 WebCrypto 的运行时上,会通过 `"browser"` 字段替换为严格默认的 `SecureBufferBrowser`;构造该对象需要显式的 `{ insecureMemory: true }` 确认,因为浏览器运行时无法使用 `mlock`。
## 快速开始
```
import { EnvelopeClient } from '@de-otio/crypto-envelope';
using client = new EnvelopeClient({ masterKey: crypto.getRandomValues(new Uint8Array(32)) });
const wire = await client.encrypt({ type: 'note', body: 'hello' });
const back = await client.decrypt(wire);
// → { type: 'note', body: 'hello' }
```
`encrypt` / `decrypt` 是异步的(每个密钥的 `MessageCounter` 使用返回 `Promise` 的接口,以便持久的后端——SQLite、DynamoDB、Redis——可以接入)。`wire` 默认是紧凑型 v2 (CBOR) 线性格式的 `Uint8Array`;可以通过 `{ format: 'v1' }` 选择 v1 JSON,两者均支持无损双向转换。
### 密码解锁
```
import {
EnvelopeClient,
deriveMasterKeyFromPassphrase,
} from '@de-otio/crypto-envelope';
const masterKey = await deriveMasterKeyFromPassphrase(
'correct horse battery staple',
salt, // 16+ random bytes, persisted alongside the ciphertext
{ algorithm: 'argon2id' },
);
using client = new EnvelopeClient({ masterKey });
```
Argon2id 是规定的默认值(OWASP 2023 第二层级:t=3, m=64 MiB, p=1)。PBKDF2-SHA256 可作为兼容性后备方案,专用于受 WebCrypto 限制的运行时;其迭代次数下限为 1,000,000,并且采用此分支时会发出一次性警告。
### 用于互操作的 AES-256-GCM
```
import { EnvelopeClient } from '@de-otio/crypto-envelope';
using client = EnvelopeClient.forAesGcmInterop({ masterKey });
```
XChaCha20-Poly1305 是所有新信封的默认选项。仅当需要解密或与要求 AES-GCM 的系统(或受 FIPS 限制的环境)进行互操作时,才推荐使用 `forAesGcmInterop`。AES-GCM 带有每个密钥 2³² 条消息的上限——一旦超过此限制,客户端将通过抛出 `NonceBudgetExceeded` 拒绝进一步的加密。
`wire` 默认是紧凑型 v2 (CBOR) 线性格式的 `Uint8Array`。可以通过 `{ format: 'v1' }` 选择 v1 JSON;两者均可以通过 `upgradeToV2` / `downgradeToV1` 实现无损双向转换,并且 `decrypt()` 会自动检测格式。
如需更精细的控制,底层函数也已导出:
```
import {
encryptV1,
decryptV1,
deriveContentKey,
deriveCommitKey,
} from '@de-otio/crypto-envelope';
const cek = deriveContentKey(masterKey);
const commitKey = deriveCommitKey(masterKey);
const envelope = encryptV1({ payload: { x: 1 }, cek, commitKey, kid: 'default' });
const recovered = decryptV1(envelope, cek, commitKey);
```
## 此包的防护范围
每个功能的设计理由都可以追溯到特定类别的应用层密码学错误:
- **Nonce 重用** → 通过 XChaCha20-Poly1305 使用 192 位随机 nonce(默认)。AES-256-GCM 的 96 位 nonce 可用于互操作,并在 `EnvelopeClient` 处强制执行每个密钥 2³² 的硬性消息上限——跨进程的计数器状态可通过 `MessageCounter` 插入。公共 API 中从不允许用户提供 nonce。
- **跳过 AAD / 版本降级** → AAD 是强制性的,并绑定了版本 + 算法 + blob ID + 密钥标识符。
- **算法替换** → `alg` 绑定到 AAD 中;nonce 宽度检查会在原语层拒绝跨算法的密文。
- **多密钥 / 分区预言机攻击** → 通过带有独立域分隔字符串的 HKDF 提供专用的承诺密钥;承诺 HMAC 绑定到 blob ID;在 AEAD **之前**进行验证(密钥承诺,而非上下文承诺——详见 SECURITY.md)。
- **静默序列化漂移** → 对明文使用 RFC 8785 规范 JSON,并在加密后进行验证(每个输出在发布前都会经过解密的双向转换)。
- **弱 KDF 参数** → 强制将 OWASP-2023 第二层级的 Argon2id(t=3, m=64 MiB, p=1, dkLen=32)作为默认值。针对仅支持 WebCrypto 的运行时提供了 PBKDF2-SHA256,具有 1,000,000 次迭代的下限并在首次使用时发出警告。
- **密钥混淆** → `MasterKey` 品牌类型可防止将派生自密码的字节未经显式取消品牌转换就直接传递给 AEAD 原语作为 CEK。
- **计时攻击** → 全程使用常数时间比较(纯 JS 的 XOR 累加;可跨运行时移植)。
- **交换空间 / 崩溃转储中的密钥泄露** → 在 Node 上通过 `sodium_malloc` / `sodium_memzero` 实现 `SecureBuffer`。浏览器和其他不支持 mlock 的运行时则使用**严格默认的** `SecureBufferBrowser`,要求在构造时显式声明 `{ insecureMemory: true }`——绝无静默降级。
- **使用 `Math.random` 生成密钥** → 仅使用 `globalThis.crypto.getRandomValues`;对于安全敏感的值没有用户可调用的 RNG。在缺少 WebCrypto 时会直接抛出异常,而不是回退。
- **静默解密失败** → 在 AEAD 之前验证承诺;解密要么返回明文,要么抛出异常。
已发布的测试向量涵盖了 RFC 8785 规范化、RFC 5869 附录 A.1 HKDF-SHA256、RFC 4231 §4.3 HMAC-SHA256、`draft-irtf-cfrg-xchacha` §A.3.1 XChaCha20-Poly1305 KAT、针对 libsodium `crypto_pwhash` 的 Argon2id 跨实现 KAT、RFC 7914 §11 PBKDF2-SHA256 向量、NIST SP 800-38D / McGrew-Viega AES-256-GCM 测试用例 13–16,以及 66 个 Wycheproof 对抗性 AES-256-GCM 向量(keySize=256 / ivSize=96 / tagSize=128)。
## 错误处理
所有库错误都是 `EnvelopeError` 的实例,并带有适合 `switch` 语句的稳定 `code` 字符串。请从主入口点导入这些类:
```
import {
EnvelopeError,
AuthenticationFailedError,
UnsupportedAlgorithmError,
UnsupportedVersionError,
MalformedEnvelopeError,
TruncatedCiphertextError,
NonceBudgetExceeded,
} from '@de-otio/crypto-envelope';
```
### 错误类层次结构
```
EnvelopeError (base — code: string, message: string)
├── AuthenticationFailedError code: 'AUTHENTICATION_FAILED'
├── UnsupportedAlgorithmError code: 'UNSUPPORTED_ALGORITHM'
├── UnsupportedVersionError code: 'UNSUPPORTED_VERSION'
├── MalformedEnvelopeError code: 'MALFORMED_ENVELOPE'
├── TruncatedCiphertextError code: 'TRUNCATED_CIPHERTEXT'
└── NonceBudgetExceeded code: 'NONCE_BUDGET_EXCEEDED'
```
### 基于错误代码的分支处理
```
import { EnvelopeError, AuthenticationFailedError } from '@de-otio/crypto-envelope';
try {
const plaintext = await client.decrypt(wire);
} catch (e) {
if (!(e instanceof EnvelopeError)) throw e; // rethrow non-envelope errors
switch (e.code) {
case 'AUTHENTICATION_FAILED':
// Wrong key or tampered envelope — indistinguishable by design.
// Do NOT retry with a different key; present a generic "decryption failed" error.
break;
case 'MALFORMED_ENVELOPE':
case 'TRUNCATED_CIPHERTEXT':
// Structural problem before any key material was used.
// Log and discard; the envelope cannot be salvaged.
break;
case 'UNSUPPORTED_ALGORITHM':
case 'UNSUPPORTED_VERSION':
// Envelope was produced by a newer library version.
// Upgrade the library or reject the envelope.
break;
case 'NONCE_BUDGET_EXCEEDED':
// AES-256-GCM per-key cap reached. Rotate the master key.
break;
}
}
```
### 分区预言机防御
`AuthenticationFailedError` 是用于**所有**认证失败的唯一错误类:错误的 CEK、错误的承诺密钥、被篡改的密文、被篡改的 AAD 以及被篡改的承诺。每种情况的消息和代码都是刻意保持一致的。区分它们将允许分区预言机攻击(Len–Grubbs–Ristenpart,USENIX 2021 §4.2):拥有解密预言机访问权限的对手可以通过观察发生的失败模式,对候选密钥集进行二分搜索。调用方必须等同对待所有 `AUTHENTICATION_FAILED` 错误。
## 维护姿态
这是一个小型组织的、主要供内部使用的项目。以下是一些诚实的预期:
- **这是 [chaoskb](https://github.com/de-otio/chaoskb) 提取出来的加密层。** 设计决策首先为 chaoskb 服务;其他用例仅尽最大努力支持。
- **公开发布是为了透明度和参考**,而不是作为具有 SLA 的受支持产品。
- **鼓励分叉。** 采用 MIT 许可是有意为之。线性格式和测试向量的设计使得分叉版本能够保持互操作。
- **尽最大努力响应安全问题。** 披露流程请参见 [SECURITY.md](./SECURITY.md)。
## 开发
需要 Node 22+。
```
npm install
npm run build
npm test # fast suite (~400 ms)
npm run test:slow # Argon2id cross-implementation KAT (~15 s)
npm run lint
```
## 许可证
[MIT](./LICENSE)。
标签:AAD, AEAD, Bun, Deno, GNU通用公共许可证, JSON, libsodium, Node.js, npm包, TypeScript, Vercel Edge, WebCrypto, XML 请求, 加密, 安全开发, 安全插件, 密码学, 密钥承诺, 手动系统调用, 浏览器, 漏洞扫描器, 自动化攻击, 规范格式, 认证加密, 防误用, 零依赖