如何理解 Motrix 浏览器扩展配对协议 MBP1:SPAKE2 首次配对与 AEAD 安全通道
【免费下载链接】MotrixA full-featured download manager.项目地址: https://gitcode.com/GitHub_Trending/mo/Motrix
如果你要开发、审查或二次接入 Motrix 浏览器扩展与 Motrix bridge server 之间的连接,需要理解的是 MBP1——一份把密码学参数钉死到字节级的 wire 契约(protocolVersion = 1)。它解决一个具体问题:早期 Native Messaging 时代靠 0600 权限的endpoint.json隐式证明"连的就是用户自己的 Motrix",而改用固定候选端口段 16802–16806 之后,这个证明失效了。MBP1 用四组机制替代它:基于 SPAKE2 的首次配对(口令是 Motrix 显示、用户手动输入进扩展的短时效配对码)、基于长期对称凭据mutualKey的重连 challenge–response、握手后每一帧都包裹在 AES-256-GCM envelope 内的安全信道、以及 Native Messaging 环境下的一次性 attestation ticket。本文按开发者视角走通其中两条主线——SPAKE2 首次配对与 AEAD 安全通道——并给出用规范测试向量校验实现的方法。规范全文见 MBP1 中文规范 与 英文版。
先固定两个前提:角色分配与唯一 ciphersuite
角色分配是 [RFC 9382] §3.1 的约定:扩展是 A 方(发起方,使用点M),Motrix bridge server 是 B 方(应答方,使用点N)。
MBP1 v1 只支持恰好一个 ciphersuite,没有协商、没有降级路径;不支持它的对端直接握手失败(fail closed):
| 组件 | 选择 |
|---|---|
| 群G | edwards25519(RFC 8032),基点为 RFC 8032 基点,阶ℓ = 2^252 + 27742317777372353535851937790883648493,cofactorh = 8 |
| PAKE | SPAKE2,ciphersuite SPAKE2-edwards25519-SHA256-HKDF-HMAC |
| Hash / KDF / MAC | SHA-256 / HKDF-SHA-256 / HMAC-SHA-256 |
| MHF(口令→标量) | scrypt,N=2^14, r=8, p=1, dkLen=64 |
| AEAD | AES-256-GCM,12 字节 nonce,16 字节 tag |
| 签名(ticket 绑定) | Ed25519 |
固定点M、N取 RFC 9382 §6 的 edwards25519 常量(32 字节 RFC 8032 编码):
M = d048032c6ea0b6d697ddc2e86bda85a33adac920f1bf18e1b0c6d166a5cecdaf N = d3bfb518f44f3430f29d0c92af503865a1ed3281dc69b35dd868ba85f886c4ab实现来源有硬约束(规范 §3):曲线运算、SPAKE2 组合、scrypt、Ed25519 必须来自捆绑库并精确锁版本——TypeScript 侧为@noble/curves@2.4.0与@noble/hashes@2.4.0(即生成规范向量所用的版本)。MUST NOT使用 WebCrypto 的 X25519/Ed25519:它要求 Chrome 133 / Firefox 130,而扩展最低支持 Chrome 120+ / Firefox 121+。对称原语(AES-256-GCM、HKDF、HMAC)MAY 使用 WebCrypto。所有涉密比较必须 constant-time,标量乘法必须走库的 secret-scalar 路径。
进入配对:四个入口面与一个硬性门
bridge server 监听 loopback,绑定 16802–16806 中第一个空闲端口(全占用时降级 ephemeral 端口)。与 MBP1 相关的四个面:
| 面 | 方法 | 认证 | 用途 |
|---|---|---|---|
/discovery | GET | 无 | 探测 hint:这里有没有 Motrix、是哪个实例 |
/nonce | POST+ 自定义头 | 无 | 一次性配对 nonce 签发 |
/pair | WS upgrade,?nonce= | MBP1 首次配对 | code-entry PAKE 配对 |
/v1 | WS upgrade,URL 无凭据 | MBP1 重连 | challenge–response 会话 |
两个入口级规则值得在实现前先记住:
- 路由即 demultiplexing:
/pair只接受首配状态机,/v1只接受重连状态机;不存在 token-only 模式、?token=查询参数或 legacy 帧格式。认证前连接只停留在带硬性 deadline 与容量上限的 pre-authentication 表中,绝不进入 live-session map。 - 两条 WebSocket 路由都要求
motrix-bridge.v1subprotocol:客户端 MUST 在Sec-WebSocket-Protocol中提供它,否则在检查路由之前、消耗任何 nonce 之前就以401拒绝。 POST /nonce必须携带自定义头X-Motrix-Bridge: 1(使请求成为 non-simple 请求,跨源网页被浏览器 preflight 拦截,server 不授予任何 CORS)。nonce 一次性、60 秒过期、只被/pair消费。- 绑定 loopback 期间,所有路由 MUST 拒绝
Host不严格等于127.0.0.1[:port]、localhost[:port]或[::1][:port]的请求(403),封死 DNS rebinding。
GET /discovery返回的 JSON(instanceId、appVersion、extensionPairing.protocol: "mbp1"等字段)只是 hint,MUST NOT 据此授予信任或做 legacy 降级;端口 pin 只能在该端口上完成双向认证的 MBP1 会话之后提交。
SPAKE2 首次配对:消息流程与口令派生
信道激活前,/pair上的所有消息都是单个 WebSocket文本帧,内容为恰好一个带type判别字段的 JSON 对象,二进制字段用 base64url。未知type、乱序、重复、超大帧(认证前 > 16 KiB)或 schema 校验失败一律以protocolViolation中止连接。完整流程(规范 §6.1):
extension (A) Motrix (B) | | |-- pairHello ------------------------------>| 校验 nonce、origin、 | | ticket;排队审批对话框 |<------------------------------- pairAccept | (对话框显示配对码) | | | 用户在 Motrix 窗口读取配对码 | | 用户在扩展中输入配对码 | | | |-- pakeA {pA} ----------------------------->| |<----------------------------- pakeB {pB} | |-- confirmA {cA, ticketProof?} ------------>| 验证 cA(+ proof) |<----------------------------- confirmB {cB}| | 验证 cB | |============ AEAD 信道激活 ==================| |<------------------------- credentialOffer | | 持久化到 storage.local | |-- credentialAck --------------------------->| 持久化 commit |<--------------------- credentialCommitted | |============ MDXP initialize... ============|下文 wire 示例中的<...>均为每次连接的运行时值(UUID、随机字节等),不是固定字符串。pairHello是 A 发出的首帧:
{ "type": "pairHello", "protocolVersion": 1, "browser": "chromium" | "firefox", "claimedExtensionId": "<store ID or Gecko ID>", "clientInstallationId": "<UUIDv4 persisted in storage.local>", "nmTicket": { ... }, "ticketBindingKey": "<b64url 32-byte Ed25519 public key>" }nmTicket可选;携带时ticketBindingKey必填。server 收到后按序处理:校验?nonce=(无效则在后续工作之前关 socket)→ 校验 Host 与 Origin → 在执行任何会话状态或对话框之前做 pending-pair 去重、全局上限与 backoff → 若带nmTicket则先于 key confirmation 校验 ticket(要求bindingPub == ticketBindingKey、callerId == claimedExtensionId)→ 解析身份三态 → 排队恰好一个审批对话框。pairAccept只表示"对话框已排队",不携带批准语义——扩展 MUST NOT 把任何 server 消息当作"用户已批准",只有 key confirmation 成功才是证明。
配对码即 PAKE 口令
配对码(规范 §7)的格式:Crockford base32 字母表0123456789ABCDEFGHJKMNPQRSTVWXYZ(排除I、L、O、U),8 个符号恰好 40 bit,取自 5 个 CSPRNG 字节;展示为大写XXXX-XXXX,只显示在 Motrix 审批对话框中。配对码就是 PAKE 口令:MUST 绝不以任何形式经任何网络信道传输、绝不写入日志。扩展侧输入先本地规范化(去连字符空格、转大写、映射O→0、I→1、L→1)再要求恰好 8 个字母表符号;本地校验失败的输入直接在 popup 拒绝,不产生网络流量、不消耗尝试次数。
生命周期:每个配对会话一个码,在最早到达的时点作废——生成后120 秒、对话框关闭、WebSocket 关闭、或第 3 次失败尝试。第 3 次失败后 server 发pairError {code:"rateLimited"}并关闭 socket。尝试计数由双方独立记录,断连不会重置。
码 → 标量w的派生(§6.2),pw是规范化后 8 个 ASCII 字节,pairNonce是本连接消费的那个 ASCII nonce 原文:
salt = "MBP1/w/v1" ‖ UTF8(pairNonce) h = scrypt(pw, salt, N=2^14, r=8, p=1, dkLen=64) w = OS2IP(h) mod ℓpairNonce使w会话唯一。若w = 0则以pairingFailed中止(概率约 2^-252,无重试语义)。scrypt 是 RFC 推荐的 MHF,同时封死在配对码存活期内对主动攻击 transcript 记录的离线穷举。
SPAKE2 计算、transcript 与 key schedule
按 RFC 9382 §3.3(A = 扩展,B = Motrix):
- A 以拒绝采样从
[1, ℓ)均匀抽x(抽 32 个 CSPRNG 字节按大端解释,值为 0 或 ≥ ℓ 时重抽)。X = x·P,pA = w·M + X;B 同法抽y,Y = y·P,pB = w·N + Y。 - 收到的点 MUST 能按 RFC 8032 规范编码解码为曲线上点,否则以
protocolViolation中止。 - A 计算
K = h·x·(pB − w·N),B 计算K = h·y·(pA − w·M),h = 8;K为单位元则中止(计一次失败尝试)。 x、y每次协议运行必须新抽、绝不复用;PAKE 状态只存内存,运行以任何方式结束即销毁。
TranscriptTT把双方身份与全部密码学材料绑在一起(§6.4):
A_id = enc("MBP1/A/v1") ‖ enc(browser) ‖ enc(verifiedOrigin) ‖ enc(claimedExtensionId) ‖ enc(clientInstallationId) B_id = enc("MBP1/B/v1") ‖ enc("motrix-bridge") ‖ enc(instanceId) TT = enc(A_id) ‖ enc(B_id) ‖ enc(pA) ‖ enc(pB) ‖ enc(K) ‖ enc(I2OSP(w, 32))enc(s) = len64LE(s) ‖ s(8 字节小端长度前缀),verifiedOrigin取 WebSocket upgrade 的Origin头 ASCII 值。AAD 进一步绑定 nonce 与 ticket:
AAD = encU32BE(protocolVersion) ‖ enc(pairNonce) ‖ enc(ticketBindingKeyOrEmpty) ‖ enc(ticketDigestOrEmpty)任何 origin、browser、ID、nonce 或 binding key 的调换(misbinding)都会让双方 AAD 或 TT 失配,从而在 key confirmation 处失败——这是"篡改不静默降级"的结构性保证。
Key schedule(RFC 9382 §4,SHA-256):
Ke ‖ Ka = SHA-256(TT) (各 16 字节) KcA ‖ KcB = HKDF-SHA-256(ikm=Ka, salt=empty, info="ConfirmationKeys" ‖ AAD, L=32) (各 16 字节) cA = HMAC-SHA-256(KcA, TT) cB = HMAC-SHA-256(KcB, TT)A 先发cA;B MUST 在发cB之前验证cA(带 ticket 时还须验证ticketProof——对"MBP1/ticket-proof/v1" ‖ TT的 Ed25519 签名,按 RFC 8032 strict 规则、zip215: false验证)。两处验证均 constant-time。验证失败计一次失败尝试,B 回复pairError {code:"codeMismatch", attemptsRemaining},配对码仍存活时 MAY 在同一连接以全新x重跑pakeA。
尝试限制双方独立执行:扩展自行执行每配对会话至多 3 轮协议运行、自pairHello起 180 秒绝对 deadline、以及全局失败 backoff(min(30 · 2^(n−1), 3600)秒,成功配对或 24 小时后重置),无论对端报告什么——server 发来的attemptsRemaining是不可信展示数据,MUST NOT 用来放宽本地限制。
配对会话 traffic key 与两阶段凭据提交
双向 confirmation 通过后(§6.6):
kC2S = HKDF-SHA-256(ikm=Ke, salt="MBP1/pair/v1", info="MBP1-pair-traffic-c2s", L=32) kS2C = HKDF-SHA-256(ikm=Ke, salt="MBP1/pair/v1", info="MBP1-pair-traffic-s2c", L=32)MBP1 中每个 HKDF/HMAC 调用都携带全局唯一 label,key 分离从不依赖 IKM 或 salt 的偶然差异。此后连接上所有帧(凭据消息与 MDXP 一视同仁)都在 AEAD envelope 内传输。
信道内的凭据签发是两阶段提交(§6.7):B 先以provisional状态持久化凭据再发credentialOffer {credentialId, mutualKey};A 写入storage.local后,在发送credentialAck之前先持久化unacked → commit-uncertainwrite-ahead,然后才发 ack;B 持久化标记committed后回credentialCommitted。这套顺序保证 ack/commit 窗口内任意位置的崩溃都留下可重连状态。凭据 principal 是{browser, verifiedOrigin, clientInstallationId},第二个浏览器 profile 是新 principal,互不影响;过期或被吊销的凭据一律要求重新 code-entry 配对,绝不静默重新信任。
AEAD 安全通道:envelope 结构、重放保护与用量上限
envelope 在/pair上自 §6.6 起、在/v1上自重连成功起激活,双向、覆盖每一帧,位于 MDXP 之下(MDXP JSON-RPC 载荷字节即明文,原样不动):
frame = seq64BE ‖ AES-256-GCM(key = k_dir, nonce, plaintext, aad) nonce = dirTag(4 字节 BE) ‖ seq64BE (12 字节) dirTag = 0x00000001(client→server)| 0x00000002(server→client) aad = "MBP1/env/v1"(ASCII,11 字节)实现要点(§10):
- 帧是 WebSocket二进制消息,一条消息一帧;信道激活后的文本帧是协议违例。
seq每方向从 0 开始、每帧恰好加 1。接收方 MUST 要求seq等于本地期望计数(严格单调、无窗口);任何跳号、重复或 GCM 认证失败 MUST 立即关闭连接。重放保护就是这条严格序号检查。- key 按方向独立,nonce 唯一性由构造保证。但唯一性不等于用量上界:连接 MUST 在任一方向超过2^24 帧或2^30 个已加密 AES block(16 GiB 明文)(先到为准)之前关闭——经重连重建、派生新 key。v1 不做原地 rekey。
- 单帧明文上限 1 MiB;server 的 WebSocket parser MUST 把单条消息限制在 1 MiB + 8 字节序号 + 16 字节 tag,防止未认证对端先迫使缓冲超大消息。认证前更严格的 16 KiB 帧限制由状态机独立执行。
信道激活之后不再有pairError——envelope 违规与用量上限都通过关闭码报告:1002(协议违规,统一码,从不指明失败项)、4001(达到用量上限,双方皆无失当,客户端按重连流程重建)、1011(关闭方自身内部故障)。客户端 MUST NOT 依据关闭码分支:任何已建立 envelope 信道的关闭都意味着"经 §8 重建",关闭码只让日志可读,不承载协议状态。
重连路径(§8)与首配共用同一套成帧规则:/v1的 URL 不携带任何凭据,server 先发reconnectChallenge {S},客户端回reconnectResponse {credentialId, C, mac},其中mac = HMAC-SHA-256(mutualKey, "MBP1-R/c" ‖ S ‖ C ‖ RT);成功后reconnectAccept.mac由客户端在发送任何其他内容之前验证,失败即视为"不是我的 Motrix"(清端口 pin、回退扫描/重新配对)。对未知credentialId与错误 MAC,server 行为完全一致(dummy key 做 constant-time 验证、统一回复authFailed后关闭),使该面不成为凭据 ID oracle。
边界:MBP1 不防御什么
理解协议前先接受它的边界(§1.1),避免高估保证。不在范围内:同 UID 本地代码(可ptraceMotrix、读storage.local)、root 或具备 raw-socket / eBPF 能力的代码、共享 X11 输入注入、被攻破的浏览器。MBP1 必须挡住的是共宿主机的不同 UID 用户:他能抢占 loopback 候选端口并主动连接或中继,但无法读取其他 UID 的 0600 文件,也无法被动截获其他 UID 已建立的 loopback 流。透明中继(抢占端口、把一场 PAKE 会话逐帧转发给真正的 Motrix)只能建立一条他自己读不到的端到端密钥——AEAD 信道拒绝其读取、伪造、篡改;它留在路径中的存在、可见流量大小与时序,是 loopback 端口抢占的固有残留,文档如实记录,不声称已关闭。
审批对话框侧另有身份三态(§5):official(Chromium verified Origin 或有效 NM ticket 的callerId命中不可变 allowlist src/shared/config/native-messaging-extensions.json)、attested-non-official(有效 ticket 证明了确切 ID 但不在 allowlist)、unverified(无 attestation 的 Firefox/pair或候选段扫描到的对端)。"官方"身份只读 allowlist,绝不读 NM manifest 集合。
验证实现:规范测试向量与参考生成器
规范 §13 给出了可执行的验证路径。跨实现向量是规范性的,位于 bridge-pairing-protocol-vectors.json(与本规范同目录),全部字节串为 hex 编码,包含五组:
spake2— 固定输入(code、pairNonce、身份,以及像 RFC 向量一样直接给出的w、x、y)下的完整首配运行及期望pA、pB、K、TT、Ke、Ka、KcA、KcB、cA、cB、traffic key。因为 RFC 9382 Appendix B 只提供 P-256 向量,通用 SPAKE2 核心的实现 MUST 先通过全部四组RFC P-256 向量(证明 TT 布局与 key schedule 正确),再信任 edwards25519 实例化。scryptW— 配对码规范化与w派生(§6.2)。reconnect—RT、客户端与 server MAC、traffic key(§8)。nmTicket—ticketKey派生、规范 MAC、ticketDigest,外加 weak binding-key 拒绝用例(单位元、small-order 点、dirty/non-torsion-freebindingPub均 MUST 被拒;S ≥ ℓ与非规范R签名 MUST 被 strict 验证拒绝)。envelope— 给定 key/明文下的 AEAD 帧,含期望拒绝用例:错误序号、篡改密文、仅翻转 dirTag(key 不变)——忽略dirTag的实现无法蒙混过关。
向量由参考生成器产出,脚本已入库:scripts/generate-bridge-pairing-vectors.mjs。它分两步:先用全部四组 RFC 9382 Appendix B P-256 向量验证通用 SPAKE2 核心,再生成 MBP1 edwards25519 向量并自检(含 strict Ed25519 验证与 weak binding-key 拒绝)。直接运行方式(<output.json>替换为你自己的输出路径):
node scripts/generate-bridge-pairing-vectors.mjs /tmp/mbp1-vectors.json注意副作用:npm script 入口pnpm run generate:bridge-vectors的默认输出路径是仓库内的 docs/bridge-pairing-protocol-vectors.json,会直接覆写这份规范性文件。规范同时要求"给定记录输入,重新生成 MUST 是确定性的"——所以更稳妥的做法是把向量写到仓库外路径,再与仓库内文件 diff,确认字节级一致。
向量文件之外,实现测试套件 MUST 覆盖向量无法表达的有状态用例:双侧尝试上限(§6.5/§7.2)、跨断连的全局计数(§7.3)、以及轮换的崩溃点(§6.7)。仓库内的参考实现位于 src/core/bridge/mbp1/,可对照阅读:spake2-core.ts(SPAKE2 组合)、pairing-code.ts(码的生成与规范化)、scrypt-w.ts(w派生)、transcript.ts(TT/AAD)、envelope.ts与envelope-message-stream.ts(envelope 编解码与序号检查)、ticket-verify.ts(NM ticket 校验)、reconnect-mac.ts与reconnect-session.ts(重连 MAC 与状态机),每个模块旁都有同名*.test.ts。
升级依赖前先过 gate
规范 §14 的实现 gate 里有一条直接影响二次开发:任何@noble/curves/@noble/hashes版本升级都要重跑全部向量并重新开启密码学审查门禁——2.4.0 这一精确组合的依赖审查是在 2026-09-03 经独立审查批准后才满足的(附录 C 的 Dep-pin-2 记录:全部四组 RFC P-256 向量复现、310 项 MBP1 单元测试与 77 项回环传输测试通过)。同理,复用 PAKE 标量或改变 threat model 也会重新开启审查。实现侧还需遵守两条日志纪律:任何日志级别都 MUST NOT 记录配对码、w、PAKE 中间值、key、MAC 或 ticket;server MUST NOT 泄露codeMismatch/attemptsRemaining之外的内部失败步骤。
核对完成的路径是:实现对照五组规范向量全部通过 → SPAKE2 核心先过 RFC 9382 全部四组 P-256 向量 → 有状态用例(尝试上限、全局计数、轮换崩溃点)有独立测试覆盖 → 依赖版本精确锁定 2.4.0 并记录审计依据。满足这些,你的实现就落在规范定义的验收边界内;协议本身不覆盖的残留(透明中继的路径存在性、同 UID 本地代码)则按 §1.1 的威胁模型理解,不写进"已解决"。
【免费下载链接】MotrixA full-featured download manager.项目地址: https://gitcode.com/GitHub_Trending/mo/Motrix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考