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 | 常见叫法 | 绑定后的核心密钥 |
|---|---|---|
| LE | BLE / Low Energy | LTK |
| BR/EDR | 经典蓝牙 / Bluetooth Classic | Link 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对话生成