第4章 生成非对称密钥对用于身份验证:KeyPairGenerator、签名与验证流程
4.1 原理概述
在蓝牙持久化通信中,设备双方必须确认对方身份,防止中间人攻击或伪造设备接入。基于非对称密钥的身份验证通过“私钥签名、公钥验证”完成:
- 发送方:使用自己的私钥对数据(如Challenge、时间戳或会话ID)进行签名,生成数字签名。
- 接收方:使用发送方的公钥(预先交换或通过证书获得)验证签名是否匹配,从而确认数据来自持有对应私钥的设备。
Android Keystore 系统保证私钥一旦生成即无法导出,即使攻击者获取了应用文件也无法窃取私钥。本章基于 KeyPairGenerator 生成非对称密钥对,使用 Signature API完成签名与验证流程。
4.2 密钥生成配置
推荐使用 RSA (2048位) 或 EC (P-256) 算法。EC在相同安全级别下计算量更小,适合蓝牙低功耗场景。生成时需指定 KeyStore 别名、算法、密钥大小、以及用途(签名/验证)。
| 算法 | 密钥长度 | 签名算法 | 优势 |
|---|---|---|---|
| RSA | 2048位 | SHA256withRSA | 兼容性高,多数蓝牙芯片原生支持 |
| EC | P-256 (256位) | SHA256withECDSA | 签名短,功耗低,适合BLE |
4.3 实现流程
整体步骤分为三个环节:生成密钥对、签名数据、验证签名。以下为典型流程图。
4.4 核心API与代码实现
4.4.1 生成密钥对并存入Keystore
// 需要 API 23+ 且设备支持硬件安全存储
KeyPairGenerator generator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
"bluetooth_auth_key", // 别名,用于后续引用
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
.setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(false) // 蓝牙场景通常无需用户生物认证
.build();
generator.initialize(spec);
KeyPair keyPair = generator.generateKeyPair();
// 无需手动保存私钥,Keystore 会自动管理
PublicKey publicKey = keyPair.getPublic(); // 公钥用于交换
4.4.2 签名(发送方)
// 从 Keystore 获取签名入口
PrivateKey privateKey = (PrivateKey) keyStore.getKey("bluetooth_auth_key", null);
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initSign(privateKey);
// 对要进行身份验证的数据签名(例如Challenge + 设备ID)
byte[] challengeData = getChallengeFromReceiver(); // 接收方发来的随机数
signature.update(challengeData);
byte[] signedData = signature.sign();
// 将 signedData 通过蓝牙发送给接收方
sendOverBluetooth(signedData);
4.4.3 验证(接收方)
// 接收方已预先获取发送方的公钥(通过安全交换或证书)
PublicKey senderPublicKey = getSenderPublicKey();
Signature signature = Signature.getInstance("SHA256withECDSA");
signature.initVerify(senderPublicKey);
signature.update(challengeData); // 必须使用相同的原始数据
boolean isValid = signature.verify(receivedSignedData);
if (isValid) {
// 身份验证通过,建立安全会话
} else {
// 拒绝连接,可能存在重放或伪造
}
4.5 调试要点
- 确认 Keystore 可用性:在低端设备或模拟器可能退化到软件Keystore,调用
keyStore.containsAlias()检查。 - 签名/验证使用相同数据:Challenge 必须严格一致,包括字节顺序和编码。推荐使用
ByteArrayOutputStream统一构造。 - 公钥交换方式:可以通过蓝牙初始服务发现(如 SDP)或外带方式(如二维码扫描 NFC)。
- 日志输出:打印签名长度、公钥格式(X.509)、算法 OID 以便比对。
4.6 部署集成注意事项
- 线程模型:签名/验证操作可能涉及硬件加密,建议不在主线程执行,使用
AsyncTask或HandlerThread。 - 密钥有效期:默认不设置过期时间,但建议每 90~180 天重新生成密钥对增强安全性。
- 多设备支持:每个远程设备应关联唯一的
KeyStore别名(如"bluetooth_auth_key_"+deviceMac)。
4.7 常见错误与解决方案
| 异常 | 可能原因 | 解决方案 |
|---|---|---|
KeyStoreException: Unknown error | Keystore 未初始化或设备不支持 | 在 onCreate 中调用 KeyStore.getInstance("AndroidKeyStore"),捕获 KeyStoreException 并回退软件方案 |
InvalidKeyException | 公私钥不匹配或算法不兼容 | 检查签名算法与密钥算法一致(RSA用 SHA256withRSA,EC用 SHA256withECDSA) |
SignatureException: Signature length incorrect | ECC 签名长度被截断或补0错误 | 接收方直接使用蓝牙原始字节数组,不要用 String.getBytes() 转换 |
NullPointerException 在 getKey() | 别名不存在或之前未保存 | 首次生成后立即调用 keyStore.setEntry() 或依赖 generateKeyPair() 自动存入 |
4.8 性能与稳定性优化
- 缓存公钥对象:验证操作频繁时,将
PublicKey实例缓存到ConcurrentHashMap中,避免每次从Keystore读取。 - 优先使用 EC P-256:签名体积小(约70字节),计算时间仅为 RSA2048 的 30%~50%。
- 开启硬件加速:在
KeyGenParameterSpec中调用.setIsStrongBoxBacked(true)(仅部分设备支持)提升抗物理攻击能力。 - 避免重复签名:蓝牙配对握手阶段,一次签名即可用于整个会话(配合会话ID和有效期),无需每条数据都签名。
安全警告:不要将私钥明文写入 SharedPreferences 或文件。Keystore 已保证私钥不可导出,但若使用
Provider 手动存储会破坏这一安全假设。公钥可以明文传输,但建议与证书链一起交换以防止公钥被替换。
实践建议:在蓝牙连接建立后的 Challenge-Response 流程中,让接收方先生成随机 Challenge 发给发送方,发送方签名后返回,接收方验证。这样可以防止重放攻击。Challenge 长度建议 16~32 字节。
4.9 完整流程伪代码
// 步骤A:设备准备阶段(双方)
KeyPairGenerator(EC, P-256, "bluetooth_auth_key") → KeyPair
// 步骤B:配对阶段(例如BLE广播或SPP连接后)
1. 接收方生成随机 challenge (32 bytes) → 发送给发送方
2. 发送方:
- 获取私钥: keyStore.getKey("bluetooth_auth_key", null)
- Signature.initSign(privateKey)
- Signature.update(challenge)
- signedData = Signature.sign()
- 发送 signedData + 发送方公钥(若初次连接)
3. 接收方:
- 存储/更新发送方公钥
- Signature.initVerify(publicKey)
- Signature.update(challenge)
- result = Signature.verify(signedData)
- 返回 ACK / NAK
// 步骤C:后续会话(可选)
- 会话中可使用对称密钥加密,但身份验证仍基于签名
关键API汇总
KeyPairGenerator.getInstance(String algorithm, String provider)KeyGenParameterSpec.Builder.setPurposes(int purposes)Signature.getInstance(String algorithm)initSign(PrivateKey) / initVerify(PublicKey)update(byte[]) / sign() / verify(byte[])