双模蓝牙解绑、CTKD 与 iOS 配对机制整理

3 阅读14分钟

1. 问题背景

讨论的是 双模蓝牙设备,也就是同时支持:

BLE / LE / Bluetooth Low Energy
BR/EDR / Bluetooth Classic / 经典蓝牙

的设备。

典型场景:

  • 设备通过 BLE 提供 GATT 控制、配置、状态同步;
  • 设备通过 BR/EDR 提供 SPP、A2DP、HFP、HID 等经典蓝牙能力;
  • 用户已经在 iPhone 系统蓝牙设置中完成配对;
  • App 里希望实现“解除绑定 / 解绑 / 取消配对”;
  • 希望解绑后不影响下一次连接,包括本机重新绑定、其他手机绑定。

一、iOS App 能否在 App 内解除系统蓝牙配对?

1.1 结论

不能。

普通 iOS App 不能静默删除系统蓝牙设置中的已配对记录,也不能代替用户执行:

设置 > 蓝牙 > 某设备 > 忽略此设备

这个动作。

也就是说,App 内的“解绑”不能等价于系统层面的“Forget This Device”。

1.2 App 能做什么

App 可以做的是:

断开当前 BLE 连接
停止扫描
停止自动重连
清理 App 本地绑定关系
清理 App 本地缓存
通知外设解除业务绑定
让外设清理 owner / bond / session / token
让外设主动断开连接并进入可重新绑定状态

1.3 App 做不到什么

App 做不到:

删除 iOS 系统蓝牙设置中的设备项
删除 iOS 系统保存的 BR/EDR Link Key
删除 iOS 系统保存的 BLE LTK / IRK 等 bond 信息
代替用户点击“忽略此设备”
直接操作系统蓝牙配对数据库

1.4 工程理解

所以在 iOS 上,“解绑”应该被设计成:

App 业务解绑 + 外设状态清理 + 云端绑定解除

而不是:

删除 iOS 系统蓝牙配对记录

系统蓝牙设置里残留配对项,是 iOS 权限边界导致的正常现象。


二、iOS App 内解绑应该怎么做才算干净?

2.1 核心原则

不要把 iOS 系统配对状态当作业务绑定状态。

系统配对只代表链路层或安全层可能已经建立过信任关系,不代表这个设备仍属于当前 App 用户。

真正的绑定状态应该由以下几方共同决定:

App 本地绑定状态
云端账号-设备绑定状态
外设端 owner / binding 状态
应用层 token / session 是否有效

2.2 App 侧解绑流程

推荐流程:

用户点击解绑
  ↓
App 停止自动重连
  ↓
App 停止扫描 / 停止业务命令队列
  ↓
App 取消 notify 订阅
  ↓
App 向外设发送 UNBIND_REQUEST
  ↓
外设返回 UNBIND_ACK
  ↓
App 清理本地数据库 / Keychain / 缓存
  ↓
App 清理云端绑定关系
  ↓
App 主动断开 BLE 连接
  ↓
App 进入未绑定状态

示意代码:

func unbind(peripheral: CBPeripheral) {
    // 1. 停止业务层自动重连
    reconnectPolicy.disable(for: peripheral.identifier)

    // 2. 取消 notify
    for service in peripheral.services ?? [] {
        for characteristic in service.characteristics ?? [] {
            if characteristic.isNotifying {
                peripheral.setNotifyValue(false, for: characteristic)
            }
        }
    }

    // 3. 向外设写入解绑命令
    // write: UNBIND_REQUEST(accountId, deviceId, sessionToken, signature)

    // 4. 等待外设 UNBIND_ACK

    // 5. 清理本地数据库、Keychain、缓存、重连队列

    // 6. 断开连接
    centralManager.cancelPeripheralConnection(peripheral)
}

2.3 App 本地应该清理哪些内容

建议至少清理:

