XChat v3 Phase 2 — Juicebox SDK 适配 Spike 结果¶
状态:✅ 兼容,无需 fork 结论:直接使用
juicebox_sdkv0.3.4 (git tag0.3.4,commit3ac7202) 通过 git 依赖引入 完成时间:2026-05-11
关键发现¶
1. crates.io 不发布 juicebox_sdk¶
官方 SDK 仓库为 juicebox-systems/juicebox-sdk,是个 workspace,
不发布到 crates.io。引入方式:
juicebox_sdk = {
git = "https://github.com/juicebox-systems/juicebox-sdk",
tag = "0.3.4",
features = ["tokio", "reqwest"],
optional = true,
}
注意 crate 名是 juicebox_sdk(下划线),不是 juicebox-sdk。
2. 版本必须精确锁 0.3.4¶
X 服务端 realm-*.x.com/req 请求头:
实测来自 tests/fixtures/xchat_e2e_evidence/965_juicebox_realm_west1_1.network-request 抓包。不要随便升级——0.3.5 / 0.3.6 可能改了 Noise handshake 或 OPRF 流程。
3. 子模块 SSH 拉取陷阱¶
仓库带一个子模块 artifacts(测试 fixtures),URL 写死为 [email protected]:...(SSH)。
首次 cargo check 会失败:
修复:用 git URL 重写:
git config --global url."https://github.com/".insteadOf "[email protected]:"
这是开发机一次性配置。CI 环境可以注入 .cargo/config.toml:
并提前在 CI 步骤里运行上面的 git config。
4. X 服务端 Configuration JSON 直接兼容 SDK 类型¶
X 通过 AddXChatPublicKeyMutation 响应里的 key_store_token_map_json 字段把 Juicebox Configuration 以 JSON 字符串塞回来:
{
"realms": [
{"id":"69d282a74c3d5a35589cd61ccf6ed878", "address":"https://realm-b.x.com/"},
{"id":"97429dc9061ad7da70fa8fe4ea4e2ed6", "address":"https://realm-east1.x.com/",
"public_key":"e8b2205c63e448514a7579b1fc338e1f7442739ce3fd47fdafb916890d3e1341"},
{"id":"450f6886b8539dd5a1711024c780eae1", "address":"https://realm-west1.x.com/",
"public_key":"ca02aea58fd9529383fc179ccb1f8d3d80a63072567a78352568c2256a49821a"}
],
"register_threshold": 3,
"recover_threshold": 2,
"pin_hashing_mode": "Standard2019"
}
这个 JSON 直接 deserialize 成 juicebox_sdk::Configuration(用 Configuration::from_json 或 serde_json::from_str)。
SDK 的 Configuration 字段顺序、命名、PinHashingMode::Standard2019 全部一一对应。
5. 三个 realm 中一个没 public_key 是 by-design¶
realm-b.x.com 没 public_key,意味着该 realm 不强制 Noise NK 加密握手;SDK 用纯 TLS。
另外两个有 public_key 的 realm 走完整 Noise NK 25519 ChaChaPoly BLAKE2s 握手。SDK
内置处理这两种情况,无需我们干预。
6. JWT token 来自 X 服务端,不需要 SDK 内部生成¶
每个 realm 对应一个 JWT:
alg: HS256✓ (SDK 支持)kid: xcorp:1— 自定义 keyid 表明 X 是 Juicebox 的"租户"iss: xcorp— tenant 名sub: <user_id>:<timestamp>— 把 user_id + 时间戳作为 secret_idaud: <realm_id>— 每个 realm 一个 tokenexp - nbf <= 3600s(实测 600s)
SDK 的 AuthTokenManager trait 让我们把这 3 个固定 token 注入进去:
struct StaticAuthTokens(HashMap<RealmId, AuthToken>);
impl AuthTokenManager for StaticAuthTokens {
async fn get(&self, realm: &RealmId) -> Option<AuthToken> { self.0.get(realm).cloned() }
}
7. 依赖增量评估¶
引入 juicebox_sdk 后会拉入:
- reqwest 0.11 + hyper-rustls 0.24 + tokio-rustls 0.24 — 新增 HTTP 栈(与现有 rquest 共存)
- argon2 0.5 — PIN hashing
- blake2 0.10 + chacha20poly1305 0.10 — Noise transport
- curve25519-dalek / x25519-dalek — Noise key agreement
- jwt-simple 0.11 — JWT 解析(仅依赖)
- juicebox_oprf / juicebox_noise / juicebox_realm_api / juicebox_realm_auth / juicebox_networking / juicebox_marshalling / juicebox_secret_sharing
wheel 体积增量预估 ≈ 2-3 MB(release strip)。Phase 4 发版前重新评估,必要时启用 lto = "fat"。
替代方案(已否决)¶
| 选项 | 否决理由 |
|---|---|
| 自实现 Juicebox v0.3.4 protocol | OPRF + Noise + Secret Sharing + Argon2 完整实现需 5-7 工作日,且没有第三方独立验证,安全风险大 |
| Fork SDK 移除 SSH 子模块 | 没有理由——git config 重写一次性解决,且子模块只含测试 fixture,不影响 SDK 库代码 |
| 用 SDK 但本地用 rquest 替换 reqwest | SDK 的 networking 抽象暴露 http::Client trait,技术上可适配。留作 Phase 4 优化,不阻塞 Phase 2 |
下一步(Phase 2 实施)¶
A2 - A4:
- A2 在 src/x/xchat/juicebox.rs 封装 SDK,提供:
- XChatJuiceboxClient::from_server_configuration(json: &str, tokens: HashMap<RealmId, String>) 构造
- async fn register(pin: &str, secret: &[u8]) -> Result<()>
- async fn recover(pin: &str) -> Result<Vec<u8>>
- async fn delete() -> Result<()>
- A3 单元测试:用 SDK 提供的软件 realm 测试 register/recover round-trip(不需要真实 X 服务器)
- A4 集成测试(Phase 2 末尾):用真实账号 mamadoufri23588 跑一次完整 enroll → recover
参考:
- Juicebox 白皮书
- SDK README
- 抓包 tests/fixtures/xchat_e2e_evidence/{962,965,966,967,968,969,970}_*