# 附录 L：E2EE 实现指南（非规范性）

> **本文档为非规范性内容**：提供端到端加密的实现建议、交互流程、代码示例和安全考虑，不是协议强制要求。

## K.1 E2EE 完整交互流程

**核心原则**：
- **服务端仅转发**：Gateway 只负责转发加密消息，不参与加密/解密，无法读取消息内容
- **客户端端到端**：所有加密操作在客户端完成，密钥协商、加密、解密均在端点进行
- **算法协商优先**：由于每个 AID 可能支持多种曲线和对称加密算法，必须先协商算法再交换密钥

### K.1.1 会话建立时序图

```
Alice (发送方)                Gateway                Bob (接收方)
  │                              │                        │
  │ 1. e2ee.negotiate ────────>│                        │
  │    {to, curves, ciphers}     │                        │
  │                              │                        │
  │                              │ 2. 转发协商请求 ─────>│
  │                              │    event/e2ee.         │
  │                              │    negotiate_request   │
  │                              │                        │
  │                              │                        │ 3. 选择算法
  │                              │                        │    (取交集)
  │                              │                        │
  │                              │<──── 4. 响应协商 ──────│
  │                              │    {curve, cipher}     │
  │                              │                        │
  │<──── 5. 转发协商结果 ────────│                        │
  │    event/e2ee.               │                        │
  │    negotiate_response        │                        │
  │                              │                        │
  │ 6. 生成临时密钥对            │                        │
  │    (ephemeral keypair)       │                        │
  │                              │                        │
  │ 7. e2ee.initiate ──────────>│                        │
  │    {session_id,              │                        │
  │     ephemeral_pk}            │                        │
  │                              │                        │
  │                              │ 8. 转发密钥交换 ─────>│
  │                              │    event/e2ee.         │
  │                              │    session_request     │
  │                              │                        │
  │                              │                        │ 9. 生成临时密钥对
  │                              │                        │ 10. 执行 ECDH
  │                              │                        │     计算共享密钥
  │                              │                        │
  │                              │<──── 11. e2ee.accept ──│
  │                              │     {session_id,       │
  │                              │      ephemeral_pk}     │
  │                              │                        │
  │<──── 12. 转发密钥响应 ───────│                        │
  │     event/e2ee.              │                        │
  │     session_established      │                        │
  │                              │                        │
  │ 13. 执行 ECDH                │                        │
  │     计算共享密钥             │                        │
  │                              │                        │
  │ 14. message.send ──────────>│                        │
  │     {encrypted: true,        │                        │
  │      e2ee_session_id,        │                        │
  │      payload: {iv,           │                        │
  │       ciphertext, tag}}      │                        │
  │                              │                        │
  │                              │ 15. 转发加密消息 ────>│
  │                              │     event/message.     │
  │                              │     received           │
  │                              │                        │
  │                              │                        │ 16. 解密消息
  │                              │                        │     验证 tag
```

### K.1.2 密钥交换详细流程

**步骤 1：Alice 发起算法协商**
```javascript
// Alice 提出自己支持的算法
const myCurves = ['P-256', 'P-384', 'X25519'];
const myCiphers = ['AES-256-GCM', 'ChaCha20-Poly1305'];

const negotiateId = crypto.randomUUID();
const response = await rpc.call('e2ee.negotiate', {
  to: 'bob.aid.pub',
  negotiate_id: negotiateId,
  supported_curves: myCurves,
  supported_ciphers: myCiphers
});

// Gateway 转发给 Bob，Bob 选择算法后返回
```

**步骤 2：Bob 接收协商请求并选择算法**
```javascript
// Bob 收到 event/e2ee.negotiate_request
rpc.on('event/e2ee.negotiate_request', async (params) => {
  const { from, negotiate_id, supported_curves, supported_ciphers } = params;

  // 选择双方都支持的算法（取交集）
  const curve = selectCommonCurve(myCurves, supported_curves); // 'P-256'
  const cipher = selectCommonCipher(myCiphers, supported_ciphers); // 'AES-256-GCM'

  // 响应协商（Gateway 会转发给 Alice）
  await rpc.call('e2ee.negotiate_response', {
    negotiate_id: negotiate_id,
    curve: curve,
    cipher: cipher
  });
});
```