设备业务 ID
设备 SN
账号-设备绑定关系
CBPeripheral.identifier
BLE service / characteristic 缓存
notify 订阅状态
自动重连队列
后台恢复状态中的 peripheral 引用
Keychain 中的 token / session key / refresh key
本地数据库中的设备能力信息
固件版本缓存
配置项缓存
OTA 状态
同步任务
命令队列
云端设备映射

尤其要注意:
不要只调用 cancelPeripheralConnection 就认为解绑完成。

cancelPeripheralConnection 只表示 App 请求断开当前 BLE 连接,不代表:

系统忘记了设备
外设清除了绑定
云端解绑成功
经典蓝牙断开
业务 token 失效

三、外设侧如何保证解绑干净?

3.1 外设才是干净解绑的关键

iOS App 不能删除系统 bond,所以必须让外设端成为“解绑状态”的权威来源。

外设端应该实现明确的解绑命令,例如:

UNBIND_REQUEST {
  app_user_id
  phone_binding_id
  device_sn
  nonce
  timestamp
  signature/session_token
}

3.2 外设收到解绑命令后应做什么

外设应执行:

校验当前连接是否有解绑权限
删除当前 owner / account / phone binding
删除应用层 session key
删除 token / token hash
清理业务配置
清理 CCCD / notify 状态
清理 BLE bond / LTK / IRK 信息,如果协议栈支持
清理 BR/EDR Link Key,如果协议栈支持
清理经典蓝牙 profile 授权状态
递增 pairingEpoch
返回 UNBIND_ACK
主动断开 BLE 连接
主动断开 BR/EDR 连接
进入 unbound / pairable / advertising 状态

3.3 外设应支持两种解绑方式

解绑类型适用场景行为
当前用户解绑App 内正常解绑只清理当前账号、当前手机、当前绑定关系
强制解绑 / 恢复出厂换手机、旧手机丢失、系统 bond 混乱清理所有 owner、bond、session、token、配置和历史状态

3.4 双模设备的特殊注意点

双模设备不能只清 BLE 或只清经典蓝牙。

需要分别考虑:

BLE bond / LTK / IRK
BR/EDR Link Key
GATT 应用层绑定
SPP / A2DP / HFP / HID 等 profile 状态
云端账号绑定
外设 owner 状态

否则会出现:

App 显示解绑了,但系统蓝牙还连着
BLE 解绑了,但经典蓝牙音频还自动连接
经典蓝牙断开了,但 GATT 仍然认为已绑定
外设对旧手机仍然保留 owner

四、系统设置里仍有配对记录时,如何不影响下一次连接?

4.1 核心原则

系统配对状态可以残留,但业务绑定必须重新确认。

每次连接后,App 都应该先查询外设状态:

GET_BIND_STATE

外设返回:

unbound
bound_to_me
bound_to_other
pending_unbind

4.2 同一台 iPhone 重新绑定

由于系统蓝牙设置里可能还保存旧 bond,同一台 iPhone 可能无需再次弹系统配对框就能建立加密链路。

这时 App 不应该直接认为设备已绑定。

正确流程:

连接外设
  ↓
读取 bind_state
  ↓
如果是 unbound,则重新走业务绑定流程
  ↓
生成新的 bindingId / sessionToken
  ↓
外设保存新的 owner / token

4.3 另一台手机绑定

外设在解绑后应进入可绑定状态:

unbound
pairable
advertising

广播中可以携带:

device_sn_hash
bind_state
pairing_epoch
protocol_version

例如:

manufacturerData:
  device_sn_hash
  bind_state = 0x00
  pairing_epoch = 12
  protocol_version = 3

其中 pairing_epoch 很有用。

每次解绑后递增:

pairing_epoch = pairing_epoch + 1

App 发现 epoch 变化,就应该认为旧缓存全部失效。

4.4 旧手机仍自动连接怎么办

这在双模设备中很常见,尤其是:

A2DP
HFP
SPP
HID

这类经典蓝牙 profile。

处理方式:

外设允许链路连接,但拒绝旧业务 token
外设返回 AUTH_REQUIRED / UNBOUND
外设主动断开旧连接
外设在新绑定窗口内优先允许新手机绑定
外设必要时清理旧 BR/EDR Link Key
App 提示用户如系统仍自动连接,可手动忽略设备

