第3章 对称密钥用于蓝牙加密:KeyGenerator配置、密钥别名、访问限制
3.1 原理概述
蓝牙通信的加密层依赖对称密钥(AES-CCM)来保护空中数据包。在嵌入式 Android 环境中,对称密钥不应以明文形式存储在应用私有目录或 SQLite 中,而应托管于 Android Keystore,利用硬件安全模块(TEE / StrongBox)实现密钥材料不导出。本章聚焦三个核心维度:
- KeyGenerator 配置:算法、密钥尺寸、随机数来源、GCM/CCM 模式参数。
- 密钥别名:字符串标识符的命名规范、作用域、与蓝牙设备地址的持久化映射策略。
- 访问限制:应用级隔离、用户认证门控、导出策略、KeyStore 超时与锁定行为。
关键设计原则:对称密钥一旦写入 KeyStore,其明文在应用进程空间中应被彻底清除;后续蓝牙加密操作仅通过
KeyStore.getEntry() 获取 SecretKey 句柄,而非字节数组。
3.2 KeyGenerator 配置参数
Android KeyGenerator 需通过 KeyGenParameterSpec.Builder 指定所有安全属性。下表列出蓝牙 AES-CCM 场景的必备与推荐参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
keySize |
128 / 256 | 蓝牙 4.2+ 强制 AES-128;256 位提供余量,需确认 SoC 支持 |
blockMode |
KeyProperties.BLOCK_MODE_GCM |
蓝牙 LE Secure Connections 使用 AES-CCM,GCM 是相近的认证加密模式 |
encryptionPaddings |
KeyProperties.ENCRYPTION_PADDING_NONE |
GCM/CCM 不需要填充,使用 NoPadding |
keyValidityStart |
当前时间戳 | 立即生效,防止使用过期生成的重放密钥 |
keyValidityForOriginationEnd |
null 或 90 天 | 可设定密钥轮换周期,过期后仅能解密不能加密 |
randomizedEncryptionRequired |
true | 强制每次加密使用随机 IV,避免蓝牙包重放 |
userAuthenticationRequired |
false 或 true | 蓝牙后台加密通常关闭;高安全场景可绑定生物认证 |
3.3 密钥别名机制
别名是 KeyStore 中唯一标识对称密钥的字符串。蓝牙持久化场景的别名设计需满足:可追溯、防冲突、支持设备更换。
3.3.1 命名规范
// 推荐格式:bt_aes_{蓝牙MAC地址(无冒号)}_{应用包名hash}
// 示例:
String alias = "bt_aes_001A7BDA2311_" + appHash;
- 前缀固定:
bt_aes_区分密钥用途,避免与 RSA/EC 混淆。 - 蓝牙地址:全大写、无冒号,确保持久化映射唯一。
- 应用 Hash:多进程或多 profile 场景下进一步隔离。
3.3.2 持久化映射表
别名与蓝牙设备地址的映射存储在 EncryptedSharedPreferences 中,而非明文 SharedPreferences:
// 存储映射
String alias = buildAlias(deviceAddress);
encryptedPrefs.edit()
.putString("key_alias_" + deviceAddress, alias)
.apply();
// 查询映射
String savedAlias = encryptedPrefs.getString("key_alias_" + deviceAddress, null);
注意:切勿将蓝牙地址直接拼接为别名而不加 Hash 或包名前缀。当同一设备多应用共享蓝牙适配器时,别名冲突会导致密钥覆盖。
3.4 访问限制模型
Android KeyStore 的访问限制从应用沙箱、用户认证、导出策略三个层级施加控制:
| 层级 | 控制方式 | 蓝牙场景建议 |
|---|---|---|
| 应用沙箱 | 每个 UID 独立的 KeyStore 空间 | 默认隔离,跨应用共享密钥需使用 KeyStore.setKeyStoreFlag() + 签名权限 |
| 用户认证 | setUserAuthenticationRequired(true) + 超时 |
仅用于配对阶段的高敏感操作;后台加密建议关闭 |
| 导出策略 | setIsStrongBoxBacked(true) 且 KeyProperties.PURPOSE_ENCRYPT | PURPOSE_DECRYPT |
进入 StrongBox 后密钥材料永不可导出,即使 root 也无法提取 |
| 密钥过期 | keyValidityForOriginationEnd |
设置 90 天轮换,到期自动失效 |
3.5 完整实现流程
- 初始化 KeyGenerator:指定算法
KeyProperties.KEY_ALGORITHM_AES。 - 构建参数规格:通过
KeyGenParameterSpec.Builder设置密钥大小、块模式、填充、用途、有效期、用户认证标志。 - 生成密钥:调用
KeyGenerator.generateKey(),密钥直接写入 KeyStore,不入应用内存。 - 存储别名映射:将
(蓝牙地址 → 别名)写入EncryptedSharedPreferences。 - 读取与使用:通过
KeyStore.getEntry(alias, null)获取SecretKey句柄,用于 Cipher 初始化。 - 轮换与清理:调用
KeyStore.deleteEntry(alias)并移除映射记录。
3.6 代码实现:KeyGenerator 配置与密钥生成
以下示例展示在 Android 应用中生成 AES-128 GCM 密钥并建立蓝牙地址映射的完整代码:
// ========== 1. 构建 KeyGenParameterSpec ==========
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setKeySize(128)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setRandomizedEncryptionRequired(true)
.setKeyValidityStart(Calendar.getInstance().getTime())
.setKeyValidityForOriginationEnd(null) // 不限制加密期限
.setUserAuthenticationRequired(false) // 后台蓝牙不要求用户认证
.build();
// ========== 2. 初始化 KeyGenerator ==========
KeyGenerator keyGen = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGen.init(spec);
// ========== 3. 生成密钥(直接写入 KeyStore)==========
keyGen.generateKey();
// ========== 4. 存储别名映射到 EncryptedSharedPreferences ==========
String aliasKey = "key_alias_" + deviceAddress;
encryptedPrefs.edit().putString(aliasKey, alias).apply();
// ========== 5. 后续蓝牙加密时取出句柄 ==========
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(alias, null);
// ========== 6. 初始化 Cipher 用于蓝牙包加密 ==========
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV(); // 随机 IV,附带在蓝牙包中传输
生产建议:
setKeyValidityForOriginationEnd 推荐设为 90 天,并在蓝牙连接建立前检查当前时间是否在有效期内。过期密钥应触发重新生成并更新映射表。
3.7 调试与验证方法
对称密钥生成后,可通过以下手段验证密钥是否正确安装且访问受限策略生效:
- KeyStore 内容列举:
keyStore.aliases()迭代输出所有别名,确认新生成的bt_aes_别名存在。 - 密钥导出测试:尝试
secretKey.getEncoded(),若返回null说明密钥不可导出(StrongBox 或 KeyStore 策略生效)。 - 加密/解密往返:用同一密钥加密随机 16 字节数据,再解密,验证结果一致。
- 跨应用访问验证:另一进程使用同一别名查询,应抛出
KeyStoreException或返回null。
// 调试日志示例
Log.d("BT_KEYSTORE", "Alias: " + alias);
Log.d("BT_KEYSTORE", "Key algo: " + secretKey.getAlgorithm());
Log.d("BT_KEYSTORE", "Exportable: " + (secretKey.getEncoded() != null ? "YES" : "NO (secure)"));
3.8 常见错误与解决方案
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
KeyStoreException: KeyStore未初始化 |
未调用 keyStore.load(null) |
在 getKey() 前执行 keyStore.load(null) |
InvalidAlgorithmParameterException: GCM 需要 128 位密钥 |
蓝牙 SoC 不支持 256 位 AES 或 GCM 模式限制 | 降级至 setKeySize(128) |
KeyStoreException: 别名已存在且无法覆盖 |
未先 deleteEntry() 直接重新生成 |
生成前调用 keyStore.containsAlias(alias) 并删除旧密钥 |
NullPointerException: SecretKey 为 null |
别名拼写错误或映射表与 KeyStore 不一致 | 统一通过常量构建别名,避免硬编码字符串 |
SecurityException: 用户未认证 |
setUserAuthenticationRequired(true) 且未解锁设备 |
蓝牙后台场景关闭用户认证,或使用 setUserAuthenticationValidityDurationSeconds(300) |
3.9 性能与稳定性优化
- 密钥句柄缓存:
KeyStore.getKey()涉及 Binder 跨进程调用,蓝牙高频加密场景应将SecretKey句柄缓存在应用内存中(非密钥材料),避免每次加密都查询 KeyStore。 - 批量预生成:如果应用需要同时管理多个蓝牙设备,可在后台线程一次性生成所有密钥并建立映射,减少配对阶段的延迟。
- 别名索引加速:映射表使用
ConcurrentHashMap做内存缓存,键为蓝牙地址,值为别名,避免频繁读取 EncryptedSharedPreferences。 - StrongBox 退避:如果设备部署 StrongBox,写入延迟约 100-300ms,应在
AsyncTask或WorkManager中执行生成操作,避免阻塞蓝牙连接建立。
稳定性红线:不要在
BluetoothGatt 连接回调中直接执行 KeyGenerator.generateKey()。该操作可能触发关键池熵收集,导致连接超时。应始终在连接建立前的准备阶段完成密钥生成。
3.10 部署清单检查
- ✓ 确认
AndroidManifest.xml声明android:keyStore权限(无需额外权限,KeyStore 默认可用)。 - ✓ 验证目标设备 API Level ≥ 23(Marshmallow),
KeyGenParameterSpec最低支持此版本。 - ✓ 在
build.gradle中设定minSdkVersion 23或使用兼容库处理低版本回退。 - ✓ 测试环境确认 SoC 支持
KeyProperties.PURPOSE_ENCRYPT | PURPOSE_DECRYPT组合。 - ✓ 使用
EncryptedSharedPreferences(AndroidX Security Crypto)存储别名映射,主键为蓝牙地址。 - ✓ 集成 KeyStore 内容定期巡检,清理孤儿密钥(有映射但已无对应蓝牙设备)。