**步骤 3：Alice 收到协商结果并生成临时密钥对**
```javascript
// Alice 收到 event/e2ee.negotiate_response
rpc.on('event/e2ee.negotiate_response', async (params) => {
  const { from, negotiate_id, curve, cipher } = params;

  // 保存协商结果
  negotiations.set(negotiate_id, { peer: from, curve, cipher });

  // 生成临时 ECDH 密钥对（使用协商好的曲线）
  const ephemeralKeyPair = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: curve },
    true,
    ['deriveKey']
  );

  // 发起密钥交换
  const sessionId = crypto.randomUUID();
  const ephemeralPublicKey = await exportPublicKey(ephemeralKeyPair.publicKey);

  await rpc.call('e2ee.initiate', {
    to: from,
    session_id: sessionId,
    negotiate_id: negotiate_id, // 关联协商结果
    ephemeral_public_key: ephemeralPublicKey
  });

  // 保存临时密钥对
  pendingSessions.set(sessionId, {
    peer: from,
    curve: curve,
    cipher: cipher,
    ephemeralKeyPair: ephemeralKeyPair
  });
});
```

**步骤 4：Bob 接收密钥交换请求并响应**
```javascript
// Bob 收到 event/e2ee.session_request
rpc.on('event/e2ee.session_request', async (params) => {
  const { from, session_id, negotiate_id, ephemeral_public_key } = params;

  // 获取之前协商的算法
  const negotiation = negotiations.get(negotiate_id);
  const { curve, cipher } = negotiation;

  // 生成临时密钥对（使用协商好的曲线）
  const ephemeralKeyPair = await crypto.subtle.generateKey(
    { name: 'ECDH', namedCurve: curve },
    true,
    ['deriveKey']
  );

  // 执行 ECDH，计算共享密钥
  const alicePublicKey = await importPublicKey(ephemeral_public_key, curve);
  const sharedSecret = await crypto.subtle.deriveKey(
    { name: 'ECDH', public: alicePublicKey },
    ephemeralKeyPair.privateKey,
    { name: cipher.startsWith('AES') ? 'AES-GCM' : 'ChaCha20-Poly1305', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  // 保存会话密钥
  e2eeSessions.set(session_id, {
    peer: from,
    sharedKey: sharedSecret,
    cipher: cipher
  });

  // 接受会话（Gateway 会转发给 Alice）
  const myEphemeralPublicKey = await exportPublicKey(ephemeralKeyPair.publicKey);
  await rpc.call('e2ee.accept', {
    session_id: session_id,
    ephemeral_public_key: myEphemeralPublicKey
  });
});
```

**步骤 5：Alice 收到响应并计算共享密钥**
```javascript
// Alice 收到 event/e2ee.session_established
rpc.on('event/e2ee.session_established', async (params) => {
  const { from, session_id, ephemeral_public_key } = params;

  // 获取之前保存的临时密钥对和算法
  const pending = pendingSessions.get(session_id);
  const { curve, cipher, ephemeralKeyPair } = pending;

  // 执行 ECDH，计算共享密钥
  const bobPublicKey = await importPublicKey(ephemeral_public_key, curve);
  const sharedSecret = await crypto.subtle.deriveKey(
    { name: 'ECDH', public: bobPublicKey },
    ephemeralKeyPair.privateKey,
    { name: cipher.startsWith('AES') ? 'AES-GCM' : 'ChaCha20-Poly1305', length: 256 },
    false,
    ['encrypt', 'decrypt']
  );

  // 保存会话密钥
  e2eeSessions.set(session_id, {
    peer: from,
    sharedKey: sharedSecret,
    cipher: cipher
  });

  // 清理临时数据
  pendingSessions.delete(session_id);
});
```

## K.2 加密消息实现

### K.2.1 消息加密

```javascript
async function encryptMessage(sessionId, plaintext) {
  const session = e2eeSessions.get(sessionId);
  if (!session) {
    throw new Error('E2EE session not found');
  }

  // 生成随机 IV
  const iv = crypto.getRandomValues(new Uint8Array(12)); // 96-bit for GCM

  // 加密消息
  const plaintextBuffer = new TextEncoder().encode(plaintext);
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv: iv },
    session.sharedKey,
    plaintextBuffer
  );

  // 提取认证标签（最后 16 字节）
  const ciphertextArray = new Uint8Array(ciphertext);
  const dataLength = ciphertextArray.length - 16;
  const encryptedData = ciphertextArray.slice(0, dataLength);
  const tag = ciphertextArray.slice(dataLength);

  return {
    iv: arrayBufferToBase64(iv),
    ciphertext: arrayBufferToBase64(encryptedData),
    tag: arrayBufferToBase64(tag)
  };
}
```

