安全机制
经典蓝牙与 BLE 如何保证数据传输安全,包括配对、加密、认证机制详解。
🔐 经典蓝牙 vs BLE 安全对比
flowchart TB
subgraph Classic["经典蓝牙安全"]
C1["PIN Code 配对"]
C2["SAFER+ 加密"]
C3["E0 流密码"]
C4["8-16 位 PIN"]
end
subgraph BLE["BLE 安全"]
B1["LE Legacy AES-CCM"]
B2["LE SC P-256 ECDH"]
B3["6 位 OOB Numeric"]
B4["AES-128 加密"]
end
style Classic fill:#fff3cd,stroke:#ffc107
style BLE fill:#e3f2fd,stroke:#007bff
| 特性 | 经典蓝牙 (BR/EDR) | BLE (低功耗) |
|---|---|---|
| 配对方式 | PIN Code (8-16 位) | Passkey (6 位)/OOB/Numeric |
| 加密算法 | E0 流密码 / SAFER+ | AES-128 CCM |
| 密钥长度 | 8-16 bytes | 7-16 bytes |
| 密钥生成 | 基于 PIN + BD_ADDR | ECDH 密钥交换 |
| MITM 保护 | 可选 | LE SC 强制 |
| 安全版本 | SSP (2.1+) | LE SC (4.2+) |
📡 经典蓝牙安全机制
1. 配对流程 (Pairing)
sequenceDiagram
participant A as 设备 A 手机主机
participant B as 设备 B 耳机从机
Note over A,B: 场景 1 固定 PIN 码 最常见
Note over B: 设备 B 有固定 PIN 如 0000
A->>A: 用户输入 PIN 0000
A->>A: 生成 Kinit = E PIN BD_ADDR_B
B->>B: 使用固定 PIN 生成 Kinit
Note over A,B: 场景 2 设备 B 显示 PIN
Note over B: 设备 B 显示 PIN 如 123456
A->>A: 用户输入显示的 PIN
A->>A: 生成 Kinit = E PIN BD_ADDR_B
B->>B: 使用已知 PIN 生成 Kinit
Note over A,B: 阶段 2 链路密钥生成
A->>B: 随机数 AU_RAND_A
B->>A: 随机数 AU_RAND_B
A->>A: 生成 Klink = E2 Kinit AU_RAND_A BD_ADDR_A
B->>B: 生成 Klink = E2 Kinit AU_RAND_B BD_ADDR_B
Note over A,B: 阶段 3 认证
A->>B: 认证请求 AU_RAND_A
B->>A: 认证响应 SRES = E1 Klink AU_RAND_A BD_ADDR_B
A->>A: 验证 SRES
Note over A,B: 阶段 4 加密启用
A->>B: 加密请求
A->>A: 生成加密密钥 Kc = E3 Klink COF_RAND BD_ADDR
B->>B: 生成加密密钥 Kc
A->>B: 加密数据 E0 流密码
常见 PIN 码场景:
- 固定 PIN: 耳机/车载通常为
0000或1234 - 设备显示 PIN: 智能电视/游戏机显示 PIN,手机输入
- Just Works: 无输入 (SSP 2.1+)
2. 加密流程
// 经典蓝牙加密密钥生成
// 步骤 1: 生成初始化密钥 Kinit
// 使用 SAFER+ 算法,基于 PIN 码和设备地址
Kinit = SAFER+(PIN, BD_ADDR, 128)
// 步骤 2: 生成链路密钥 Klink
// 双方交换随机数,生成共享密钥
Klink = E(Kinit, AU_RAND_A XOR AU_RAND_B)
// 步骤 3: 生成加密密钥 Kc (8-16 bytes)
Kc = E(Klink, COF_RAND, BD_ADDR)
// 步骤 4: E0 流密码加密
// 使用 Kc 初始化 E0 流密码生成器
// 每时隙生成 1 bit 密钥流
ciphertext = plaintext XOR keystream
// E0 流密码特点:
// - 基于 4 个 LFSR (线性反馈移位寄存器)
// - 密钥长度可变 (8-16 bytes)
// - 每时隙更新密钥流
3. 安全模式
| 模式 | 说明 | 安全级别 |
|---|---|---|
| Mode 1 | 非安全模式 | 无安全 |
| Mode 2 | 服务级安全 | 按需加密 |
| Mode 3 | 链路级安全 | 强制加密 |
| SSP (2.1+) | 安全简单配对 | Numeric/Passkey/OOB |
📶 BLE 安全机制
1. LE Legacy (BLE 4.0-4.1)
sequenceDiagram
participant P as 外设 Peripheral
participant C as 中心 Central
Note over P,C: Phase 1 配对特性交换
P->>C: 配对请求 IO 能力、认证要求
C->>P: 配对响应
Note over P,C: Phase 2 临时密钥生成
P->>P: 生成 TK Temporary Key
C->>C: 生成 TK Temporary Key
Note over P,C: 配对方式
Note over P,C: Just Works TK = 0
Note over P,C: Passkey TK = 6 位数字
Note over P,C: OOB TK = 带外数据
Note over P,C: Phase 3 短期密钥 STK 生成
P->>C: 随机数 Ra
C->>P: 随机数 Rb
P->>P: STK = AES TK Ra XOR Rb
C->>C: STK = AES TK Ra XOR Rb
Note over P,C: Phase 4 长期密钥 LTK 分发
P->>C: 加密传输 LTK
C->>C: 存储 LTK 用于重连
2. LE Secure Connections (BLE 4.2+)
sequenceDiagram
participant P as 外设 Peripheral
participant C as 中心 Central
Note over P,C: Phase 1 配对特性交换
P->>C: 配对请求 LE SC 支持
C->>P: 配对响应
Note over P,C: Phase 2 公钥交换 ECDH P-256
P->>P: 生成私钥 SKa 公钥 PKa
C->>C: 生成私钥 SKb 公钥 PKb
P->>C: 发送公钥 PKa
C->>P: 发送公钥 PKb
Note over P,C: Phase 3 DHKey 计算
P->>P: DHKey = ECDH SKa PKb
C->>C: DHKey = ECDH SKb PKa
Note over P,C: Phase 4 认证 Numeric Comparison
P->>P: 计算确认值 Va
C->>C: 计算确认值 Vb
P->>P: 显示 6 位数字 Va mod 1000000
C->>C: 显示 6 位数字 Vb mod 1000000
Note over P,C: 用户确认数字相同
Note over P,C: Phase 5 密钥生成
P->>P: LTK = f DHKey 随机数
C->>C: LTK = f DHKey 随机数
Note over P,C: 加密连接建立
3. BLE 加密密钥层次
// BLE 密钥层次结构
// ========== LE Legacy (BLE 4.0-4.1) ==========
// 1. TK (Temporary Key) - 临时密钥
// - Just Works: TK = 0
// - Passkey: TK = 6 位数字 (20 bits)
// - OOB: TK = 128 位带外数据
// 2. STK (Short Term Key) - 短期密钥
// STK = AES128(TK, S1(Ra || Rb))
// - Ra, Rb: 双方生成的 128 位随机数
// - S1: 取低 64 位
// - 用于初始加密连接
// 3. LTK (Long Term Key) - 长期密钥
// - 通过加密连接分发
// - 存储用于后续重连
// - 8-16 bytes
// 4. IRK (Identity Resolving Key) - 身份解析密钥
// - 用于生成私有地址
// - 保护设备隐私
// 5. CSRK (Connection Signature Resolving Key)
// - 用于数据签名
// ========== LE Secure Connections (BLE 4.2+) ==========
// 1. DHKey (Diffie-Hellman Key) - 256 位
// DHKey = ECDH_P256(SKa, PKb) = ECDH_P256(SKb, PKa)
// - SKa/SKb: 私钥
// - PKa/PKb: 公钥
// 2. MacKey (128 位) - 用于认证
// MacKey = f(DHKey, N1 || N2, "btle")
// 3. LTK (Long Term Key) - 128 位
// LTK = f(DHKey, N1 || N2, "btlk")
// - N1, N2: 双方随机数
// - f: AES-CMAC 函数
// 4. IRK, CSRK (同上)
🔑 配对流程详解
BLE 配对方式对比
| 配对方式 | IO 能力要求 | MITM 保护 | 安全性 | 典型场景 |
|---|---|---|---|---|
| Just Works | 无显示/无输入 | ❌ 无 | 低 | 手环、传感器 |
| Passkey Entry | 键盘或显示 | ✅ 有 | 中 | 键盘、电视 |
| Numeric Comparison | 双方都有显示 | ✅ 有 | 高 | 手机对手机 |
| OOB (Out of Band) | NFC/其他通道 | ✅ 有 | 最高 | NFC 配对 |
配对代码示例
// Nordic SDK - BLE 配对配置
void ble_gap_sec_params_init(void) {
ble_gap_sec_params_t sec_params = {0};
// 配对配置
sec_params.bond = 1; // 允许绑定 (存储 LTK)
sec_params.mitm = 1; // 需要 MITM 保护
sec_params.lesc = 1; // 使用 LE Secure Connections
sec_params.keypress = 0; // 不支持按键确认
sec_params.io_caps = BLE_GAP_IO_CAPS_DISPLAY_YESNO; // 有显示,可确认
sec_params.oob = 0; // 无 OOB 数据
sec_params.min_key_size = 16; // 最小密钥长度 16 bytes
sec_params.max_key_size = 16; // 最大密钥长度 16 bytes
// 密钥分发配置
sec_params.kdist_own.enc = 1; // 分发加密密钥 (LTK)
sec_params.kdist_own.id = 1; // 分发身份密钥 (IRK)
sec_params.kdist_peer.enc = 1; // 接收对端加密密钥
sec_params.kdist_peer.id = 1; // 接收对端身份密钥
// 回复配对请求
sd_ble_gap_sec_params_reply(m_conn_handle,
BLE_GAP_SEC_STATUS_SUCCESS,
&sec_params,
NULL);
}
// 配对事件处理
void on_ble_gap_evt_auth_status(ble_gap_evt_auth_status_t *p_auth_status) {
if (p_auth_status->auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
// 配对成功
// p_auth_status->bonded: 是否已绑定
// p_auth_status->enc_mode: 加密模式
// p_auth_status->key_size: 密钥长度 (7-16 bytes)
// 存储 LTK 到 Flash
if (p_auth_status->bonded) {
store_ltk_to_flash(&p_auth_status->keys);
}
} else {
// 配对失败
// BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP: 不支持配对
// BLE_GAP_SEC_STATUS_PASSKEY_ENTRY_FAILED: 密码错误
// BLE_GAP_SEC_STATUS_OOB_NOT_AVAILABLE: OOB 数据不可用
}
}
🛡️ 防止其他手机应用读取数据
BLE 设备如何防止同一手机上的其他未授权应用读取数据。
1. 连接级别保护 (加密绑定)
sequenceDiagram
participant Device as BLE 设备
participant App1 as 授权 App
participant App2 as 其他 App
participant Phone as 手机系统
Note over Device,App1: 阶段 1 首次配对
App1->>Device: 配对请求
Device->>App1: 显示 6 位数字
App1->>App1: 用户确认数字
App1->>Device: 确认完成
Device->>App1: 加密连接建立
Device->>Device: 存储 App1 的 IRK LTK
Note over Device,App2: 阶段 2 其他 App 尝试连接
App2->>Device: 扫描发现设备
App2->>Device: 连接请求
Device->>Device: 检查绑定列表
Device->>App2: 拒绝 未绑定
Note over Device,Phone: 阶段 3 系统级保护
Phone->>Phone: 每个 App 独立的蓝牙权限
Phone->>App2: 无权限访问已配对设备
2. GATT 权限控制
| 保护级别 | 实现方式 | 效果 |
|---|---|---|
| 加密访问 | 要求加密连接才能读取特征值 | 未配对 App 无法读取 |
| 绑定检查 | 检查设备是否在绑定列表 | 只有配对的手机可访问 |
| 权限分级 | 不同特征值不同安全级别 | 敏感数据需要更高权限 |
| 应用认证 | 自定义应用层认证 | 二次验证确保授权 |
3. 固件实现示例
// Nordic SDK - GATT 权限控制
// 配置特征值安全级别
ble_gatts_char_md_t char_md = {0};
ble_gatts_attr_md_t attr_md = {0};
// 安全级别配置
attr_md.vloc = BLE_GATTS_VLOC_STACK; // 存储在协议栈
attr_md.vlen = 1; // 可变长度
// 读取权限:需要加密连接
attr_md.read_perm.sm = 1; // 安全模式 1
attr_md.read_perm.lv = 4; // 安全级别 4 (需要 MITM + 加密)
// 写入权限:需要绑定
attr_md.write_perm.sm = 1;
attr_md.write_perm.lv = 4;
// 应用特征值
char_md.char_props.read = 1;
char_md.char_props.write = 1;
char_md.char_props.notify = 1;
// 添加特征值
sd_ble_gatts_characteristic_add(service_handle,
&char_md,
&attr,
&char_handle);
// 检查连接是否加密
bool is_connection_encrypted(uint16_t conn_handle) {
ble_gap_conn_sec_t conn_sec;
sd_ble_gap_conn_sec_get(conn_handle, &conn_sec);
// 检查加密模式
return (conn_sec.enc_mode.encryption > 0);
}
// 检查是否已绑定
bool is_bonded(ble_gap_addr_t *p_addr) {
ble_gap_sec_params_t sec_params;
// 检查地址是否在绑定列表中
return check_bonding_list(p_addr);
}
// 拒绝未授权访问
void on_gatt_read_request(ble_gatts_evt_read_t *p_evt) {
if (!is_connection_encrypted(p_evt->conn_handle)) {
// 连接未加密,拒绝访问
sd_ble_gatts_rw_authority_reply(p_evt->conn_handle,
BLE_GATTS_AUTHORIZE_TYPE_READ,
BLE_GAP_AUTHORIZATION_STATUS_REJECT);
return;
}
if (!is_bonded(&p_evt->peer_addr)) {
// 设备未绑定,拒绝访问
sd_ble_gatts_rw_authority_reply(p_evt->conn_handle,
BLE_GATTS_AUTHORIZE_TYPE_READ,
BLE_GAP_AUTHORIZATION_STATUS_REJECT);
return;
}
// 授权访问
sd_ble_gatts_rw_authority_reply(p_evt->conn_handle,
BLE_GATTS_AUTHORIZE_TYPE_READ,
BLE_GAP_AUTHORIZATION_STATUS_ACCEPT);
}
4. 手机系统级保护
Android
- 蓝牙权限: BLUETOOTH_CONNECT (Android 12+)
- 配对设备: 只有配对的 App 可访问
- 系统隔离: 每个 App 独立的连接句柄
- 位置权限: 扫描需要位置权限
iOS
- 权限控制: NSBluetoothAlwaysUsageDescription
- 配对绑定: 系统管理配对信息
- App 隔离: 每个 App 独立访问
- 后台限制: 后台蓝牙访问受限
最佳实践
- 启用 LE Secure Connections (MITM 保护)
- 要求加密连接 (Security Level 4)
- 启用绑定 (Bonding) 存储 LTK
- 敏感特征值设置更高权限
- 实现应用层认证 (可选)
5. 安全级别配置建议
| 数据类型 | 读取权限 | 写入权限 | 说明 |
|---|---|---|---|
| 公开数据 (设备名称) | 无要求 | N/A | 广播中可获取 |
| 一般数据 (电量) | 加密 | 加密 | 需要配对 |
| 敏感数据 (健康数据) | 加密 + MITM | 加密 + MITM | 需要确认配对 |
| 控制命令 (固件升级) | N/A | 加密 + 绑定 + 认证 | 最高安全级别 |
📱 设备无显示屏的配对方案
BLE 设备没有显示屏时,如何实现安全配对。
方案对比
| 方案 | 硬件要求 | 安全性 | 用户体验 | 典型设备 |
|---|---|---|---|---|
| Just Works | 无 | ⚠️ 低 (无 MITM) | ✅ 最简单 | 手环、传感器 |
| 固定 PIN | 无 | ⚠️ 中 (PIN 可能泄露) | ✅ 简单 | 耳机、车载 |
| 包装印刷 PIN | 无 | ✅ 高 (唯一 PIN) | ✅ 简单 | 智能锁、医疗设备 |
| 二维码/NFC | NFC 或二维码标签 | ✅ 高 (OOB 认证) | ✅ 扫码即可 | 智能家居 |
| 物理按钮确认 | 一个按钮 | ✅ 高 (用户确认) | ⚠️ 需按按钮 | 智能锁、开关 |
1. Just Works (最简单)
sequenceDiagram
participant Device as BLE 设备 无屏
participant Phone as 手机 App
Device->>Phone: 广播 可配对
Phone->>Device: 连接请求
Phone->>Device: 配对请求
Device->>Device: IO 能力 NoInputNoOutput
Phone->>Phone: 自动确认 Just Works
Device->>Phone: 配对完成 TK = 0
Device->>Phone: 加密连接建立
Note over Device,Phone: 无 MITM 保护,可能被中间人攻击
// Nordic SDK - Just Works 配置
// 适用于无显示屏、无输入能力的设备
void ble_gap_sec_params_init(void) {
ble_gap_sec_params_t sec_params = {0};
sec_params.bond = 1; // 允许绑定
sec_params.mitm = 0; // ⚠️ 不需要 MITM 保护
sec_params.lesc = 1; // 使用 LE Secure Connections
sec_params.io_caps = BLE_GAP_IO_CAPS_NONE; // 无输入无输出
// 密钥分发
sec_params.kdist_own.enc = 1;
sec_params.kdist_own.id = 1;
sec_params.kdist_peer.enc = 1;
sec_params.kdist_peer.id = 1;
sd_ble_gap_sec_params_reply(m_conn_handle,
BLE_GAP_SEC_STATUS_SUCCESS,
&sec_params,
NULL);
}
2. 固定 PIN 码 (设备出厂预设)
sequenceDiagram
participant Device as BLE 设备
participant Phone as 手机 App
participant User as 用户
Note over Device: 设备有固定 PIN
Note over Device: 如 0000 或 123456
Phone->>Device: 连接请求
Phone->>User: 提示输入 PIN
User->>Phone: 输入 PIN 0000
Phone->>Device: 配对请求 + PIN
Device->>Device: 验证 PIN 与预设比较
Device->>Phone: 配对成功
Note over Device,Phone: PIN 可能通过产品泄露
// 固定 PIN 码配置
#define FIXED_PIN "0000" // 固定 PIN 码
void ble_gap_sec_params_init(void) {
ble_gap_sec_params_t sec_params = {0};
sec_params.bond = 1;
sec_params.mitm = 0; // ⚠️ 固定 PIN 无 MITM 保护
sec_params.lesc = 0; // 可使用 LE Legacy
sec_params.io_caps = BLE_GAP_IO_CAPS_NONE;
sec_params.oob = 0;
// 设置固定 PIN
// 注意:LE Legacy 支持固定 PIN,LE SC 需要其他方案
sec_params.kdist_own.enc = 1;
sec_params.kdist_peer.enc = 1;
sd_ble_gap_sec_params_reply(m_conn_handle,
BLE_GAP_SEC_STATUS_SUCCESS,
&sec_params,
NULL);
}
3. 包装印刷唯一 PIN (推荐)
sequenceDiagram
participant Device as BLE 设备
participant Phone as 手机 App
participant User as 用户
participant Box as 产品包装
Note over Box: 包装上印刷唯一 PIN
Note over Box: 如 PIN A1B2C3D4
User->>User: 查看包装上的 PIN
User->>Phone: 输入 PIN
Phone->>Device: 配对请求 + PIN
Device->>Device: 验证 PIN 与 Flash 中存储比较
Device->>Phone: 配对成功
Note over Device,Phone: 每个设备唯一 PIN,更安全
// 包装印刷唯一 PIN 实现
// 每个设备烧录唯一 PIN (生产时写入 Flash)
#define PIN_LENGTH 8
static const uint8_t device_pin[PIN_LENGTH] = {
0xA1, 0xB2, 0xC3, 0xD4, // 唯一 PIN 码
0xE5, 0xF6, 0x07, 0x18
};
// 将 PIN 转换为 6 位数字用于配对
uint32_t get_passkey(void) {
// 使用 PIN 的哈希值生成 6 位数字
uint32_t hash = crc32(device_pin, PIN_LENGTH);
return hash % 1000000; // 6 位数字 (000000-999999)
}
void ble_gap_sec_params_init(void) {
ble_gap_sec_params_t sec_params = {0};
sec_params.bond = 1;
sec_params.mitm = 1; // ✅ 需要 MITM 保护
sec_params.lesc = 1; // 使用 LE Secure Connections
sec_params.io_caps = BLE_GAP_IO_CAPS_NONE;
sec_params.oob = 0;
// 设置 OOB 数据 (使用唯一 PIN)
sec_params.oob = 1;
memcpy(sec_params.oob_data, device_pin, PIN_LENGTH);
sec_params.kdist_own.enc = 1;
sec_params.kdist_peer.enc = 1;
sd_ble_gap_sec_params_reply(m_conn_handle,
BLE_GAP_SEC_STATUS_SUCCESS,
&sec_params,
NULL);
}
4. 二维码/NFC (OOB 带外配对)
sequenceDiagram
participant Device as BLE 设备
participant Phone as 手机 App
participant User as 用户
participant QR as 二维码 NFC
Note over QR: 二维码包含
Note over QR: 设备地址
Note over QR: OOB 数据
User->>Phone: 扫描二维码
Phone->>Phone: 解析 OOB 数据
Phone->>Device: 连接请求
Phone->>Device: 配对请求 + OOB 数据
Device->>Device: 验证 OOB 数据
Device->>Phone: 配对成功
Note over Device,Phone: 最高安全性,MITM 保护
// 二维码/NFC OOB 配对实现
// 生成二维码数据
// 格式:BLE:AA:BB:CC:DD:EE:FF:TK:12345678901234567890
void generate_qr_code(uint8_t *qr_data, size_t *len) {
ble_gap_addr_t addr;
sd_ble_gap_addr_get(&addr);
// 获取 OOB 数据 (128 位 TK)
uint8_t oob_tk[16];
sd_rand_application_vector_get(oob_tk, 16);
// 生成二维码字符串
*len = sprintf((char*)qr_data,
"BLE:%02X:%02X:%02X:%02X:%02X:%02X:TK:%032X",
addr.addr[5], addr.addr[4], addr.addr[3],
addr.addr[2], addr.addr[1], addr.addr[0],
oob_tk);
}
// OOB 配对配置
void ble_gap_sec_params_init(void) {
ble_gap_sec_params_t sec_params = {0};
ble_gap_lesc_oob_data_t oob_data;
sec_params.bond = 1;
sec_params.mitm = 1; // ✅ MITM 保护
sec_params.lesc = 1; // LE Secure Connections
sec_params.io_caps = BLE_GAP_IO_CAPS_NONE;
sec_params.oob = 1; // ✅ 启用 OOB
// 生成 OOB 数据
sd_ble_gap_lesc_oob_data_generate(&oob_data);
// 设置 OOB 数据
sec_params.kdist_own.enc = 1;
sec_params.kdist_peer.enc = 1;
sd_ble_gap_sec_params_reply(m_conn_handle,
BLE_GAP_SEC_STATUS_SUCCESS,
&sec_params,
&oob_data);
}
5. 物理按钮确认 (推荐用于智能锁等)
sequenceDiagram
participant Device as BLE 设备 有按钮
participant Phone as 手机 App
participant User as 用户
User->>Device: 长按按钮 3 秒
Device->>Device: 进入配对模式
Device->>Phone: 广播 可配对
Phone->>Device: 连接请求
Phone->>Device: 配对请求
Note over Device: 按钮灯闪烁
User->>Device: 短按按钮确认
Device->>Phone: 配对成功
Note over Device,Phone: 用户物理确认,MITM 保护
// 物理按钮确认实现
#define BUTTON_PIN 17
#define LONG_PRESS_MS 3000 // 长按 3 秒进入配对
void button_handler(uint8_t pin_no, uint8_t button_action) {
static uint32_t press_start = 0;
if (button_action == BUTTON_PUSH) {
press_start = app_timer_cnt_get();
} else if (button_action == BUTTON_RELEASE) {
uint32_t press_duration = app_timer_cnt_get() - press_start;
if (press_duration >= LONG_PRESS_MS) {
// 长按 3 秒,进入配对模式
enter_pairing_mode();
} else {
// 短按,确认配对
confirm_pairing();
}
}
}
void enter_pairing_mode(void) {
ble_gap_adv_params_t adv_params = {0};
adv_params.properties.type = BLE_GAP_ADV_TYPE_CONNECTABLE_SCANNABLE_UNDIRECTED;
adv_params.interval.min = 160; // 100ms
adv_params.interval.max = 160;
// 按钮灯闪烁提示
led_blink(3);
sd_ble_gap_adv_start(&adv_params, APP_BLE_CONN_CFG_TAG);
}
void confirm_pairing(void) {
// 按钮短按确认配对
sd_ble_gap_auth_key_reply(m_conn_handle,
BLE_GAP_AUTH_KEY_TYPE_NONE,
NULL);
// 按钮灯常亮提示
led_on();
}
方案选择建议
| 设备类型 | 推荐方案 | 理由 |
|---|---|---|
| 手环/传感器 | Just Works | 成本最低,用户体验好 |
| 智能锁 | 物理按钮 + 唯一 PIN | 高安全性,用户确认 |
| 医疗设备 | 包装印刷唯一 PIN | 唯一性,可追溯 |
| 智能家居 | 二维码/NFC | 扫码即可,体验好 |
| 耳机/音箱 | 固定 PIN (0000) | 行业惯例,简单 |
💾 配对信息存储详解
配对成功后保存什么信息?什么情况下会重新要求配对?
1. 配对成功后保存的信息
flowchart TB
subgraph Bonding["绑定信息 Bonding Data"]
LTK["LTK 长期密钥 16 bytes"]
IRK["IRK 身份解析密钥 16 bytes"]
ADDR["对端设备地址 6 bytes"]
EDIV["EDIV 加密除数 2 bytes"]
RAND["RAND 随机数 8 bytes"]
CSRK["CSRK 签名密钥 16 bytes"]
end
subgraph Storage["存储位置"]
Flash["设备 Flash"]
PhoneFlash["手机 Flash"]
end
LTK --> Flash
IRK --> Flash
ADDR --> Flash
EDIV --> Flash
RAND --> Flash
CSRK --> Flash
LTK --> PhoneFlash
IRK --> PhoneFlash
ADDR --> PhoneFlash
style Bonding fill:#e3f2fd,stroke:#007bff
style Storage fill:#d4edda,stroke:#28a745
保存的密钥信息
| 密钥 | 大小 | 用途 | 存储位置 |
|---|---|---|---|
| LTK Long Term Key |
16 bytes | 加密连接,重连时生成会话密钥 | 设备 + 手机 |
| IRK Identity Resolving Key |
16 bytes | 解析私有地址,识别设备 | 设备 + 手机 |
| CSRK Connection Signature Key |
16 bytes | 数据签名认证 | 设备 + 手机 |
| EDIV + RAND | 10 bytes | 与 LTK 一起唯一标识密钥 | 设备 + 手机 |
| 设备地址 | 6 bytes | 对端设备的 MAC 地址 | 设备 + 手机 |
2. 绑定信息存储示例
// Nordic SDK - 绑定信息存储结构
typedef struct {
// 密钥信息
uint8_t ltk[16]; // 长期密钥
uint8_t irk[16]; // 身份解析密钥
uint8_t csrk[16]; // 签名密钥
// 密钥标识
uint16_t ediv; // 加密除数
uint8_t rand[8]; // 随机数
// 设备信息
uint8_t peer_addr[6]; // 对端设备地址
uint8_t addr_type; // 地址类型 (公共/随机)
// 标志位
uint8_t flags; // 绑定标志
// bit0: LTK 存在
// bit1: IRK 存在
// bit2: CSRK 存在
// bit3: 已加密
} bond_info_t;
// 存储到 Flash
void store_bonding_info(bond_info_t *p_info) {
// 擦除 Flash 扇区
sd_flash_page_erase((uint32_t*)BONDING_FLASH_ADDR, 1);
// 写入绑定信息
sd_flash_write((uint32_t*)BONDING_FLASH_ADDR,
(uint32_t*)p_info,
sizeof(bond_info_t) / 4);
}
// 从 Flash 读取
void load_bonding_info(bond_info_t *p_info) {
memcpy(p_info,
(void*)BONDING_FLASH_ADDR,
sizeof(bond_info_t));
}
// 检查是否已绑定
bool is_bonded(ble_gap_addr_t *p_peer_addr) {
bond_info_t bond_info;
load_bonding_info(&bond_info);
// 比较设备地址
if (memcmp(bond_info.peer_addr,
p_peer_addr->addr,
6) == 0) {
return true; // 已绑定
}
return false; // 未绑定
}
3. 什么情况下会重新要求配对?
flowchart TB
A[设备尝试连接] --> B{检查绑定列表}
B -->|找到绑定 | C{密钥有效?}
B -->|未找到 | D[要求配对]
C -->|LTK 有效 | E[直接加密连接]
C -->|LTK 失效 | D
D --> F[用户确认]
F --> G[生成新密钥]
G --> H[更新绑定列表]
H --> E
style D fill:#ffc107,color:#000
style E fill:#28a745,color:#fff
需要重新配对的场景
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 设备恢复出厂设置 | Flash 中的绑定信息被清除 | 删除手机上的配对记录,重新配对 |
| 手机删除配对 | 手机清除了绑定信息 | 设备端也删除对应绑定,重新配对 |
| 密钥过期/失效 | 安全策略要求定期更新密钥 | 自动触发重新配对 |
| 设备地址变更 | 使用私有地址且 IRK 不匹配 | 重新配对更新 IRK |
| 跨设备配对 | 设备 A 配对,用手机 B 连接 | 每个手机需要独立配对 |
| 固件升级后 | 安全协议变更或密钥格式变化 | 清除旧绑定,重新配对 |
4. 绑定管理最佳实践
// Nordic SDK - 绑定管理
// 配对完成回调
void on_ble_gap_evt_auth_status(ble_gap_evt_auth_status_t *p_auth_status) {
if (p_auth_status->auth_status == BLE_GAP_SEC_STATUS_SUCCESS) {
// 配对成功
if (p_auth_status->bonded) {
// 已绑定,存储密钥到 Flash
bond_info_t bond_info;
// 获取密钥信息
ble_gap_sec_keys_t sec_keys;
sd_ble_gap_sec_info_get(m_conn_handle, &sec_keys);
// 填充绑定结构
memcpy(bond_info.ltk, sec_keys.p_enc_key->ltk_info.ltk, 16);
memcpy(bond_info.irk, sec_keys.p_id_key->id_info.irk, 16);
memcpy(bond_info.csrk, sec_keys.p_sign_key->csrk, 16);
bond_info.ediv = sec_keys.p_enc_key->ltk_info.ediv;
memcpy(bond_info.rand, sec_keys.p_enc_key->ltk_info.rand, 8);
// 获取对端地址
ble_gap_addr_t peer_addr;
sd_ble_gap_addr_get(&peer_addr);
memcpy(bond_info.peer_addr, peer_addr.addr, 6);
bond_info.addr_type = peer_addr.addr_type;
// 设置标志位
bond_info.flags = 0x07; // LTK + IRK + CSRK 都存在
// 存储到 Flash
store_bonding_info(&bond_info);
// 更新绑定计数
increment_bond_count();
}
} else {
// 配对失败处理
handle_pairing_failure(p_auth_status->auth_status);
}
}
// 重连时检查绑定
void on_ble_gap_evt_connected(ble_gap_evt_connected_t *p_connected) {
ble_gap_addr_t *p_peer_addr = &p_connected->peer_addr;
// 检查是否在绑定列表中
if (is_bonded(p_peer_addr)) {
// 已绑定,尝试加密连接
bond_info_t bond_info;
load_bonding_info(&bond_info);
// 检查 LTK 是否有效
if (bond_info.flags & 0x01) { // LTK 存在
// 使用存储的 LTK 加密连接
ble_gap_enc_info_t enc_info;
memcpy(enc_info.ltk, bond_info.ltk, 16);
enc_info.ediv = bond_info.ediv;
memcpy(enc_info.rand, bond_info.rand, 8);
sd_ble_gap_sec_info_reply(m_conn_handle, &enc_info, NULL, NULL);
} else {
// LTK 无效,要求重新配对
request_repairing();
}
} else {
// 未绑定,要求配对
request_pairing();
}
}
// 删除绑定
void delete_bonding(ble_gap_addr_t *p_peer_addr) {
bond_info_t bond_info;
load_bonding_info(&bond_info);
// 比较地址
if (memcmp(bond_info.peer_addr, p_peer_addr->addr, 6) == 0) {
// 清除绑定信息
memset(&bond_info, 0, sizeof(bond_info_t));
store_bonding_info(&bond_info);
// 减少绑定计数
decrement_bond_count();
}
}
// 清除所有绑定
void delete_all_bondings(void) {
// 擦除整个绑定 Flash 区域
for (int i = 0; i < BONDING_FLASH_PAGES; i++) {
sd_flash_page_erase((uint32_t*)(BONDING_FLASH_ADDR + i * 4096), 1);
}
}
5. 绑定数量限制
| 设备类型 | 典型绑定数 | Flash 占用 | 说明 |
|---|---|---|---|
| 手环/手表 | 1-2 个 | ~128 bytes | 通常只连接一个手机 |
| 蓝牙耳机 | 5-8 个 | ~512 bytes | 多设备切换 |
| 智能锁 | 10-20 个 | ~1-2 KB | 多家庭成员 |
| 蓝牙网关 | 50-100 个 | ~5-10 KB | 连接多个子设备 |
🛡️ 安全最佳实践
经典蓝牙
- 使用 SSP (2.1+), 避免旧 PIN 配对
- 启用 Mode 3 (链路级加密)
- 使用 16 位 PIN 码
- 定期更新密钥
BLE (推荐配置)
- 启用 LE Secure Connections (4.2+)
- 设置 mitm = 1 (MITM 保护)
- 使用 Numeric Comparison 或 OOB
- 密钥长度 16 bytes
- 启用绑定 (Bonding) 存储 LTK
避免的安全问题
- ❌ Just Works (无 MITM 保护)
- ❌ 密钥长度 < 16 bytes
- ❌ 不验证配对状态
- ❌ 明文传输敏感数据
- ❌ 使用 LE Legacy (4.0-4.1)
📊 安全级别对比
| 蓝牙版本 | 配对方式 | 加密算法 | MITM 保护 | 推荐度 |
|---|---|---|---|---|
| 经典 2.0- | PIN Code | E0 / SAFER+ | 可选 | ❌ 不推荐 |
| 经典 2.1+ (SSP) | Numeric/Passkey | E0 / AES | ✅ 支持 | ⚠️ 可用 |
| BLE 4.0-4.1 | LE Legacy | AES-CCM | ⚠️ 部分 | ⚠️ 可用 |
| BLE 4.2+ (LE SC) | ECDH P-256 | AES-CCM | ✅ 强制 | ✅ 推荐 |
| BLE 5.2+ (LE Audio) | LE SC + LC3 | AES-CCM | ✅ 强制 | ✅ 最佳 |