跳转至

XChat v3 Phase 2 — Juicebox SDK 适配 Spike 结果

状态:✅ 兼容,无需 fork 结论:直接使用 juicebox_sdk v0.3.4 (git tag 0.3.4,commit 3ac7202) 通过 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 请求头:

x-juicebox-version: 0.3.4

实测来自 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 会失败:

failed to start SSH session: Failed getting banner; class=Ssh

修复:用 git URL 重写:

git config --global url."https://github.com/".insteadOf "[email protected]:"

这是开发机一次性配置。CI 环境可以注入 .cargo/config.toml

[net]
git-fetch-with-cli = true

并提前在 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_jsonserde_json::from_str)。 SDK 的 Configuration 字段顺序、命名、PinHashingMode::Standard2019 全部一一对应。

5. 三个 realm 中一个没 public_key 是 by-design

realm-b.x.compublic_key,意味着该 realm 不强制 Noise NK 加密握手;SDK 用纯 TLS。 另外两个有 public_key 的 realm 走完整 Noise NK 25519 ChaChaPoly BLAKE2s 握手。SDK 内置处理这两种情况,无需我们干预。

6. JWT token 来自 X 服务端,不需要 SDK 内部生成

每个 realm 对应一个 JWT:

eyJhbGciOiJIUzI1NiIsImtpZCI6Inhjb3JwOjEifQ.eyJpc3MiOiJ4Y29ycCIsInN1YiI6IjE5Nzz...
  • alg: HS256 ✓ (SDK 支持)
  • kid: xcorp:1 — 自定义 keyid 表明 X 是 Juicebox 的"租户"
  • iss: xcorp — tenant 名
  • sub: <user_id>:<timestamp> — 把 user_id + 时间戳作为 secret_id
  • aud: <realm_id> — 每个 realm 一个 token
  • exp - 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}_*