Devious-Creations/E2EE-Core
GitHub: Devious-Creations/E2EE-Core
面向移动端 E2EE 应用的密码学核心库,提供密钥层次结构、X25519+SAS 配对握手、中继消息 ratchet 及认证封装,确保服务器仅持有密文。
Stars: 0 | Forks: 0
# e2ee-core — 一个端到端加密核心
[](https://github.com/Devious-Creations/E2EE-Core/actions/workflows/test.yml)
这是一个端到端加密移动应用的加密核心:服务器**仅以密文形式**存储用户数据、关系("dynamic")数据、证明图像以及中继消息。密钥永远不会离开用户的设备。本包正是实现这一目标的代码——密钥层次结构、配对握手、消息 ratchet 以及封装例程——除此之外没有别的。
我们**仅使用经过审计的标准原语**——没有自行滚动的加密或哈希算法:
| 库 | 用途 |
| --- | --- |
| [`tweetnacl`](https://github.com/dchest/tweetnacl-js) | XSalsa20-Poly1305 (`secretbox`),X25519 密钥协商 (`box.before`) |
| [`@noble/hashes`](https://github.com/paulmillr/noble-hashes) | scrypt,PBKDF2-SHA256,SHA-256,HMAC-SHA256 |
| `tweetnacl-util` | base64 / UTF-8 编解码器 |
## 运行
```
npm install
npm test # node --test — no jest/babel toolchain to trust
npm run demo # end-to-end walk-through of every protocol
```
要求 Node ≥ 20。测试套件针对内存 adapter 运行每一个协议,因此无需任何配置。
## 包含的范围(以及不包含的内容)
此包**仅包含密码学部分**。应用所做的两件事特意*没有*包含在此处,因为它们属于传输/存储,而不是加密:
- **静止状态下的密钥存储位置** —— 应用使用设备 keychain
(`expo-secure-store`)。在这里它对应 [`KeyStore`](./src/interfaces.js)
接口。在 [`adapters/`](./adapters) 中有一个内存实现。
- **消息如何流动** —— 应用使用 Supabase(Realtime 用于配对握手,Storage 用于证明 blob,Postgres 用于加密平面)。在这里,配对握手需要一个 [`Transport`](./src/interfaces.js);其他所有操作都只是*将密文返回*给调用者以供存储/发送。
这种分离正是其核心意义所在:**与安全相关的逻辑完全不知道服务器是什么。** 切换到内存 adapter,每个协议都会在一个进程中离线运行(这正是测试所做的事情)。
## 密钥层次结构
```
password ──scrypt(N=2^16,r=8,p=1)──▶ KEK ──secretbox──▶ [ wrapped DEK ] (stored server-side)
│
recovery code ──pbkdf2(10k)──▶ recovery-KEK ──secretbox──▶ ┘ (alternate unwrap path)
│
DEK (random 32B)
│
┌─────────────────────────────────────┼───────────────────────────┐
▼ ▼ ▼
encrypts the user's wraps K_shared_i (per device, cached
cloud backup blob (one per relationship) in the KeyStore)
│
pairing handshake (X25519 + SAS) ──▶ K_pair ──delivers K_shared_i once──▶ partner
│
└──▶ root of the relay message ratchet (chain keys via HMAC)
```
- **DEK**(Data Encryption Key,随机 32 字节)加密用户自己的云备份。它由通过 **scrypt** (v3: N=2¹⁶, r=8, p=1 —— 内存困难型,约 64 MiB) 从密码派生的 **KEK** 包装。只有*被包装的* DEK 存储在服务器端;本库从不传输密码或解包后的 DEK。
(边界说明:嵌入此核心的应用在普通账户登录期间可能仍会向其 auth 提供商发送相同的密码——我们的应用确实如此——因此此处的保证仅涵盖保险库密钥路径,而非应用的整个 auth 流程。)在 v1 (PBKDF2-10k) 或 v2 (scrypt N=2¹⁵/r=8/p=3, 约 32 MiB) 下写入的保险库仍可打开——存储的 `kdf` 描述符指明了版本——并在下一次成功解包密码时重新包装为 v3。
- **恢复码**(8 个代码,从 32 个无易混淆符号的字母表中提取的 12 个字符 ≈ 每个 60 位)通过成本更低的 KDF (**PBKDF2**-SHA256, 10k) 包装 DEK —— 高熵不需要拉伸。
- **K_pair** 来自交互式**配对握手**:由 **Short Authentication String** 进行带外认证的临时 X25519 交换。它既是 K_shared 的一次性投递密钥,也是**中继 ratchet 的根**。
- **K_shared**(随机 32 字节,每个关系 / "dynamic" 一个)加密共享数据平面。每个成员将其存储在各自*自己的* DEK 下包装(一个“自有授权”),并且它由创建者→接受者仅投递一次,在 K_pair 下封装。
## 协议
| 模块 | 功能 |
| --- | --- |
| [`primitives.js`](./src/primitives.js) | 经过审计的库之上的轻量级、平台无关封装。其他所有内容均由此构建。 |
| [`keyVault.js`](./src/keyVault.js) | DEK 生成、scrypt/PBKDF2 KEK 派生、DEK 包装/解包、恢复码以及按关系存储密钥(通过 `KeyStore`)。 |
| [`sealing.js`](./src/sealing.js) | 在 32 字节密钥下对 JSON 对象或原始 blob 进行认证加密(备份和证明图像封装),以及封装到已发布的 X25519 公钥的加密盒子。纯函数。 |
| [`dynamicKeys.js`](./src/dynamicKeys.js) | 在两个配对成员之间配置 K_shared,**带有 AAD 绑定**(见下文)。 |
| [`pairing.js`](./src/pairing.js) | 在不可信的 `Transport` 上执行产生 K_pair 的交互式 X25519 握手 + SAS。 |
| [`ratchet.js`](./src/ratchet.js) | 基于 K_pair 的对称密钥消息 ratchet:HMAC 链密钥、每条消息的密钥、跳过的密钥处理、重放拒绝。 |
### 封装与 AAD 绑定
所有封装都使用 **XSalsa20-Poly1305** (`secretbox`):每条消息使用一个新的随机 24 字节 nonce,对密文进行认证(`open` 在出现*任何*篡改或密钥错误时抛出异常)。`secretbox` **没有 associated-data 插槽**,因此在我们需要将密文绑定到上下文的地方,我们将上下文放置**在经过认证的明文内部**并在打开时进行检查。`dynamicKeys` 对 K_shared 授权执行此操作:一个授权是 `secretbox({ d: dynamicId, k: K_shared })`,如果绑定的 `d` 不同,解包将**拒绝**该 blob —— 否则恶意服务器可能会在用户的两个 dynamic 之间交换授权(两者都在同一个 DEK 下,因此简单的交换也能顺利解包)。这种“明文中的 AAD”结构是我们最希望征求第二意见的内容之一。
### 封装盒(到公钥)
`sealing.sealBox` 将字节封装给接收者的*已发布* X25519 公钥,无需预先建立关系:临时密钥对 → X25519 共享密钥 → `secretbox`,临时公钥随密文一起传输。tweetnacl 没有 `crypto_box_seal`;这是等效的构造,使用显式的随机 nonce 而不是从两个公钥派生的 nonce。**封装盒不携带发送者身份验证** —— 接收者必须将打开失败视为预期输入,并且任何身份声明都必须由调用者在封装的 payload *内部*进行绑定。
### 配对 (X25519 + SAS)
两台设备通过**不可信的** transport 交换临时 X25519 公钥,并派生出共享根 K_pair。由于 transport 是不可信的,身份验证来自于两方在带外进行比较的 **Short Authentication String** —— 匹配的 SAS ⇒ 不存在中间人攻击。有关确切的承诺顺序和 SAS 派生,请参见 [`pairing.js`](./src/pairing.js)。
### 中继 ratchet
配对设备之间的消息使用以 K_pair 为根的**对称密钥 ratchet** 进行加密:链密钥通过 HMAC 推进,每条消息获得一个新的消息密钥,已使用的密钥将从设备状态中删除(密钥清理,而非前向保密 —— 见下文),乱序消息通过有界的跳过密钥缓存进行处理,并拒绝重放。
**请注意其实际的局限性:** 这是一个源于静态根的 *chain-key* ratchet,而不是带有每条消息 Diffie-Hellman 的完整 Double Ratchet。它**不提供归档的前向保密** —— K_pair 被保留,并且两条链都从计数器 0 确定性地重新派生,因此设备或 keychain 的受损会暴露任何仍然存在且需要解密的密文(受限于中继的保留窗口,而不是 ratchet)。而且它**不**提供妥协后安全性(受损的链状态在重新配对之前将保持受损状态)。我们认为对于此应用的模型来说,这是一个可接受的权衡,但这正是审计应该质疑的那种决策。
清理/取消配对将存活的链状态替换为带有指纹的**墓碑**:随后 ratchet 会拒绝为*相同的*根密钥重新派生链,因此内存中陈旧的根密钥副本无法悄悄地“复活”一个“已销毁”的链并解密仍然归档的历史记录(密码学擦除)。真正的重新配对会轮换根密钥——不同的指纹——并正常初始化新的链。
## 威胁模型
**服务器(以及任何网络观察者)不能做的事情:**
- 读取用户的备份、共享关系数据、证明图像或中继消息内容。它仅持有密文 + nonce。
- 获取密码、DEK、K_pair 或任何 K_shared。
- 悄悄地将加密授权从一个用户的关系交换到另一个关系中(AAD 绑定会拒绝此操作)。
- 在不产生用户会注意到的 **SAS 不匹配**的情况下对配对进行中间人攻击。
**我们信任的内容:**
- **设备 keychain**(`KeyStore` 实现)用于静态密钥的保密。
如果设备被完全入侵,其密钥将被暴露——此包不能防御已被 root 的设备。
- 两个人进行的**带外 SAS 比较**以验证配对。
- **经过审计的原语**(`tweetnacl`,`@noble/hashes`)是正确的。
- 正常工作的平台 **CSPRNG**(`globalThis.crypto.getRandomValues`)。
**明确未受保护的内容(已知限制——请仔细审查):**
- **元数据。** 谁与谁配对、消息时间以及消息/blob 大小对服务器/transport 可见。这不是一个元数据私密系统。
- 中继 ratchet 上的**妥协后安全性**(见上文)。
- **备份前向保密。** DEK 是长期有效的;对其进行破解会暴露备份。这是密码可恢复备份的固有特性。
- **transport 的可用性/排序。** Ratchet 容忍重新排序和间隙,但其本身不保证交付。
- **封装盒发送者真实性。** 封装盒不能证明发送者的身份;调用者必须在经过认证的 payload 内部绑定任何身份声明。
## 我们特别希望审查的内容
1. `dynamicKeys.js` 中**明文内的 AAD 绑定** —— 将 context id 嵌入经过认证的明文中是否是此处真正的 AAD 的合理替代方案?有什么方法可以绕过 `d !== dynamicId` 检查吗?
2. `pairing.js` 中的**配对握手** —— 承诺顺序、SAS 派生和长度,以及在攻击者控制的 transport 上的任何反射/未知密钥共享/降级角度。
3. `ratchet.js` 中的 **ratchet** —— 链密钥派生、nonce/计数器处理、跳过的密钥缓存边界(`MAX_SKIPPED_KEYS`)以及重放拒绝是否无懈可击。
4. **KDF 参数** —— scrypt N=2¹⁶/r=8/p=1(约 64 MiB)对于 2025 年的手机是否是一个合适的选择,包括在低端 Android 上那是一块连续分配的内存?恢复码的熵(约 60 位)和 PBKDF2-10k 是否站得住脚?
5. **Nonce 清理** —— 每个 `secretbox` nonce 都是全新的 24 个随机字节;在固定密钥下是否存在任何可能重复使用 nonce 的路径?
6. **封装盒构造** —— `sealBox` 是由 `box.before` + `secretbox` 和显式随机 nonce 重建的 `crypto_box_seal`(tweetnacl 缺少该原语)。它是否是一个忠实的等价物,并且对于这种构造通常的使用方式来说,无发送者身份验证的警告是否陈述得足够充分?
7. **消费者接缝** —— 注入了 `KeyStore` 和 `Transport`。*符合规范但具有对抗性*的实现(撒谎、丢弃或重放的 transport;重新排序或丢失写入的密钥存储)是否会打破威胁模型所声称的任何保证?
## 报告发现
请**不要**就疑似漏洞发布公开 issue。请改用 GitHub 的私有漏洞报告:在此仓库中选择 **Security → Report a vulnerability**。我们将在此处协调披露。非常欢迎以普通 issue / PR 的形式提供非敏感反馈(文档、样式、测试想法)。
## 许可证
[Apache-2.0](./LICENSE)。
标签:GNU通用公共许可证, MITM代理, Node.js, X25519, 加密组件, 密码学, 手动系统调用, 数据可视化, 端到端加密, 自定义脚本