产品上需要接受一个事实:

App 无法保证 iOS 系统设置里的配对项消失。

但可以保证:

旧系统配对记录不再等于业务绑定
旧手机无法继续控制设备
新手机可以重新绑定
外设状态不会被旧 token 污染

五、推荐的解绑状态机

5.1 外设侧状态机

Bound
  |
  | receive UNBIND_REQUEST
  v
Unbinding
  |
  | clear owner/session/token/bond/link key
  v
Unbound
  |
  | disconnect BLE + BR/EDR
  | start pairable advertising
  v
Pairable

5.2 App 侧状态机

Bound
  |
  | user taps unbind
  v
UnbindRequested
  |
  | receive UNBIND_ACK
  v
LocalCleaned
  |
  | cancelPeripheralConnection
  v
UnboundInApp

5.3 解绑失败兜底状态

如果解绑命令未完成,例如:

外设未返回 ACK
连接中途断开
App 被杀
系统蓝牙异常
经典蓝牙仍连接

App 应进入:

PendingUnbind

下次连接后继续:

读取 bind_state
  ↓
如果外设仍 pending_unbind
  ↓
重新发送 UNBIND_REQUEST 或 UNBIND_COMMIT
  ↓
完成清理

六、业务层绑定设计建议

6.1 不要只依赖蓝牙配对

蓝牙配对解决的是链路安全问题,不解决业务归属问题。

业务绑定建议使用:

deviceId / serialNumber
accountId
bindingId
pairingEpoch
sessionToken
devicePublicKey
phonePublicKey
nonce
signature

6.2 推荐绑定流程

App 连接外设
  ↓
App 读取设备 SN / deviceId / bind_state
  ↓
外设返回 challenge nonce
  ↓
App 使用登录态请求云端签发 binding token
  ↓
App 将 token / signature 写入外设
  ↓
外设验证 token
  ↓
外设保存 owner / bindingId / token hash
  ↓
绑定完成

6.3 推荐连接认证流程

每次连接后:

App 读取 bind_state
  ↓
外设返回 challenge
  ↓
App 使用 sessionToken 计算 response
  ↓
外设验证
  ↓
验证通过才开放业务控制能力

这样即使 iOS 系统中还有配对记录,旧手机也只能建立链路,不能继续控制设备。


七、CTKD 是什么?

7.1 名称

你说的:

双模蓝牙通过一次配对完成 BLE 和 BR/EDR 绑定

标准术语叫:

Cross-Transport Key Derivation

缩写:

CTKD

7.2 CTKD 的作用

CTKD 用于双模蓝牙设备中,让设备在一个 transport 上完成配对后,为另一个 transport 派生长期密钥。

也就是:

一次配对
  ↓
同时完成 BLE 和 BR/EDR 的绑定

7.3 两个方向

CTKD 可以有两个方向。

方向一:LE 推导 BR/EDR

LE pairing 完成
  ↓
生成 LE LTK
  ↓
通过 CTKD 派生 BR/EDR Link Key
  ↓
BR/EDR 也视为已绑定

方向二:BR/EDR 推导 LE

BR/EDR pairing 完成
  ↓
生成 BR/EDR Link Key
  ↓
通过 CTKD 派生 LE LTK
  ↓
LE 也视为已绑定

7.4 CTKD 不是简单的“同时配对”

CTKD 的本质不是两个通道同时执行配对。

它是:

一个 transport 完成 pairing / bonding
  ↓
通过标准密钥派生算法
  ↓
生成另一个 transport 所需的长期密钥

八、CTKD 来自 Apple 还是蓝牙标准?

8.1 结论

CTKD 来自蓝牙协议自身,不是 Apple 定义的。

它属于:

Bluetooth Core Specification

也就是 Bluetooth SIG 定义的蓝牙核心规范。

Apple 只是作为 iOS 蓝牙 Host / 平台实现方支持这个机制。

