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。
14.2 配对加密通道:从临时密钥到长期密钥
蓝牙配对加密的过程,其实有点像两个人第一次见面交换密码本。我简单梳理一下流程:
- 配对请求:发起方告诉对方自己支持哪些加密算法
- 临时密钥生成:双方通过某种方式(如PIN码、Numeric Comparison)生成一个短期密钥
- 链路加密:用临时密钥加密当前连接
- 长期密钥分发:在加密通道里交换长期密钥(LTK),用于后续重连
- 绑定:双方保存对方的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等带外通道交换密钥。安全性最高,但需要硬件支持。
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过的手机直接读走了,所有已配对的设备都能被冒充。
14.5 知识体系总览
下面这张图是我自己总结的蓝牙安全知识结构,你可以把它当作一个快速索引:
这张图把蓝牙安全的四个核心模块串起来了。从上往下看:安全等级决定了你要不要加密、要不要MITM保护;配对加密通道负责生成和交换密钥;MITM保护确保配对过程不被篡改;最后,所有密钥都存进KeyStore这个保险箱里。
我个人觉得,理解这四者的关系比死记API更重要。因为实际项目中,你遇到的往往是“为什么这个设备连不上”、“为什么配对不弹窗”这类问题——只有理解了底层逻辑,才能快速定位。