XChat v3 实现笔记 — Phase 1 基础设施¶
本文记录 Phase 1(基础设施)的设计决策与字节级验证依据,配合
xchat-e2e-reverse-engineering.md协议层报告阅读。
Phase 1 范围¶
Phase 1 不触网络。完成后 SDK 在本地具备:
- 密码学原语(ECDSA / ECDH P-256,AES-GCM-256,SPKI/PKCS#8 DER 编解码)
- Thrift Binary 编解码器(与官方 web 抓包字节级一致)
- 签名 payload 构造器(CSV 域分隔,逗号校验)
- 设备密钥文件(JSON + 0600 权限 + 原子写入 + Zeroize)
XChatError错误模型(透传到TwitterError)- 共享文本验证工具(
src/common/text_validation.rs,被 dm v1/v2 复用)
模块结构¶
src/x/xchat/
├── mod.rs — 模块入口,re-export 核心类型
├── error.rs — XChatError 枚举(thiserror)
├── thrift.rs — ThriftBinaryWriter / Reader + XChat 高层 helper
├── crypto.rs — ECDSA/ECDH/AES-GCM/SPKI/PKCS#8 包装
├── payload.rs — build_signature_payload(CSV 拼接)
└── keystore.rs — XChatKeyMaterial + XChatKeyStore(JSON 持久化)
依赖关系:
关键决策¶
1. 不依赖 juicebox-sdk(推迟到 Phase 2)¶
Phase 1 只需要把密钥生成好并落地为文件;PIN 备份/恢复留 Phase 2。
本 phase 完成后,本地可以"自洽地"生成一对 keypair 并签出 identity_public_key_signature,
但还无法被服务端识别。
2. 文件版本号 version: 1¶
JSON 顶部带 version 字段。后续如果改字段名/字段含义、或换密钥格式(PEM、Encrypted PKCS#8),
将 KEYSTORE_FILE_VERSION 递增,并提供迁移路径。当前 SDK 拒绝加载未来版本,避免静默错读。
3. PKCS#8 / SPKI base64 而非 PEM¶
理由:
- 省字节:PEM 文本带 -----BEGIN PRIVATE KEY----- 包装 + 换行
- 更接近官方:抓包显示官方 web 也用 base64(DER)
- 解析方便:p256::SecretKey::from_pkcs8_der
4. Zeroize 而非加密保存¶
私钥保存为明文 base64,依赖文件系统权限(0600)保护。没有再用 PIN 加密文件,
因为 PIN 已经由 Juicebox 体系保护远端备份,本地再加密一层意义不大(增加忘 PIN 不可恢复的风险)。
内存中的私钥串通过 zeroize::ZeroizeOnDrop 析构时擦除。
5. 签名输入格式:CSV,逗号字段拒签¶
逐字段:event_type、message_id、sender_id、conversation_id、conversation_token、
base64(payload),用单个逗号分隔。任一非 base64 字段含 , 会返回
XChatError::InvalidSignaturePayload,对应官方 web 的 invalid_signature_argument 错误。
6. AES-GCM nonce 12 字节 + 16 字节 tag(RustCrypto 默认)¶
不引入 nonce 复用检测——调用方负责。Phase 3 实现会话密钥时会用「key_version × seq_id」 作为 nonce 派生输入,从机制上避免复用。
字节级验证清单¶
| 单元 | 输入 | 验证目标 | 结果 |
|---|---|---|---|
thrift::encode_text_message_event |
"xchat-test-001 hello e2e" | 86 字节与 1042_SendMessageMutation.network-request 完全一致 |
✅ |
thrift::encode_signature_envelope |
抓包字段值 | 177 字节与 fixture 完全一致 | ✅ |
payload::build_signature_payload |
抓包字段 | CSV 字节与 sign_calls[1].dataB64 完全一致 |
✅ |
crypto::EcdsaVerifyingKey::verify_raw |
抓包 signing_pub + ecdh_spki_der + identity_signature |
验签返回 Ok(()) |
✅ |
所有 fixture 引用见 tests/fixtures/xchat_e2e_evidence/(gitignored)。
既有 dm 模块的复用¶
src/x/dm/client.rs::{validate_message, validate_message_with_media, validate_batch_messages}
重构为委托给 src/common/text_validation.rs::{validate_message_input, validate_batch_message_input}。
回归:14 个 dm 验证测试全部通过,行为完全一致。
已修复的副作用¶
src/x/dm/encoder.rs::MESSAGE_SUFFIX 字节布局在 Phase 1 规划阶段曾被怀疑「偏移 1 字节」。
经字节级实测确认完全正确,无需修改([thrift::tests::encode_text_message_matches_fixture]
间接验证)。
Phase 1 不含¶
- ❌ 任何 GraphQL HTTP 调用
- ❌
AddXChatPublicKeyMutation实际发送 - ❌ Juicebox 客户端协议
- ❌ Conversation key 协商
- ❌ Python 绑定 / CLI 子命令
留给 Phase 2-4。
测试数量¶
| 模块 | 单元测试 |
|---|---|
error |
4 |
thrift |
6 |
crypto |
14 |
payload |
6 |
keystore |
9 |
common::text_validation |
9 |
| 小计 | 48 |
加上 dm 复用的 14 个回归测试,Phase 1 共 62 个测试 全部通过。 全库总数 242 个测试(193 → 242,+49 净增)。
后续工作引用¶
Phase 2: /.claude/plans/20260511-160002-XChat-v3-Phase2-Enrollment.md
Phase 3-4 计划同上目录。