# 端到端加密（E2EE）

## 概述

E2EE 是 AUN 协议的可选安全层，提供端到端内容加密。它独立于传输层 TLS 和认证层 JWT，横跨三种网络拓扑（Gateway/Peer/Relay）。

服务端（Gateway、Message、Group 等）无法解密消息内容，仅能看到加密信封的元数据。

## 加密套件

| 套件 | 状态 |
|------|------|
| `P256_HKDF_SHA256_AES_256_GCM` | 当前 Python SDK 已实现 |

## 架构

### P2P E2EE — E2EEManager

纯密码学工具类，无 I/O 依赖：

- **加密/解密**：纯计算，调用方传入证书和 prekey
- **prekey 生成**：生成密钥材料，不做 RPC 上传
- **prekey 缓存**：内置 TTL 缓存（默认 1 小时）
- **本地防重放**：内置 seen set，自动过滤重复消息

AUNClient 负责 I/O 编排：获取证书、获取/上传 prekey、服务端 replay guard（可选）。

### Group E2EE — GroupE2EEManager

与 E2EEManager 平行的群组 E2EE 工具类，封装群组密钥管理、加解密、密钥分发/恢复协议：

- **密钥管理**：create_epoch / rotate_epoch / 存储 / 读取 / 清理
- **加解密**：encrypt / decrypt / decrypt_batch（内置防重放 + 外层字段校验）
- **密钥协议**：handle_incoming（统一处理分发/请求/响应）
- **状态查询**：has_secret / current_epoch / get_member_aids

所有网络操作（P2P 发送、RPC 调用）由调用方负责。

---

## P2P E2EE

### 通过 AUNClient 发送加密消息（推荐）

`message.send` 和 `group.send` 默认加密发送，无需显式传 `encrypt=True`：

```python
# 默认加密发送
await client.call("message.send", {
    "to": "peer-agent.agentid.pub",
    "payload": {"text": "Hello!"},
})

# 发送明文消息（需显式关闭加密）
await client.call("message.send", {
    "to": "peer-agent.agentid.pub",
    "payload": {"text": "Hello!"},
    "encrypt": False,
})
```

SDK 自动获取对方证书和 prekey，加密后发送。

### 通过 AUNClient 接收加密消息

推送自动解密：

```python
client.on("message.received", lambda msg: print(msg["payload"]))
```

Pull 自动解密：

```python
result = await client.call("message.pull", {"after_seq": 0, "limit": 50})
```

### E2EEManager 底层 API（裸 WebSocket 开发者）

```python
from aun_core.e2ee import E2EEManager
from aun_core.keystore.file import FileKeyStore

e2ee = E2EEManager(
    identity_fn=lambda: my_identity,
    keystore=FileKeyStore("~/.aun/myapp"),
)

# 加密
envelope, ok = e2ee.encrypt_message(
    to_aid="peer.agentid.pub", payload={"text": "Hello!"},
    peer_cert_pem=cert_bytes, prekey=prekey_dict,
)

# 解密（内含本地防重放）
decrypted = e2ee.decrypt_message(raw_message)
```

生成 prekey（不做 RPC，返回上传材料）：

```python
material = e2ee.generate_prekey()
# {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
```

Prekey 缓存：

```python
e2ee.cache_prekey("peer.agentid.pub", prekey_dict)
cached = e2ee.get_cached_prekey("peer.agentid.pub")
e2ee.invalidate_prekey_cache("peer.agentid.pub")
```

## 两级降级策略

### 模式 1：预存临时公钥（prekey_ecdh_v2）

**优先级**：最高
**条件**：服务端有对方的 prekey
**特性**：前向安全、一消息一密钥、四路 ECDH 绑定双方身份、发送方签名

发送方获取接收方预上传的 prekey，生成临时 ECDH 密钥对，通过**四路 ECDH**（ephemeral×prekey + ephemeral×identity + sender×prekey + sender×identity）+ HKDF 派生消息密钥。每条消息使用独立的临时密钥对，并附带发送方 ECDSA-SHA256 签名。

### 模式 2：长期公钥（long_term_key）

**优先级**：降级
**条件**：对方无 prekey，但有证书；且客户端安全策略允许降级
**特性**：无严格前向安全、一消息一密钥、双 DH 绑定身份、发送方签名

