14、蓝牙安全与加密:安全等级、配对加密通道、MITM保护与KeyStore存储

各位同学,今天我们来聊聊蓝牙安全。说实话,这个主题在蓝牙开发里属于“平时没人问,出事就背锅”的那种。我早年做第一个蓝牙项目时,就因为在安全等级上偷了个懒,结果被安全团队打回来三次……嗯,从那以后我就老老实实把这块啃透了。

14.1 安全等级:LEVEL_LOW / MEDIUM / HIGH

蓝牙协议栈里定义了三个安全等级,说白了就是告诉系统:“我这个服务有多怕被偷看”。

等级 常量值 含义 典型场景
LOW 0 不加密,不认证 设备发现、广播数据
MEDIUM 1 加密但不强制MITM保护 文件传输、普通数据同步
HIGH 2 加密 + 强制MITM保护 支付、健康数据、门禁控制

我个人习惯在定义GATT服务时,直接给每个Characteristic配上安全等级。比如这样:

// 定义特征权限时指定安全等级
new BluetoothGattCharacteristic(
    UUID_HEART_RATE_MEASUREMENT,
    BluetoothGattCharacteristic.PROPERTY_NOTIFY,
    BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED  // 对应MEDIUM
);

// 如果需要HIGH等级,用这个
BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM

你想想看,如果心率数据被中间人截获了,虽然不至于丢钱,但隐私泄露也挺麻烦的。所以我建议:凡是涉及用户个人数据的,至少用MEDIUM

核心原则:安全等级不是越高越好。HIGH等级会强制配对过程中的用户确认,用户体验会下降。我见过有些App把所有服务都设成HIGH,结果每次连接都要弹窗确认,用户直接卸载了。

14.2 配对加密通道:从临时密钥到长期密钥

蓝牙配对加密的过程,其实有点像两个人第一次见面交换密码本。我简单梳理一下流程:

  1. 配对请求:发起方告诉对方自己支持哪些加密算法
  2. 临时密钥生成:双方通过某种方式(如PIN码、Numeric Comparison)生成一个短期密钥
  3. 链路加密:用临时密钥加密当前连接
  4. 长期密钥分发:在加密通道里交换长期密钥(LTK),用于后续重连
  5. 绑定:双方保存对方的LTK,下次连接直接复用

这里有个坑——我曾经遇到过一个问题:设备A和设备B配对成功后,A重启了,结果B怎么都连不上。查了半天发现是A的蓝牙芯片把LTK存在了易失性内存里,重启就丢了。所以长期密钥一定要持久化存储

14.3 MITM保护:中间人攻击的克星

MITM(Man-In-The-Middle)攻击,说白了就是有个坏蛋站在你和设备中间,假装是你跟设备说话,又假装是设备跟你说话。蓝牙里怎么防?靠的是配对过程中的用户交互

Android蓝牙协议栈支持两种MITM保护方式:

  • Numeric Comparison:两边显示一个6位数字,你确认是否一致。这是最常用的方式。
  • Passkey Entry:一边显示数字,另一边输入。适合有键盘的设备。
  • Out of Band:通过NFC等带外通道交换密钥。安全性最高,但需要硬件支持。
避坑指南:我曾经在开发一个智能门锁App时,发现某些手机在配对时不会弹出Numeric Comparison的确认框。后来查了源码才知道,如果手机和门锁都支持“Just Works”模式,系统会跳过MITM保护。解决方案是:在门锁端强制要求MITM保护,这样手机端就必须走Numeric Comparison流程。

14.4 Android KeyStore:蓝牙密钥的安全保险箱

密钥存哪儿?SharedPreferences?那太危险了。Android提供了KeyStore系统,专门用来安全存储密钥材料。蓝牙协议栈的长期密钥(LTK)就应该存在这里。

我一般这么用:

// 获取KeyStore实例
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

// 生成或导入蓝牙LTK
// 注意:KeyStore不直接存储原始密钥字节,而是通过KeyGenerator或KeyStore.SecretKeyEntry
// 这里以存储一个AES密钥为例
KeyGenerator keyGenerator = KeyGenerator.getInstance(
    KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGenerator.init(
    new KeyGenParameterSpec.Builder("bluetooth_ltk_" + deviceAddress,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .build());
SecretKey key = keyGenerator.generateKey();

// 读取时
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(
    "bluetooth_ltk_" + deviceAddress, null);
SecretKey storedKey = entry.getSecretKey();

你可能会问:为什么不直接用SharedPreferences存个字符串?嗯,因为KeyStore里的密钥是硬件隔离的——即使App被攻破了,攻击者也拿不到密钥原文。我见过一个案例,某厂商把蓝牙密钥明文存在/data/data下,结果被Root过的手机直接读走了,所有已配对的设备都能被冒充。

重要提醒:KeyStore在Android 4.3(API 18)引入,但硬件-backed的KeyStore需要Android 6.0+。如果你的App需要兼容老设备,建议用KeyStore + 加密SharedPreferences做降级方案。但说实话,现在Android 6.0以下的设备已经很少了,我建议直接放弃兼容。

14.5 知识体系总览

下面这张图是我自己总结的蓝牙安全知识结构,你可以把它当作一个快速索引:

蓝牙安全知识体系 安全等级 LEVEL_LOW (0) LEVEL_MEDIUM (1) LEVEL_HIGH (2) → 决定加密与认证策略 配对加密通道 临时密钥 (STK) 长期密钥 (LTK) 链路加密 (AES-CCM) → 保证数据传输机密性 MITM保护 Numeric Comparison Passkey Entry Out of Band (NFC) → 防止中间人攻击 Android KeyStore 密钥存储 硬件隔离存储 | 防篡改 | 密钥不可导出 → 长期密钥(LTK)的安全保险箱

这张图把蓝牙安全的四个核心模块串起来了。从上往下看:安全等级决定了你要不要加密、要不要MITM保护;配对加密通道负责生成和交换密钥;MITM保护确保配对过程不被篡改;最后,所有密钥都存进KeyStore这个保险箱里。

我个人觉得,理解这四者的关系比死记API更重要。因为实际项目中,你遇到的往往是“为什么这个设备连不上”、“为什么配对不弹窗”这类问题——只有理解了底层逻辑,才能快速定位。


公众号:蓝海资料掘金营,微信deep3321