8.2 Apple 在其中的角色

Apple 做的是:

实现蓝牙 Host 协议栈
遵守 Bluetooth Core Specification
根据系统策略决定配对体验
管理系统蓝牙配对数据库
控制 App 可以访问哪些蓝牙能力

但 Apple 不是 CTKD 的定义者。

所以可以这样区分:

CTKD 机制本身:Bluetooth SIG / Bluetooth Core Spec 定义
iOS 配对 UI 和权限边界:Apple 平台策略
App 解绑能力受限:Apple iOS API 权限限制

8.3 iOS App 是否能感知 CTKD

普通 iOS App 通常不能直接感知:

当前是否正在执行 CTKD
是否由 LE LTK 派生了 BR/EDR Link Key
是否由 BR/EDR Link Key 派生了 LE LTK
密钥具体是什么
密钥是否被覆盖

App 通常只能看到结果:

BLE 特征访问时不再弹二次配对
经典蓝牙配对后 BLE 也能直接加密连接
BLE 配对后经典蓝牙 profile 也可用
系统设置中显示设备已配对

要确认 CTKD 是否发生,通常需要:

外设端协议栈日志
HCI log
btsnoop
芯片 SDK trace
配对阶段 key distribution 日志
Link Key / LTK 生成日志

九、transport 在蓝牙通信里是什么级别?

9.1 transport 的含义

在 CTKD 语境中,transport 指的是蓝牙底层传输体系或承载方式。

主要就是:

LE transport
BR/EDR transport

对应日常说法:

BLE / Low Energy
经典蓝牙 / Bluetooth Classic

9.2 transport 不是 App 层概念

这里的 transport 不是:

TCP / UDP 那种网络传输层
App 自定义协议
GATT Characteristic
SPP 通道
A2DP 音频流
HFP 通话通道

它比这些更底层。

9.3 蓝牙大致分层

可以粗略理解为:

应用业务层
  App 业务协议
  账号绑定
  设备控制命令

Profile / Service 
  BLE: GATT Service / Characteristic
  BR/EDR: SPP / A2DP / HFP / HID 

安全与配对层
  BLE: SMP / LTK / IRK / CSRK
  BR/EDR: Link Key / SSP / Secure Connections

Transport 
  LE transport
  BR/EDR transport

Controller / Radio 
  Baseband
  Link Layer
  PHY
  RF

9.4 CTKD 中 cross-transport 的意思

cross-transport 的意思是跨:

LE transport
和
BR/EDR transport

也就是:

跨 BLE 和经典蓝牙两套传输体系派生密钥

不是跨:

GATT 和 SPP
App 和外设
iOS 设置和 App
云端和本地

十、BLE 与 BR/EDR 的密钥差异

10.1 BLE 侧常见密钥

BLE 绑定后常见密钥包括:

LTK  - Long Term Key,用于后续加密连接
IRK  - Identity Resolving Key,用于解析随机地址
CSRK - Connection Signature Resolving Key,用于数据签名

其中最核心的是:

LTK

10.2 BR/EDR 侧常见密钥

BR/EDR 绑定后的核心密钥是:

Link Key

用于经典蓝牙后续链路认证和加密。

10.3 CTKD 跨的是哪两个密钥体系

CTKD 主要跨的是:

BLE LTK
和
BR/EDR Link Key

对应关系:

Transport常见叫法绑定后的核心密钥
LEBLE / Low EnergyLTK
BR/EDR经典蓝牙 / Bluetooth ClassicLink Key

十一、CTKD 与解绑的关系

11.1 为什么 CTKD 会影响解绑

因为双模设备可能通过一次配对,同时建立:

BLE bond
BR/EDR bond

如果解绑时只清 BLE,不清 BR/EDR,就可能出现:

BLE 显示解绑,但经典蓝牙仍自动连接

如果只清 BR/EDR,不清 BLE,就可能出现:

经典蓝牙断了,但 BLE GATT 仍然能加密连接

所以双模设备解绑时要考虑 CTKD 带来的“双 transport 绑定”。