使用接收方证书中的长期公钥，通过**双路 ECDH**（ephemeral×recipient_identity + sender×recipient_identity）+ HKDF 派生消息密钥，附带发送方签名。

> Python SDK 默认 `require_forward_secrecy=true`，无 prekey 时拒绝 long_term_key 降级。需显式配置才允许降级：
> ```python
> client = AUNClient({"require_forward_secrecy": False})
> ```

## Prekey 管理

SDK 连接时自动上传 prekey，定时轮换（默认每小时）。

E2EEManager 提供 `generate_prekey()` 方法生成 prekey 材料（密钥对 + 签名），私钥自动存入 keystore，公钥材料返回给调用方上传到服务端。

旧 prekey 私钥本地保留 7 天，确保在途消息可解密。

## 密文格式

### prekey_ecdh_v2 模式

```json
{
    "type": "e2ee.encrypted",
    "version": "1",
    "encryption_mode": "prekey_ecdh_v2",
    "suite": "P256_HKDF_SHA256_AES_256_GCM",
    "prekey_id": "uuid",
    "ephemeral_public_key": "base64",
    "nonce": "base64(12B)",
    "ciphertext": "base64",
    "tag": "base64(16B)",
    "sender_signature": "base64(ECDSA-SHA256)",
    "sender_cert_fingerprint": "sha256:hex",
    "aad": {
        "from": "sender.agentid.pub",
        "to": "receiver.agentid.pub",
        "message_id": "uuid",
        "timestamp": 1234567890000,
        "encryption_mode": "prekey_ecdh_v2",
        "suite": "P256_HKDF_SHA256_AES_256_GCM",
        "ephemeral_public_key": "base64",
        "recipient_cert_fingerprint": "sha256:...",
        "sender_cert_fingerprint": "sha256:...",
        "prekey_id": "uuid"
    }
}
```

### long_term_key 模式

同上，但 `encryption_mode` 为 `"long_term_key"`，无 `prekey_id` 字段，AAD 中也无 `prekey_id`。

## 防重放

- **本地 seen set**（E2EEManager 内置）：per-message_id 去重，防止同进程内重复解密
- **服务端 replay guard**（AUNClient 可选）：跨进程持久化防重放，通过 `config["server_replay_guard"]` 开启

## 防篡改

AES-GCM + AAD 验证保证消息完整性。AAD 包含 from/to/message_id/timestamp/encryption_mode/suite/ephemeral_public_key/recipient_cert_fingerprint/sender_cert_fingerprint（prekey 模式还包含 prekey_id），任何字段被篡改都会导致解密失败。

接收端还会强制验证 `sender_signature`（ECDSA-SHA256），缺少签名或验签失败的消息会被拒绝。

## 安全约定

- 加密失败时不静默降级为明文，由上层应用显式决定
- 每条消息独立临时密钥对，用完即丢
- Prekey 私钥通过 FileKeyStore 加密存储（Windows DPAPI / 跨平台 SecretStore）

---

## 群组 E2EE

### 概述

群组 E2EE 基于 **Epoch Group Key** 机制：

- 每个群组维护一个 `group_secret`（256 位对称密钥）和 `epoch`（版本号）
- 每条群消息从 `group_secret + message_id` 独立派生加密密钥（HKDF）
- 成员变更时通过 CAS 轮换 epoch，生成新的 `group_secret`
- `group_secret` 通过 P2P E2EE 通道分发（复用 prekey_ecdh_v2 / long_term_key）

**无状态设计**：每条消息独立可解密，不依赖历史状态，不会断链。

### 必选能力

群组 E2EE 是**必选能力**。所有客户端必须声明 `capabilities.group_e2ee: true`。服务端当前仅对在线客户端做能力校验；离线客户端入群时不做强制检查。消息是否加密由发送者自主决定。

### SDK 自动编排

`group.send` 默认加密发送，SDK 自动处理密钥管理：