### K.2.2 消息解密

```javascript
async function decryptMessage(sessionId, payload) {
  const session = e2eeSessions.get(sessionId);
  if (!session) {
    throw new Error('E2EE session not found');
  }

  // 解码 Base64
  const iv = base64ToArrayBuffer(payload.iv);
  const ciphertext = base64ToArrayBuffer(payload.ciphertext);
  const tag = base64ToArrayBuffer(payload.tag);

  // 合并 ciphertext 和 tag
  const combined = new Uint8Array(ciphertext.byteLength + tag.byteLength);
  combined.set(new Uint8Array(ciphertext), 0);
  combined.set(new Uint8Array(tag), ciphertext.byteLength);

  // 解密消息
  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: new Uint8Array(iv) },
    session.sharedKey,
    combined
  );

  return new TextDecoder().decode(plaintext);
}
```

### K.2.3 发送加密消息

```javascript
async function sendEncryptedMessage(to, sessionId, content) {
  // 加密消息
  const payload = await encryptMessage(sessionId, content);

  // 发送
  await rpc.call('message.send', {
    to: to,
    encrypted: true,
    e2ee_session_id: sessionId,
    payload: payload,
    timestamp: Math.floor(Date.now() / 1000)
  });
}
```

### K.2.4 接收加密消息

```javascript
rpc.on('event/message.received', async (params) => {
  if (params.encrypted) {
    // 解密消息
    const plaintext = await decryptMessage(
      params.e2ee_session_id,
      params.payload
    );

    console.log(`Encrypted message from ${params.from}: ${plaintext}`);
  } else {
    console.log(`Plain message from ${params.from}: ${params.content}`);
  }
});
```

## K.3 会话管理

### K.3.1 会话存储

```javascript
class E2EESessionManager {
  constructor() {
    this.sessions = new Map();
  }

  // 创建会话
  createSession(sessionId, peer, sharedKey, cipher) {
    this.sessions.set(sessionId, {
      peer: peer,
      sharedKey: sharedKey,
      cipher: cipher,
      createdAt: Date.now(),
      lastUsed: Date.now()
    });
  }

  // 获取会话
  getSession(sessionId) {
    const session = this.sessions.get(sessionId);
    if (session) {
      session.lastUsed = Date.now();
    }
    return session;
  }

  // 删除会话
  deleteSession(sessionId) {
    this.sessions.delete(sessionId);
  }

  // 清理过期会话（24 小时未使用）
  cleanupExpiredSessions() {
    const now = Date.now();
    const maxAge = 24 * 60 * 60 * 1000; // 24 小时

    for (const [sessionId, session] of this.sessions.entries()) {
      if (now - session.lastUsed > maxAge) {
        this.sessions.delete(sessionId);
      }
    }
  }
}
```

### K.3.2 会话重用

**推荐策略**：
- 同一对用户之间可以重用 E2EE 会话
- 会话有效期：24 小时（推荐）
- 超过有效期后自动重新协商

```javascript
async function getOrCreateE2EESession(to) {
  // 查找现有会话
  for (const [sessionId, session] of e2eeSessions.entries()) {
    if (session.peer === to) {
      const age = Date.now() - session.lastUsed;
      if (age < 24 * 60 * 60 * 1000) {
        return sessionId; // 重用现有会话
      }
    }
  }

  // 创建新会话
  return await initiateE2EESession(to);
}
```

## K.4 算法协商

### K.4.1 协商流程

**核心原则**：
- **双向协商**：发送方提出支持的算法列表，接收方从交集中选择
- **服务端转发**：Gateway 只负责转发协商消息，不参与算法选择
- **优先级策略**：接收方根据自己的优先级从交集中选择最优算法

**完整协商流程**：

