跳转至

XChat v3 实现笔记 — Phase 1 基础设施

本文记录 Phase 1(基础设施)的设计决策与字节级验证依据,配合 xchat-e2e-reverse-engineering.md 协议层报告阅读。

Phase 1 范围

Phase 1 不触网络。完成后 SDK 在本地具备:

  1. 密码学原语(ECDSA / ECDH P-256,AES-GCM-256,SPKI/PKCS#8 DER 编解码)
  2. Thrift Binary 编解码器(与官方 web 抓包字节级一致)
  3. 签名 payload 构造器(CSV 域分隔,逗号校验)
  4. 设备密钥文件(JSON + 0600 权限 + 原子写入 + Zeroize)
  5. XChatError 错误模型(透传到 TwitterError
  6. 共享文本验证工具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 持久化)

依赖关系:

error ←── thrift
       ←── crypto ←── keystore
           payload

关键决策

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_typemessage_idsender_idconversation_idconversation_tokenbase64(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 计划同上目录。