11.2 App 侧无法分别清 iOS 的 BLE / BR/EDR bond

在 iOS 上,App 不能直接删除:

BLE LTK
BR/EDR Link Key
CTKD 派生出来的跨 transport 密钥

这些由系统蓝牙栈管理。

11.3 外设侧应主动清理双 transport 状态

外设解绑时应尽量清理:

LE bond
BR/EDR Link Key
CTKD 派生关系
应用层 owner
业务 token
pairingEpoch
profile 授权

如果协议栈支持按 peer 删除,应按当前 peer 删除。
如果不支持,至少要通过应用层认证阻断旧连接继续控制设备。


十二、推荐 App / 外设交互协议

12.1 查询绑定状态

GET_BIND_STATE

返回:

{
  state: unbound | bound_to_me | bound_to_other | pending_unbind,
  device_id: "...",
  binding_id: "...",
  owner_hash: "...",
  pairing_epoch: 12,
  nonce: "..."
}

12.2 发起绑定

BIND_REQUEST {
  account_id,
  phone_id,
  binding_id,
  pairing_epoch,
  nonce,
  token,
  signature
}

返回:

BIND_ACK {
  result,
  binding_id,
  session_token,
  pairing_epoch
}

12.3 发起解绑

UNBIND_REQUEST {
  account_id,
  phone_id,
  binding_id,
  device_id,
  nonce,
  timestamp,
  signature
}

返回:

UNBIND_ACK {
  result,
  new_state: unbound,
  pairing_epoch
}

12.4 连接认证

AUTH_CHALLENGE {
  nonce,
  pairing_epoch
}
AUTH_RESPONSE {
  binding_id,
  response,
  token_signature
}

十三、产品文案建议

13.1 App 内按钮文案

建议使用:

解除设备绑定

不建议使用:

取消系统配对
删除蓝牙配对
忽略此设备

因为这些动作 App 实际做不到。

13.2 解绑成功提示

可以写:

已解除 App 内绑定。若此设备仍出现在 iPhone 蓝牙设置中,这是 iOS 系统保留的配对记录,通常不影响重新绑定。若设备仍自动连接或无法被其他手机发现,请在“设置 > 蓝牙”中手动忽略该设备,或长按设备按键恢复出厂设置。

13.3 异常提示

如果外设仍被旧手机系统自动连接:

设备可能仍被旧手机的系统蓝牙连接。请关闭旧手机蓝牙,或在旧手机“设置 > 蓝牙”中忽略该设备,然后重试绑定。

如果需要强制解绑:

请长按设备按键 10 秒恢复出厂设置,设备将清除所有已绑定手机并重新进入配对状态。

十四、最终工程结论

14.1 关于 iOS App 解绑

iOS App 不能删除系统蓝牙配对记录。
App 内解绑必须做成业务解绑,而不是系统 Forget Device。

14.2 关于干净解绑

干净解绑的关键在外设端和业务层:
清 owner
清 token
清 session
清 bond/link key
清 profile 状态
递增 pairingEpoch
主动断开
进入可绑定状态

14.3 关于下一次连接

下一次连接不要信任系统配对状态。
必须读取外设 bind_state,并进行应用层认证。

14.4 关于 CTKD

一次配对完成 BLE 和 BR/EDR 绑定的机制叫 CTKD。
全称 Cross-Transport Key Derivation。
它来自 Bluetooth Core Specification,不是 Apple 私有机制。

14.5 关于 transport

CTKD 里的 transport 指蓝牙底层传输体系:
LE transport
BR/EDR transport

不是 GATT、SPP、A2DP、App 协议或 TCP/UDP 那种概念。

14.6 一句话总括

iOS 上不能由 App 静默删除系统配对记录;双模蓝牙的一次配对双绑定机制叫 CTKD,属于蓝牙标准;真正可靠的解绑方案,是把绑定状态放在外设和业务层管理,让系统配对残留不再影响重新绑定和设备归属判断。


文章来自于AI对话生成