11 · 配对与 SSP 安全机制
核心文件:
MiBluetoothPairingController.kt、view/MiBluetoothPairingDialogFragment.kt、view/MiBluetoothPairingDialogActivity.kt适用范围:配对理论与 MANNPROB-2956 根因。本仓代码均为src/main单份(不受 flavor 影响)。 关联:02-配对流程 · 实证/home/zbc/下载/test/车机JIRA/jira-data/MANNPROB-2956/analysis/root-cause.md
1. 配对的本质:密钥交换 + 持久化
蓝牙「配对(pair)」不是建立链路,而是双方协商并持久化共享密钥——之后每次连接用这把钥匙鉴权,免得反复走 SSP。两条物理传输对应两种钥匙:
- BR/EDR:Link Key(128-bit P-256 鉴权组合钥)。2956 实测
16:24:44.855 btm_sec_link_key_notification: New link key generated key_type:8+Storing link key. key_type=0x8(key_type=8 = AUTH_COMB_P_256,Secure Connections 鉴权组合钥)。 - LE:LTK(Long Term Key)+ IRK(身份解析钥),由 SMP 分发。
落盘位置(2956 实测纠正常见误解):16:24:47.856 LegacyConfigFile: Config created with path parameter: /data/misc/bluedroid/hci1_bt_config.conf。小米双适配器路径是 /data/misc/bluedroid/hci1_bt_config.conf(hci1_ 前缀 + bluedroid/ 目录,非标准 /data/misc/bluetooth/bt_config.conf)。Fluoride 格式按设备 MAC 分段(如 [xx:xx:xx:xx:b0:4e]),段内是 LinkKey=/LE_KEY_PENC=/LE_KEY_PID= 等 key-value,没有 [LinkKeys]/[LeKeys] 聚合段。
配对完成在框架里叫 bond。BluetoothDevice 三个常量刻画状态机:
| 常量 | 数值 | 含义 |
|---|---|---|
BOND_NONE | 10 | 未配对;失败/超时/removeBond 都回此态 |
BOND_BONDING | 11 | 配对进行中;createBond() 后立即进入 |
BOND_BONDED | 12 | 已配对;密钥已落盘 |
stateDiagram-v2 [*] --> BOND_NONE BOND_NONE --> BOND_BONDING : createBond()br/MiBluetoothUtils.kt:262 BOND_BONDING --> BOND_BONDED : SSP完成+密钥落盘br/框架广播 ACTION_BOND_STATE_CHANGED BOND_BONDING --> BOND_NONE : cancelBondProcess()br/仅 BONDING 有效 BOND_BONDED --> BOND_NONE : removeBond()br/唯一撤销已完成配对的动作 note right of BOND_BONDED: BONDED 广播被 BondStateMachinebr/"先 SDP 再广播"延迟 ~1.67sbr/(2956: 44.862→46.534)
关键非对称:
BOND_BONDING→BOND_NONE可由cancelBondProcess完成;但BOND_BONDED→BOND_NONE只能由removeBond完成。这是 2956 全部血泪的根源。
2. SSP 四模式与 MITM 防护
SSP(Secure Simple Pairing,BR/EDR 2.1+)把老式 PIN 换成四个关联模型(由双方 IO capability 协商选定):
| SSP 模式 | 用户交互 | MITM 防护 | 典型场景 |
|---|---|---|---|
| Just Works | 无 | ❌ 无 | 耳机、无屏设备 |
| Numeric Comparison | 双方各显 6 位数字,两侧都点确认 | ✅ 有 | 双屏手机/车机(2956 走这条,variant=2) |
| Passkey Entry | 一侧显 6 位 passkey,另一侧输入 | ✅ 有(逐 bit 验证) | 一侧有屏无键盘 |
| Out of Band (OOB) | NFC/二维码等带外通道传密钥 | ✅ 有(最强) | 设备贴一下即配对 |
flowchart TD Start([SSP 触发]) --> Q1{双方都有屏?} Q1 -- 否 --> Q2{有输入能力?} Q1 -- 是 --> NC[Numeric Comparison<br/>显6位数字 双侧确认 MITM✅] Q2 -- 都没有 --> JW[Just Works<br/>无交互 MITM❌] Q2 -- 一方输入 --> PE[Passkey Entry<br/>一侧显一侧输入 MITM✅] Q2 -- 有OOB通道 --> OOB[OOB<br/>带外传密钥 MITM✅] NC --> Done([密钥生成 落盘]) JW --> Done PE --> Done OOB --> Done
3. Android 配对变体(PAIRING_VARIANT_*)与 UI 映射
框架把 SSP 模式 + 老 PIN 编码到 EXTRA_PAIRING_VARIANT。MiBluetoothPairingController.getDialogType()(:94-114) 归并成三种 UI:
PAIRING_VARIANT_* | 数值 | 对应 SSP 模式 | Controller 归类 | 实际 UI |
|---|---|---|---|---|
PIN | 0 | 老 PIN | USER_ENTRY_DIALOG(0) | 输入框 |
PASSKEY | 1 | Passkey Entry(输入侧) | USER_ENTRY_DIALOG(0) | 输入 6 位 |
PASSKEY_CONFIRMATION | 2 | Numeric Comparison | CONFIRMATION_DIALOG(1) | 显 6 位配对码,仅确认钮(2956 即此) |
CONSENT | 3 | Just Works | CONFIRMATION_DIALOG(1) | 不显配对码,仅确认 |
OOB_CONSENT | 6 | OOB | CONFIRMATION_DIALOG(1) | 仅确认 |
DISPLAY_PASSKEY | 4 | Passkey Entry(显示侧) | DISPLAY_PASSKEY_DIALOG(2) | 只读,自动确认 |
DISPLAY_PIN | 5 | Numeric Comparison(显示侧) | DISPLAY_PASSKEY_DIALOG(2) | 只读,自动确认 |
PIN_16_DIGITS | 7 | 老 PIN(16位) | USER_ENTRY_DIALOG(0) | 输入 16 位 |
⚠️ variant 命名鸿沟(排障必读):bugreport 同一回调有两个 variant——native
sspRequestCallback pairingVariant 0(btif 枚举,0=PASSKEY_CONFIRMATION,行205446)与 AppsendPairingRequestIntent variant=2(BluetoothDevice 常量,行205453)。Android 常量名PASSKEY_CONFIRMATION(2)在 BR/EDR Secure Connections 下承载的就是 SSP Numeric Comparison UI(显示 6 位、Yes/No,pairingAlgorithm=SC)。grep 日志看到 native”variant 0”≠ App”variant 2”,勿混。
关键代码位点(均 src/main 单份):getDialogType():94-114、formatKey():56-69(variant 2/4→%06d,5→%04d)、onPair():244-281(自动 accept 在 :265-269 PASSKEY_CONFIRMATION/CONSENT→setPairingConfirmation(true))、notifyDialogDisplayed():209-220(DISPLAY_* 自动确认)。
4. 配对完整时序(2956 实证)
下图时间戳全部取自 2956 bugreport 主 txt(行号见 root-cause.md §4.1,本节已按对抗审查修正 44.855/45.087 落盘语义)。
sequenceDiagram autonumber participant U as 用户 participant App as CarSettings participant FW as BluetoothAdapter participant Stack as bluedroid participant Act as MiBluetoothPairingDialogActivity participant Ctrl as MiBluetoothPairingController Note over U,Stack: ① 发起 U->>App: 点击可用设备 App->>FW: createBond() (40.698) FW-->>App: BOND_NONE→BOND_BONDING (40.701, 3ms 即时) Note over U,Stack: ② SSP 协商 Stack->>Stack: is_auto_accept_ssp=false (43.084)<br/>决定权上抛 App FW-->>Act: ACTION_PAIRING_REQUEST variant=2 (43.086, 配对码196742) Note over U,Stack: ③ 车机自动 accept(关键) Act->>Ctrl: isRequestPairAsClient=true<br/>createMiCarConfirmationDialog :179-182 Ctrl->>FW: setPairingConfirmation(accept=true) (43.184→43.186, 仅2ms) Note over Ctrl,FW: Numeric Comparison 被App主动绕过<br/>降级为 Just Works 语义 Note over U,Stack: ④ BR/EDR Link Key 落盘 Stack->>Stack: BOND_BONDED (44.860 应用层广播未发) Stack->>Stack: wait for service discovery UUIDs (44.862)<br/>广播被主动推迟 Stack->>Stack: Storing link key key_type=0x8 (44.855)<br/>BR/EDR Link Key 落盘 Note over U,Stack: ⑤ CTKD 派生 LE 密钥 Stack->>Stack: SMP_BR 跨传输密钥派生 (45.087)<br/>从 Link Key 派生 LE LTK + 存 IRK Note over U,Stack: ⑥ 用户取消(迟到) U->>Act: 点击取消 (46.336) Act->>Ctrl: onCancel (Fragment:232) Note over Ctrl,Stack: 历史实现:仅 cancelBondProcess Ctrl->>Stack: cancelBondProcess (46.339) Stack-->>Stack: State:IDLE Res:11 空转 Note over U,Stack: ⑦ 被延迟的 BONDED 广播 FW-->>Act: ACTION_BOND_STATE_CHANGED BONDED (46.534) Act->>Act: 走配对成功分支 dismiss(isPairSuccess=true) Act->>Act: postDelayed 150ms 拉起 CarPlay 弹窗 (46.552) Stack-->>Stack: HFP+A2dpSink 自动连接 (46.583)
5. 跨传输密钥分发:CTKD(BR→LE 方向)
方向纠正(对抗审查+主控 bugreport 核实):不是”LTK 经经典通道分发”,而是 BR→LE 方向的跨传输密钥派生(CTKD, Cross-Transport Key Derivation)。
双模设备(如 iPhone)一次 BR/EDR Secure Connections 配对:
- 44.855 先生成 BR/EDR P-256 鉴权 Link Key(
key_type=0x8AUTH_COMB_P_256)并落盘btif_storage_add_bredr_keys。 - 45.087 SMP-BR(SMP over BR/EDR)经 CTKD 从该 Link Key 计算出 LE LTK(
smp_calculate_long_term_key_from_link_key),并分发 IRK(btm_sec_save_le_key: BTM_LE_KEY_PID save peer IRK)。
即”一次配对、双传输密钥”:BR/EDR Link Key 是源,LE LTK/IRK 是派生。[BT_TRANSPORT_LE] 侧的 bond 状态变化(45.087 日志可见)是 CTKD 的副作用,不是独立的 LE 配对流程。
flowchart LR SSP([BR/EDR SSP<br/>Numeric Comparison]) --> LK[BR/EDR Link Key<br/>key_type=0x8<br/>44.855 落盘] LK -->|CTKD 派生<br/>smp_calculate_long_term_key_from_link_key| LTK[LE LTK] LK -->|分发| IRK[IRK<br/>BTM_LE_KEY_PID<br/>45.087] LTK --> Store[/hci1_bt_config.conf<br/>按MAC分段/] IRK --> Store
6. 取消语义专题(2956 核心)
⚠️ cancelBondProcess vs removeBond
cancelBondProcess():中止进行中的 SSP。仅在协议栈 pairing state ≠ IDLE(框架 bondState=BOND_BONDING)时有效。当 pairing state 已回 IDLE(已 BONDED 或未开始),栈只打印Unexpected pairing confirm(Res=11) 空转——2956 实证 46.339。
removeBond():撤销已完成的配对。是协议层唯一能把 BOND_BONDED 拉回 BOND_NONE 的动作;触发密钥从 bt_config.conf 删除并广播 BONDED→NONE。2956 根因:历史
onCancel只有cancelBondProcess一行。用户 46.336 点取消时,协议栈 44.855 早已落 Link Key、BONDED——cancelBondProcess落入 IDLE 态必然空转,配对必然落定。当前状态(对抗审查 + 主控二次核实,2026-07-23):⚠️ 修复 commit
7239803be在dev_a17_0610为悬空提交(git merge-base --is-ancestor 7239803be HEAD返回非祖先、git fsck显示 dangling)——已被 reset,当前分支未合入,bug 仍在。实测当前MiBluetoothPairingController.kt:283onCancel仍是旧版(仅cancelBondProcess一行),MiBluetoothPairingDialogActivity.kt也无mDismissed标志。修复方案(待合入)——加 bonded 降级:
fun onCancel() { if (mDevice.bondState == BluetoothDevice.BOND_BONDED) { mDevice.removeBond() // 等 BOND_NONE 广播后再视为取消完成 } else { mDevice.cancelBondProcess() } }46.336 时
getBondState()读协议栈服务侧状态(不依赖广播),已是 BONDED,故该分支可命中。配套(待合入):
MiBluetoothPairingDialogActivity.ktdismiss 引入mDismissed标志位 + 即时解注册,过滤迟到广播。
flowchart TD Cancel([用户点取消]) --> Check{getBondState?} Check -- BOND_BONDING --> CB[cancelBondProcess<br/>配对进行中 有效<br/>→BOND_NONE] Check -- BOND_BONDED --> RB[removeBond<br/>已完成 唯一可撤销<br/>→BOND_NONE] Check -- BOND_NONE --> NOP[无操作] CB --> Done([取消成功]) RB --> Done NOP --> Done Bad([历史bug路径 2956]) -.->|只调cancelBondProcess| IDLE{stack pairing state?} IDLE -- IDLE已BONDED --> Fail[Res:11 空转<br/>配对必然落定<br/>HFP/A2dp自动连接]
7. 对抗自查(三条证伪)
7.1 ❌「Numeric Comparison 必须用户点确认」
证伪:协议层 Numeric Comparison 确实需要用户确认(is_auto_accept_ssp=false @43.084 为证——栈不自动确认,把决定权上抛 App)。但本仓 MiBluetoothPairingDialogFragment.kt:179-182 在 isRequestPairAsClient=true(车机主动发起)时弹窗一创建就调 onPair()(43.184→43.186 仅 2ms),App 主动绕过了用户确认窗口,实际效果等同 Just Works。要正确表述为「App 绕过确认」,而非「协议不要求确认」——否则自相矛盾。
7.2 ❌「自动 accept 是协议要求」
证伪:is_auto_accept_ssp=false 明文证明协议栈不自动确认。本仓自动 accept 是 App 层决策(产品取舍:车机做车机侧确认省一次点击),不是协议强制。
7.3 ❌「cancelBondProcess 能撤销已完成配对」
证伪:46.339 btm_confirm_req_reply() State: IDLE Res: 11 / Unexpected pairing confirm。State: IDLE 说明栈已不在配对中(44.860 已 BONDED),取消请求被丢弃。同时段 removeBond 出现 0 次,密钥已落盘——配对必然落定。
8. 为什么 BONDED 广播会被「service discovery」延迟
ACTION_BOND_STATE_CHANGED 不是协议栈 BONDED 那一刻就发。com.android.bluetooth 的 BondStateMachine(AOSP packages/modules/Bluetooth/.../BondStateMachine.java)收到底层 BONDED 后,先发起 SDP/GATT service discovery 把对端 profile UUID 拉回,等 SDP 完成再发 BONDED 广播——这样应用层一收到广播就能直接连 profile。代价:广播被 SDP 时长推迟。2956 实测协议栈 44.860 BONDED、应用层广播 46.534 才发,Δ=1.674s(详见 20-Android框架全景)。
设计含义:「配对完成弹窗立刻 dismiss」在应用层无法可靠实现——应用层最早的 bonded 信号就是这个被延迟的广播。修复只能落在取消语义与 receiver 生命周期上。
9. 设计要点与坑
- variant 决定 UI、
isRequestPairAsClient决定是否自动确认——两变量正交。Numeric Comparison 被动配对仍走用户点击;主动配对一律自动onPair(Fragment:179-182)。 - 两条自动确认路径:主动配对走
onPair()(:265-269);DISPLAY_* 走notifyDialogDisplayed()(:209-220)。 - 取消必须区分状态(修复方案;⚠️ commit 7239803be 在 dev_a17_0610 为悬空提交、当前未合入):BONDING→
cancelBondProcess,BONDED→removeBond。getBondState()读栈服务态,不依赖被延迟的广播。 - BONDED 广播不可信赖为实时信号:被 SDP 延迟秒级;dismiss 后必须立即解注册 receiver +
mDismissed过滤迟到广播。 - 双模设备一次配对两把钥匙:BR/EDR Link Key(44.855) → CTKD 派生 LE LTK + IRK(45.087)。
is_auto_accept_ssp=false是判断「栈层是否替我做决定」的金标准日志关键字——出现即决定权在 App 层。- bt_config.conf 路径:小米双适配器
/data/misc/bluedroid/hci1_bt_config.conf,按 MAC 分段,无[LinkKeys]/[LeKeys]段。
结论:SSP 协议安全模型由「variant + 用户交互」决定,本仓通过 isRequestPairAsClient 把 Numeric Comparison 主动发起侧降级为 Just Works 语义(App 绕过确认);BondStateMachine 的「先 SDP 再广播」制造秒级竞态窗口。两者叠加使「取消已落定配对」必须靠 removeBond——这就是 2956 的全部理论根因;修复位点为 onCancel 加 bonded→removeBond 降级(commit 7239803be,⚠️ 当前在 dev_a17_0610 为悬空提交、未合入,bug 仍在)。