11 · 配对与 SSP 安全机制

核心文件:MiBluetoothPairingController.ktview/MiBluetoothPairingDialogFragment.ktview/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.confhci1_ 前缀 + 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] 聚合段。

配对完成在框架里叫 bondBluetoothDevice 三个常量刻画状态机:

常量数值含义
BOND_NONE10未配对;失败/超时/removeBond 都回此态
BOND_BONDING11配对进行中;createBond() 后立即进入
BOND_BONDED12已配对;密钥已落盘
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_VARIANTMiBluetoothPairingController.getDialogType()(:94-114) 归并成三种 UI:

PAIRING_VARIANT_*数值对应 SSP 模式Controller 归类实际 UI
PIN0老 PINUSER_ENTRY_DIALOG(0)输入框
PASSKEY1Passkey Entry(输入侧)USER_ENTRY_DIALOG(0)输入 6 位
PASSKEY_CONFIRMATION2Numeric ComparisonCONFIRMATION_DIALOG(1)显 6 位配对码,仅确认钮(2956 即此
CONSENT3Just WorksCONFIRMATION_DIALOG(1)不显配对码,仅确认
OOB_CONSENT6OOBCONFIRMATION_DIALOG(1)仅确认
DISPLAY_PASSKEY4Passkey Entry(显示侧)DISPLAY_PASSKEY_DIALOG(2)只读,自动确认
DISPLAY_PIN5Numeric Comparison(显示侧)DISPLAY_PASSKEY_DIALOG(2)只读,自动确认
PIN_16_DIGITS7老 PIN(16位)USER_ENTRY_DIALOG(0)输入 16 位

⚠️ variant 命名鸿沟(排障必读):bugreport 同一回调有两个 variant——native sspRequestCallback pairingVariant 0(btif 枚举,0=PASSKEY_CONFIRMATION,行205446)与 App sendPairingRequestIntent 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-114formatKey():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 配对:

  1. 44.855 先生成 BR/EDR P-256 鉴权 Link Key(key_type=0x8 AUTH_COMB_P_256)并落盘 btif_storage_add_bredr_keys
  2. 45.087 SMP-BR(SMP over BR/EDR)经 CTKD 从该 Link Key 计算出 LE LTKsmp_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 7239803bedev_a17_0610悬空提交git merge-base --is-ancestor 7239803be HEAD 返回非祖先、git fsck 显示 dangling)——已被 reset,当前分支未合入,bug 仍在。实测当前 MiBluetoothPairingController.kt:283 onCancel 仍是旧版(仅 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.kt dismiss 引入 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-182isRequestPairAsClient=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 confirmState: IDLE 说明栈已不在配对中(44.860 已 BONDED),取消请求被丢弃。同时段 removeBond 出现 0 次,密钥已落盘——配对必然落定。

8. 为什么 BONDED 广播会被「service discovery」延迟

ACTION_BOND_STATE_CHANGED 不是协议栈 BONDED 那一刻就发。com.android.bluetoothBondStateMachine(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. 设计要点与坑

  1. variant 决定 UI、isRequestPairAsClient 决定是否自动确认——两变量正交。Numeric Comparison 被动配对仍走用户点击;主动配对一律自动 onPair(Fragment:179-182)。
  2. 两条自动确认路径:主动配对走 onPair()(:265-269);DISPLAY_* 走 notifyDialogDisplayed()(:209-220)。
  3. 取消必须区分状态(修复方案;⚠️ commit 7239803be 在 dev_a17_0610 为悬空提交、当前未合入):BONDING→cancelBondProcess,BONDED→removeBondgetBondState() 读栈服务态,不依赖被延迟的广播。
  4. BONDED 广播不可信赖为实时信号:被 SDP 延迟秒级;dismiss 后必须立即解注册 receiver + mDismissed 过滤迟到广播。
  5. 双模设备一次配对两把钥匙:BR/EDR Link Key(44.855) → CTKD 派生 LE LTK + IRK(45.087)。
  6. is_auto_accept_ssp=false 是判断「栈层是否替我做决定」的金标准日志关键字——出现即决定权在 App 层。
  7. 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 仍在)。