端到端加密(E2EE)
MorisBox 会加密应用内内容,因此服务器只存储密文和元数据。私钥永远不会离开用户或客服人员的设备。
加密范围
| 内容 | E2EE |
|---|---|
| 支持对话(用户 ↔ 合作伙伴客服人员) | 是 |
标记为 encryption: "e2e" 的中心消息 | 是 |
| E2EE 信封内的媒体 | 是 |
| OTP / SMS / WhatsApp 备用投递正文 | 否(这些渠道必须能够读取正文) |
| 元数据(合作伙伴、时间戳、状态和大小) | 否 |
E2E 消息的内容仅存储于应用内。如果用户在应用中不在线(大约五分钟没有设备活动),MorisBox 还会发送一条不含正文的 WhatsApp 提醒,随后通过 SMS 提醒:
您的 MorisBox 收件箱收到了来自 {partner} 的新消息。请打开应用阅读(安全消息)。
此提醒绝不会包含加密正文。OTP 和登录验证码不使用 E2EE,因此用户始终可以访问自己的账户。
协议(v1)
| 元素 | 算法 |
|---|---|
| 设备身份 | X25519 |
| 密钥协商 | ECDH(X25519) |
| KDF | HKDF-SHA-256 |
| 内容加密 | AES-256-GCM |
| 传输编码 | Base64url |
content_key → AES-GCM encrypt(plaintext JSON)
wrap(content_key) per recipient device via ECDH + AES-GCM
v1 不包含 Signal 风格的双棘轮;该安全增强已列入计划。
设备密钥目录
用户设备
登录后,客户端生成身份密钥对,并发布其中的公钥:
POST /api/v1/me/devices/keys
Authorization: Bearer <user_jwt>
Content-Type: application/json
{
"identity_key_pub": "<b64url x25519 public key>"
}
合作伙伴客服人员设备(合作伙伴中心)
POST /api/v1/agent/devices/keys
Authorization: Bearer <partner_api_key>
{
"device_uuid": "<browser session uuid>",
"identity_key_pub": "<b64url>"
}
获取手机号码的密钥
GET /api/v1/keys/+228XXXXXXXX
Authorization: Bearer <partner_api_key>
{
"phone": "+228XXXXXXXX",
"devices": [
{
"device_uuid": "...",
"identity_key_pub": "...",
"platform": "web",
"has_keys": true
}
],
"agent_devices": [ ... ]
}
发送加密中心消息
POST /api/v1/messages
Authorization: Bearer <partner_api_key>
Content-Type: application/json
{
"phone": "+228XXXXXXXX",
"encryption": "e2e",
"e2e_envelope": {
"v": 1,
"alg": "x25519-hkdf-sha256-aes-256-gcm",
"mode": "sealed",
"key_version": 0,
"sender_device_uuid": "partner-agent-…",
"ciphertext": "<b64url>",
"nonce": "<b64url>",
"aad": "seed360|msg|<ref>|v1",
"key_wraps": [
{
"device_uuid": "user-device-…",
"wrap": "<b64url>",
"wrap_nonce": "<b64url>",
"sender_eph_pub": "<b64url>"
}
]
},
"content_type": "text"
}
服务器只存储信封。body 绝不会以明文形式持久化。
错误:
| 代码 | 含义 |
|---|---|
e2e_no_devices | 用户尚未发布身份密钥 |
validation | 缺少 e2e_envelope.ciphertext / nonce |
对话消息(客户端)
POST /api/v1/me/conversations/<ref>/messages
Authorization: Bearer <user_jwt>
{
"encrypted": true,
"e2e_mode": "conversation",
"e2e_key_version": 3,
"e2e_envelope": { ... }
}
上传内容密钥包装:
POST /api/v1/me/conversations/<ref>/keys
{
"key_version": 3,
"wraps": [
{
"device_uuid": "...",
"device_kind": "user",
"wrap": "...",
"wrap_nonce": "...",
"sender_eph_pub": "..."
}
]
}
对等设备目录:
GET /api/v1/me/conversations/<ref>/devices
客户端行为
| 客户端 | 行为 |
|---|---|
| 客户端应用 / 移动端 | 登录时生成密钥;加密聊天;解密信封并显示 |
| 合作伙伴中心 | 发布客服人员密钥;加密回复 / 加密中心消息 |
| 服务器 | 路由密文;永不解密 |
UI 会在加密消息上显示挂锁标签。如果设备没有对应的密钥包装,用户会看到“无法解密”。
安全注意事项
- 私钥保留在设备上(SecureStore / 本地存储;如有条件,应逐步采用硬件支持的密钥)。
- AAD 将信封绑定到消息上下文字符串,以降低替换攻击风险。
- 已撤销设备不会包含在新的密钥包装中。
- 切勿在服务器或网关上记录解密后的正文。