```javascript
// Alice 发起协商
async function negotiateE2EE(to) {
  const negotiateId = crypto.randomUUID();

  // 提出自己支持的算法
  const myCurves = ['P-256', 'P-384', 'X25519'];
  const myCiphers = ['AES-256-GCM', 'ChaCha20-Poly1305'];

  return new Promise((resolve, reject) => {
    // 监听协商响应
    const handler = (params) => {
      if (params.negotiate_id === negotiateId) {
        rpc.off('event/e2ee.negotiate_response', handler);
        resolve({
          peer: params.from,
          curve: params.curve,
          cipher: params.cipher
        });
      }
    };

    rpc.on('event/e2ee.negotiate_response', handler);

    // 发送协商请求
    rpc.call('e2ee.negotiate', {
      to: to,
      negotiate_id: negotiateId,
      supported_curves: myCurves,
      supported_ciphers: myCiphers
    }).catch(reject);

    // 超时处理
    setTimeout(() => {
      rpc.off('event/e2ee.negotiate_response', handler);
      reject(new Error('Negotiation timeout'));
    }, 30000);
  });
}

// Bob 处理协商请求
rpc.on('event/e2ee.negotiate_request', async (params) => {
  const { from, negotiate_id, supported_curves, supported_ciphers } = params;

  // 选择算法（从交集中选择）
  const curve = selectCommonCurve(myCurves, supported_curves);
  const cipher = selectCommonCipher(myCiphers, supported_ciphers);

  // 保存协商结果
  negotiations.set(negotiate_id, {
    peer: from,
    curve: curve,
    cipher: cipher,
    timestamp: Date.now()
  });

  // 响应协商
  await rpc.call('e2ee.negotiate_response', {
    negotiate_id: negotiate_id,
    curve: curve,
    cipher: cipher
  });
});
```

### K.4.2 曲线选择

```javascript
function selectCommonCurve(myCurves, peerCurves) {
  // 优先级：P-256 > P-384 > X25519 > SM2
  const priority = ['P-256', 'P-384', 'X25519', 'SM2'];

  for (const curve of priority) {
    if (myCurves.includes(curve) && peerCurves.includes(curve)) {
      return curve;
    }
  }

  throw new Error('No common curve found');
}
```

### K.4.3 对称加密算法选择

```javascript
function selectCommonCipher(myCiphers, peerCiphers) {
  // 优先级：AES-256-GCM > ChaCha20-Poly1305 > SM4-GCM
  const priority = ['AES-256-GCM', 'ChaCha20-Poly1305', 'SM4-GCM'];

  for (const cipher of priority) {
    if (myCiphers.includes(cipher) && peerCiphers.includes(cipher)) {
      return cipher;
    }
  }

  throw new Error('No common cipher found');
}
```

### K.4.4 协商失败处理

```javascript
try {
  const negotiation = await negotiateE2EE('bob.aid.pub');
  console.log(`Negotiated: ${negotiation.curve}, ${negotiation.cipher}`);
} catch (error) {
  if (error.message === 'No common curve found') {
    console.error('对方不支持任何我们支持的曲线');
  } else if (error.message === 'No common cipher found') {
    console.error('对方不支持任何我们支持的加密算法');
  } else if (error.message === 'Negotiation timeout') {
    console.error('协商超时，对方可能离线');
  } else {
    console.error('协商失败:', error.message);
  }
}
```

## K.5 密钥导出和导入

### K.5.1 导出公钥

```javascript
async function exportPublicKey(publicKey) {
  const exported = await crypto.subtle.exportKey('spki', publicKey);
  return arrayBufferToBase64(exported);
}
```

### K.5.2 导入公钥

```javascript
async function importPublicKey(publicKeyBase64, curve) {
  const keyData = base64ToArrayBuffer(publicKeyBase64);

  return await crypto.subtle.importKey(
    'spki',
    keyData,
    { name: 'ECDH', namedCurve: curve },
    true,
    []
  );
}
```

### K.5.3 Base64 工具函数

```javascript
function arrayBufferToBase64(buffer) {
  const bytes = new Uint8Array(buffer);
  let binary = '';
  for (let i = 0; i < bytes.byteLength; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}

function base64ToArrayBuffer(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}
```

## K.6 错误处理

### K.6.1 会话建立失败

```javascript
try {
  await rpc.call('e2ee.initiate', {
    to: 'bob.aid.pub',
    session_id: sessionId,
    curve: 'P-256',
    cipher: 'AES-256-GCM',
    ephemeral_public_key: ephemeralPublicKey
  });
} catch (error) {
  if (error.code === -32101) {
    console.error('Unsupported curve, fallback to P-256');
  } else if (error.code === -32102) {
    console.error('Unsupported cipher, fallback to AES-256-GCM');
  } else {
    console.error('E2EE initiation failed:', error.message);
  }
}
```

### K.6.2 解密失败

```javascript
try {
  const plaintext = await decryptMessage(sessionId, payload);
  return plaintext;
} catch (error) {
  console.error('Decryption failed:', error.message);

  // 可能的原因：
  // 1. 会话密钥不匹配
  // 2. 消息被篡改（tag 验证失败）
  // 3. 会话已过期

  // 尝试重新建立会话
  await initiateE2EESession(peer);
  throw new Error('Message decryption failed, please retry');
}
```

