第4章 生成非对称密钥对用于身份验证:KeyPairGenerator、签名与验证流程

4.1 原理概述

在蓝牙持久化通信中,设备双方必须确认对方身份,防止中间人攻击或伪造设备接入。基于非对称密钥的身份验证通过“私钥签名、公钥验证”完成:

  • 发送方:使用自己的私钥对数据(如Challenge、时间戳或会话ID)进行签名,生成数字签名。
  • 接收方:使用发送方的公钥(预先交换或通过证书获得)验证签名是否匹配,从而确认数据来自持有对应私钥的设备。

Android Keystore 系统保证私钥一旦生成即无法导出,即使攻击者获取了应用文件也无法窃取私钥。本章基于 KeyPairGenerator 生成非对称密钥对,使用 Signature API完成签名与验证流程。

4.2 密钥生成配置

推荐使用 RSA (2048位) 或 EC (P-256) 算法。EC在相同安全级别下计算量更小,适合蓝牙低功耗场景。生成时需指定 KeyStore 别名、算法、密钥大小、以及用途(签名/验证)。

算法密钥长度签名算法优势
RSA2048位SHA256withRSA兼容性高,多数蓝牙芯片原生支持
ECP-256 (256位)SHA256withECDSA签名短,功耗低,适合BLE

4.3 实现流程

整体步骤分为三个环节:生成密钥对、签名数据、验证签名。以下为典型流程图。

非对称密钥签名与验证流程 发送方设备 Keystore 私钥 签名操作 输出签名+数据 接收方设备 发送方公钥 验证操作 结果:通过/失败 蓝牙 BLE / SPP 通道 关键保证 1. 私钥始终存储在Keystore硬件安全区 2. 公钥可通过蓝牙配对时交换 / 安全通道分发 3. 签名算法需与密钥算法匹配(如RSA→SHA256withRSA)

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 errorKeystore 未初始化或设备不支持在 onCreate 中调用 KeyStore.getInstance("AndroidKeyStore"),捕获 KeyStoreException 并回退软件方案
InvalidKeyException公私钥不匹配或算法不兼容检查签名算法与密钥算法一致(RSA用 SHA256withRSA,EC用 SHA256withECDSA)
SignatureException: Signature length incorrectECC 签名长度被截断或补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[])