| 触发场景 | SDK 自动行为 |
|---------|-------------|
| `group.create` | 创建 epoch 1 密钥 |
| `group.add_member` | P2P E2EE 分发密钥给新成员 |
| `group.kick` | 服务端 CAS 轮换 epoch + 分发新密钥 |
| `group.review_join_request`(approve) | 分发密钥给新成员 |
| `group.leave` | 离开者自身不执行轮换；剩余在线 admin/owner 收到 group.changed 事件后自动 CAS 轮换 + 分发新密钥 |
| 收到加密消息但无密钥 | 自动发起密钥恢复请求 |
| 收到 P2P 密钥分发/请求/响应 | 自动处理 |

### GroupE2EEManager 底层 API（裸 WebSocket 开发者）

```python
from aun_core.e2ee import GroupE2EEManager
from aun_core.keystore.file import FileKeyStore

group_e2ee = GroupE2EEManager(
    identity_fn=lambda: my_identity,
    keystore=FileKeyStore("~/.aun/myapp"),
)

# 创建群组密钥
info = group_e2ee.create_epoch("grp_abc", member_aids=["alice.aid", "bob.aid"])
# 返回 {"epoch": 1, "commitment": "<sha256 hex>", "distributions": [{"to": "bob.aid", "payload": {...}}]}

# 加密群消息
envelope = group_e2ee.encrypt("grp_abc", payload={"text": "Hello"}, message_id="gm-uuid")

# 解密群消息（内含防重放 + 外层字段校验）
decrypted = group_e2ee.decrypt(raw_message)

# 轮换 epoch
new_info = group_e2ee.rotate_epoch("grp_abc", new_member_aids)
# 返回格式与 create_epoch 相同：{"epoch": N, "commitment": "...", "distributions": [...]}

# 查询状态
group_e2ee.has_secret("grp_abc")       # 是否持有密钥
group_e2ee.current_epoch("grp_abc")     # 当前 epoch
group_e2ee.get_member_aids("grp_abc")   # 已知成员列表
```

### 密钥分发/恢复协议

通过 P2P E2EE 通道传输，payload 类型：

**分发**（`e2ee.group_key_distribution`）：
```python
{
    "type": "e2ee.group_key_distribution",
    "group_id": "grp_abc",
    "epoch": 2,
    "group_secret": "base64",
    "member_aids": ["alice.aid", "bob.aid"],
    "commitment": "3f2a...e91c",
    "distributed_by": "alice.aid",
    "distributed_at": 1234567890000,
    "manifest": {
        "manifest_version": 1, "group_id": "grp_abc", "epoch": 2,
        "prev_epoch": 1, "member_aids": [...], "added": [...], "removed": [...],
        "initiator_aid": "alice.aid", "issued_at": 1234567890000,
        "signature": "base64(ECDSA-SHA256)"
    }
}
```

**请求**（`e2ee.group_key_request`）：
```python
{
    "type": "e2ee.group_key_request",
    "group_id": "grp_abc",
    "epoch": 2,
    "requester_aid": "charlie.aid"
}
```

**响应**（`e2ee.group_key_response`）：
```python
{
    "type": "e2ee.group_key_response",
    "group_id": "grp_abc",
    "epoch": 2,
    "group_secret": "base64",
    "member_aids": [...],
    "commitment": "3f2a...e91c"
}
```

SDK 通过 `GroupE2EEManager.handle_incoming(msg)` 统一处理这三种消息。

### 群组密文格式

```json
{
    "type": "e2ee.group_encrypted",
    "version": "1",
    "encryption_mode": "epoch_group_key",
    "suite": "P256_HKDF_SHA256_AES_256_GCM",
    "epoch": 2,
    "nonce": "base64(12B)",
    "ciphertext": "base64",
    "tag": "base64(16B)",
    "sender_signature": "base64(ECDSA-SHA256)",
    "sender_cert_fingerprint": "sha256:hex",
    "aad": {
        "group_id": "grp_abc",
        "from": "sender.agentid.pub",
        "message_id": "gm-uuid",
        "timestamp": 1234567890000,
        "epoch": 2,
        "encryption_mode": "epoch_group_key",
        "suite": "P256_HKDF_SHA256_AES_256_GCM"
    }
}
```

**密钥派生**：
```
msg_key = HKDF-SHA256(
    ikm  = group_secret,
    info = "aun-group:{group_id}:msg:{message_id}",
    length = 32
)
```

**外层字段校验**：解密时 MUST 校验外层 `group_id`、`sender_aid`/`from` 与 AAD 中的值一致。