## K.7 安全考虑

### K.7.1 临时密钥管理

**推荐做法**：
- 每次会话使用新的临时密钥对（ephemeral keypair）
- 会话结束后立即销毁临时私钥
- 不要重用临时密钥对

```javascript
async function cleanupSession(sessionId) {
  const session = e2eeSessions.get(sessionId);
  if (session) {
    // 清除密钥（浏览器会自动 GC）
    session.sharedKey = null;
    e2eeSessions.delete(sessionId);
  }
}
```

### K.7.2 前向保密（Forward Secrecy）

**实现方式**：
- 使用临时 ECDH 密钥交换（不使用长期密钥加密）
- 定期轮换会话密钥（推荐 24 小时）
- 历史消息无法用新密钥解密

### K.7.3 认证标签验证

**重要性**：
- 认证标签（tag）用于验证消息完整性
- 解密前必须验证 tag
- tag 验证失败说明消息被篡改

```javascript
// Web Crypto API 自动验证 tag
// 如果 tag 验证失败，decrypt() 会抛出异常
try {
  const plaintext = await crypto.subtle.decrypt(
    { name: 'AES-GCM', iv: iv },
    sharedKey,
    combined // ciphertext + tag
  );
} catch (error) {
  // Tag 验证失败或解密失败
  throw new Error('Message authentication failed');
}
```

### K.7.4 重放攻击防护

**推荐做法**：
- 在消息中包含时间戳
- 接收方检查时间戳，拒绝过期消息（如 > 5 分钟）
- 使用 nonce 防止重放（可选）

```javascript
async function sendEncryptedMessage(to, sessionId, content) {
  const timestamp = Math.floor(Date.now() / 1000);
  const message = JSON.stringify({
    content: content,
    timestamp: timestamp,
    nonce: crypto.randomUUID()
  });

  const payload = await encryptMessage(sessionId, message);

  await rpc.call('message.send', {
    to: to,
    encrypted: true,
    e2ee_session_id: sessionId,
    payload: payload,
    timestamp: timestamp
  });
}
```

## K.8 性能优化

### K.8.1 会话缓存

```javascript
// 缓存会话，避免重复协商
const sessionCache = new Map(); // key: peer_aid, value: session_id

async function sendMessage(to, content) {
  let sessionId = sessionCache.get(to);

  if (!sessionId || !e2eeSessions.has(sessionId)) {
    // 创建新会话
    sessionId = await initiateE2EESession(to);
    sessionCache.set(to, sessionId);
  }

  await sendEncryptedMessage(to, sessionId, content);
}
```

### K.8.2 批量加密

```javascript
async function sendBatchEncrypted(to, messages) {
  const sessionId = await getOrCreateE2EESession(to);

  const encryptedMessages = await Promise.all(
    messages.map(msg => encryptMessage(sessionId, msg))
  );

  // 批量发送
  await rpc.call('message.send_batch', {
    to: to,
    messages: encryptedMessages.map((payload, i) => ({
      encrypted: true,
      e2ee_session_id: sessionId,
      payload: payload,
      timestamp: Math.floor(Date.now() / 1000)
    }))
  });
}
```

## K.9 跨平台实现

### K.9.1 浏览器实现

使用 Web Crypto API（如上述示例）。

### K.9.2 Node.js 实现

```javascript
const crypto = require('crypto');

function encryptMessage(sharedSecret, plaintext) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', sharedSecret, iv);

  let ciphertext = cipher.update(plaintext, 'utf8');
  ciphertext = Buffer.concat([ciphertext, cipher.final()]);

  const tag = cipher.getAuthTag();

  return {
    iv: iv.toString('base64'),
    ciphertext: ciphertext.toString('base64'),
    tag: tag.toString('base64')
  };
}

function decryptMessage(sharedSecret, payload) {
  const iv = Buffer.from(payload.iv, 'base64');
  const ciphertext = Buffer.from(payload.ciphertext, 'base64');
  const tag = Buffer.from(payload.tag, 'base64');

  const decipher = crypto.createDecipheriv('aes-256-gcm', sharedSecret, iv);
  decipher.setAuthTag(tag);

  let plaintext = decipher.update(ciphertext);
  plaintext = Buffer.concat([plaintext, decipher.final()]);

  return plaintext.toString('utf8');
}
```

### K.9.3 Go 实现

参考 `agentcp/e2ee.go` 中的实现。

---
