feat: 手表免密加入账号(桌面端签发 + 手机端确认转交) - #2193
Merged
Merged
Conversation
A watch has no keyboard, so it cannot log into a BitFun account the way every other surface does. Add `provision_peer_device`: a paired client asks the desktop over the already-encrypted room channel to register the watch on the account, and the desktop answers with a full credential. Why not reuse `GetDelegatedIdentity`: that yields a 24-hour delegated token, which suits a phone that can re-authenticate on its own. The watch is a primary surface with no way to re-enter a password, so it needs the 30-day full credential the relay's `/api/auth/provision-device` mints. `request_id` is minted by the device being provisioned rather than by the desktop, so a retry anywhere along the watch -> phone -> desktop chain replays one idempotent relay request instead of registering a second device on the account. The relay never sees `master_key`: it travels back inside the room channel's own encryption, which the relay only forwards. The provisioner holds the same account lease as delegation and re-checks the account generation on both sides of the network call, so an account switch mid-flight fails the request instead of minting a credential against the account that just went away. Covered by 7 tests in `service::remote_connect::tests`: provisioning requires a trusted pairing, is bound to the account established during that pairing, rejects a provider for another account, rejects a credential minted for a different device, rejects device ids the relay would refuse, surfaces the relay's own failure reason, and holds the account lease until the response is released. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The phone is the only device in the chain that can both reach the desktop over a QR-paired room channel and show a human the request. It relays a watch's provisioning request to the desktop, asks for approval, and hands the minted credential to the watch over distributed KV. The approval card sits above every route rather than inside a settings screen: a watch is waiting on the answer, and burying it behind whatever screen the phone happens to be on turns a five-second confirmation into a support question. `provisionPeerDevice` goes straight to the room client instead of through `send()`. The account-device transport reaches a different desktop handler that has no trusted pairing identity and would answer "not available on this host"; `hasRoomChannel()` lets the caller find that out before offering the feature rather than after a 45-second wait. The outcome is reported, not thrown: "the desktop answered and refused" carries a message worth showing verbatim, while "no answer at all" covers both an offline desktop and one too old to know the command — an older build decrypts the payload, fails to deserialize the unknown variant, and replies with nothing. An `Error` cannot carry that distinction back through the transport's error collapsing. `master_key` is never written to the distributed KV in the clear. The watch publishes an ephemeral X25519 public key with its request, and the phone wraps the key to it, so the KV holds ciphertext only the requesting watch can open. Bundle name changes from the scaffold's `com.example.bitfun_mobile` to `com.bitfun.app`, shared with the watch. `distributedKVStore` isolates stores by bundleName + storeId, so two different bundles get two unrelated databases and the handoff cannot work at all. The cost is that an existing install becomes a different app: no data carries over and users re-login. The HAP signing profile has to be regenerated for the new bundle before this ships. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
手表没有键盘,无法像其他端一样登录 BitFun 账号。这个 PR 打通「桌面端签发凭证 → 手机端确认并转交 → 手表直接可用」这条链路,手表端不出现任何输入框。
桌面端(Rust)
新增
provision_peer_device命令:已配对的客户端通过已加密的 room channel 请求桌面端把手表注册到账号上,桌面端调用 relay 的/api/auth/provision-device后把签发的凭证原路返回。GetDelegatedIdentity:那条给的是 24 小时委托令牌,适合手机——它自己能重新登录。手表是主设备且无法再输密码,需要 30 天完整凭证。request_id由被授权的设备生成,不是桌面端生成。手表 → 手机 → 桌面端这条链路上任意一环重试,重放的都是同一个幂等 relay 请求,不会在账号里多注册一台设备。master_key:它走 room channel 自己的加密,relay 只做转发。手机端(HarmonyOS)
手机是链路里唯一既能通过扫码 room channel 够到桌面端、又能把请求展示给人看的设备。
provisionPeerDevice直连 room client 而非走send():account-device 通道到的是桌面端另一个处理器,那里没有可信配对身份,只会回 "not available on this host"。hasRoomChannel()让调用方在提供功能之前就知道这件事,而不是等 45 秒超时。Error没法把这个区别带过传输层的错误收敛。master_key不以明文进入分布式 KV:手表随请求发布一次性 X25519 公钥,手机把密钥包给它,KV 里只有那块手表能解开的密文。com.example.bitfun_mobile→com.bitfun.app,与手表端共用。distributedKVStore按 bundleName + storeId 隔离,两个不同 bundle 拿到的是两个互不相干的库,凭证交接物理上跑不通——所以这不是清理脚手架命名,是功能前提。代价:已安装的旧包等于另一个 app,数据不通、用户需要重新登录;HAP 签名 profile 需要按新 bundle 重新生成后才能发版。
验证
cargo test -p bitfun-core provisioning→ 7 passed, 0 failed:覆盖「必须先有可信配对」「绑定配对时确立的那个账号」「拒绝别的账号的签发方」「拒绝签给另一台设备的凭证」「拒绝 relay 会拒的 device_id 形状」「原样透出 relay 的失败原因」「响应释放前一直持有账号租约」。cargo check -p bitfun-core/-p bitfun-services-integrations均干净。e63084bc5),无冲突,rebase 后重跑测试结果一致。