Android加密基础:AES、RSA、密钥存储方案对比

做Android开发,尤其是涉及到用户敏感数据的时候,加密是绕不开的一关。我个人刚接触这块时也晕过——AES、RSA、还有各种KeyStore,到底选哪个?怎么搭才不出ANR?

说白了,加密不是万能药,但不用它,你的App就像没锁门的保险柜。这一章咱们就把Android上最常用的两种加密算法——AES和RSA——彻底理清楚,再聊聊密钥该怎么存才安全。

本章核心:搞懂对称加密与非对称加密的区别,掌握Android上AES和RSA的使用姿势,避开密钥存储的那些坑。

对称加密:AES

AES(Advanced Encryption Standard)是目前最主流的对称加密算法。什么叫对称?加密和解密用的是同一个密钥。速度快、适合加密大量数据,比如文件、图片、数据库字段。

Android上使用AES,我建议用AES/GCM/NoPadding模式。GCM模式自带认证标签,能防篡改。你想想,如果只加密不验证,坏人改了密文你都不知道。

核心参数

参数推荐值说明
算法AES密钥长度128/192/256位,Android推荐256
工作模式GCM自带认证,不需要额外HMAC
填充方式NoPaddingGCM内部处理,不额外填充
IV12字节随机每次加密必须不同,否则泄露明文模式
小心IV重复:我曾经踩过这个坑——用了固定IV,导致同样的明文每次产生同样的密文,攻击者能看出规律。每次加密都要生成新的随机IV,并把它和密文一起存下来。

代码示例:AES-GCM加密与解密

// 加密
fun encryptAesGcm(plainText: ByteArray, key: SecretKey): ByteArray {
    val iv = ByteArray(12).apply { SecureRandom().nextBytes(this) }
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, iv)
    cipher.init(Cipher.ENCRYPT_MODE, key, spec)
    val cipherText = cipher.doFinal(plainText)
    // 返回 iv + cipherText,解密时需要一起提供
    return iv + cipherText
}

// 解密
fun decryptAesGcm(encryptedData: ByteArray, key: SecretKey): ByteArray {
    val iv = encryptedData.copyOfRange(0, 12)
    val cipherText = encryptedData.copyOfRange(12, encryptedData.size)
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val spec = GCMParameterSpec(128, iv)
    cipher.init(Cipher.DECRYPT_MODE, key, spec)
    return cipher.doFinal(cipherText)
}

看到没?加密时我把IV和密文拼在一起返回,解密时再切出来。这样省去了单独存储IV的麻烦。GCM的认证标签由内部自动校验,如果密文被篡改,doFinal会抛AEADBadTagException。

非对称加密:RSA

RSA是非对称加密的代表——公钥加密,私钥解密。速度比AES慢得多,但它能解决密钥分发问题。比如AES的密钥怎么安全传给对方?用RSA加密那个AES密钥,再传过去。

RSA在Android上的最佳实践

  • 密钥长度:2048位(4096太慢,1024已不安全)
  • 填充方案:必须用RSA/ECB/OAEPWithSHA-256AndMGF1Padding,千万不要用PKCS1Padding(有已知漏洞)
  • 加密数据长度限制:OAEP模式下最多加密190字节(2048位密钥)。超出就报错
实用建议:RSA别直接加密大文件。我习惯的做法是:生成一个随机的AES密钥(32字节),用RSA公钥加密它,然后用AES密钥加密真正的数据。这样既利用了RSA的安全分发,又享受了AES的高效。

代码示例:RSA-OAEP加密

fun rsaEncrypt(data: ByteArray, publicKey: PublicKey): ByteArray {
    val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
    cipher.init(Cipher.ENCRYPT_MODE, publicKey)
    return cipher.doFinal(data)  // data长度不能超过190字节
}

密钥存储方案对比

密钥放哪?放SharedPreferences?那不叫加密,叫笑话。Android提供了几种安全存储方案,我根据项目经验排了个表:

方案安全性性能适用场景避坑点
Android KeyStore极高(硬件隔离)中等RSA私钥、AES密钥API 23以上才支持硬件安全模块,低版本可能回退到软件实现
EncryptedSharedPreferences高快小数据、、偏好设置内部使用AES-256+GCM,但密钥仍托管在KeyStore
NDK + 硬编码密钥低(可逆向)极快不推荐,除非整包加固我见过有人把密钥写死在so里,结果被IDA一把梭
远程密钥服务器极高依赖网络动态密钥,需联网有网络延迟,可能造成ANR,需异步处理
我的首选:Android KeyStore + AES-GCM。KeyStore生成并存储AES密钥,应用只拿到一个别名,真正的密钥数据App无法导出。这样就算App被反编译,密钥也拿不到。

生成与使用KeyStore中的密钥

// 生成AES密钥存入KeyStore
fun generateAesKeyInKeyStore(alias: String): SecretKey {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .build()
    )
    return keyGenerator.generateKey()
}

// 获取已存在的密钥
fun getKeyFromKeyStore(alias: String): SecretKey? {
    val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    return keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry
}

注意:setKeySize(256)在部分Android 10以下设备上可能不支持,建议做运行时检测。如果设备不支持256位,可以降级到128位,但要记录日志。

知识体系框架

下面这张图把本章的核心脉络串起来了。你能看到AES和RSA各自负责什么,密钥怎么存,以及组合起来的样子。

Android加密基础:AES、RSA、密钥存储方案 对称加密:AES-GCM 非对称加密:RSA-OAEP 密钥256位 GCM模式+认证 IV随机12字节 密钥2048位 OAEP+SHA-256填充 密钥存储方案 Android KeyStore EncryptedSP 远程密钥服务器 NDK硬编码(不推荐) 推荐组合 实际工程中:RSA加密AES密钥,AES加密用户数据,密钥存于Android KeyStore

组合策略:RSA + AES + KeyStore

一个一个讲完了,来一个实战组合拳。我最近重构的一个金融类App就是这样干的:

  1. App启动时,从KeyStore取出或生成AES密钥(别名"user_aes_key")
  2. 登录时,服务器下发RSA公钥(证书绑定防中间人)
  3. 用RSA公钥加密本地AES密钥,密文上传服务器
  4. 服务器用私钥解密,拿到AES密钥用于后续通信
  5. 本地数据直接用AES-GCM加密存储,密钥全程不离开KeyStore
特别注意ANR问题:KeyStore操作(特别是首次生成密钥)可能涉及硬件交互,可能耗时几十毫秒到几百毫秒。千万不要在主线程调用。我记得有一次在启动时直接生成AES密钥,结果低端机卡了300多ms,被系统打了一个ANR。后来改成在后台协程里预生成,等用的时候再取。

至于解密,我建议把解密操作做成挂起函数,用Dispatchers.IO调度。这样既不会阻塞UI,又能利用多核。

常见误区总结

  • 把密钥硬编码在代码里——反编译一下全暴露,形同虚设
  • AES使用ECB模式——同样的明文块产生同样的密文块,照片加密后还能看出轮廓
  • RSA直接加密大块数据——超过密钥长度限制直接抛异常,正确的做法是混合加密
  • 忽略KeyStore的异常回退——部分老旧设备不支持硬件KeyStore,需要降级到软件实现,但必须记录并告警