第53章:安全与加密——数据加密(AES/RSA)、KeyStore、安全存储、混淆(ProGuard/R8)
说到移动端安全,我估计不少朋友第一反应是「加个密就完事了」。嗯,这话对了一半。加密确实重要,但怎么加密、密钥放哪、代码怎么防逆向,这些才是真正的坑。我在项目中见过太多「加密了但等于没加密」的案例,说白了就是姿势不对。
这一章,咱们就聊聊 Kotlin 项目里那些绕不开的安全话题。我会结合自己的踩坑经验,把 AES、RSA、KeyStore、安全存储和代码混淆这些知识点串起来。你想想看,如果连密钥都硬编码在代码里,那加密还有什么意义?
核心观点:安全不是单一技术,而是一套组合拳。加密算法 + 密钥管理 + 存储安全 + 代码混淆,缺一不可。
53.1 对称加密 vs 非对称加密:选哪个?
先理清概念。对称加密,比如 AES,加密和解密用同一个密钥。速度快,适合加密大量数据。非对称加密,比如 RSA,用公钥加密、私钥解密。速度慢,但解决了密钥分发问题。
我个人习惯是:数据量大用 AES,密钥传输用 RSA。说白了就是混合使用——用 RSA 加密 AES 的密钥,再用 AES 加密实际数据。这样既安全又高效。
AES 加密示例(Kotlin)
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import java.util.Base64
fun aesEncrypt(plainText: String, secretKey: SecretKey): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val iv = ByteArray(12) // GCM 推荐 12 字节 IV
SecureRandom().nextBytes(iv)
val gcmSpec = GCMParameterSpec(128, iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)
val encrypted = cipher.doFinal(plainText.toByteArray())
// 将 IV 和密文拼接后 Base64 编码
val combined = iv + encrypted
return Base64.getEncoder().encodeToString(combined)
}
我的经验:GCM 模式比 CBC 更推荐,因为它自带认证标签,能防篡改。我之前用 CBC 模式时,就遇到过数据被中间人修改但解密没报错的情况,后来全换成 GCM 了。
RSA 加密示例(Kotlin)
import java.security.KeyPairGenerator
import java.security.PublicKey
import javax.crypto.Cipher
import java.util.Base64
fun rsaEncrypt(data: String, publicKey: PublicKey): String {
val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
val encrypted = cipher.doFinal(data.toByteArray())
return Base64.getEncoder().encodeToString(encrypted)
}
注意:RSA 一次能加密的数据长度有限,通常不超过密钥长度减去填充开销。比如 2048 位密钥,最多加密 190 字节左右。所以别拿 RSA 直接加密大文件,会崩的。
53.2 密钥管理:别把密钥写死在代码里
我见过最离谱的代码,是把 AES 密钥直接写成一个字符串常量,还叫 SECRET_KEY。反编译一下,明晃晃的就在那。你想想看,这跟没加密有啥区别?
正确的做法是使用 Android KeyStore。KeyStore 是系统级的安全容器,密钥存储在硬件安全模块(TEE/SE)中,应用进程根本拿不到密钥明文。
使用 KeyStore 生成和存储密钥
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import java.security.KeyStore.SecretKeyEntry
fun storeKeyInKeyStore(alias: String): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply {
load(null)
}
if (keyStore.containsAlias(alias)) {
return (keyStore.getEntry(alias, null) as SecretKeyEntry).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()
}
关键点:KeyStore 生成的密钥,应用只能通过别名引用,无法直接导出。就算应用被逆向,攻击者也拿不到密钥本身。这是 Android 官方推荐的做法。
53.3 安全存储:SharedPreferences 不安全,那用什么?
SharedPreferences 默认以 XML 明文存储,root 过的手机随便看。我建议用 EncryptedSharedPreferences,它是 Jetpack Security 库提供的,底层用 AES256-GCM 加密,密钥存在 KeyStore 里。
使用 EncryptedSharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPreferences = EncryptedSharedPreferences.create(
context,
"secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 存数据
sharedPreferences.edit().putString("token", "your_token_here").apply()
// 读数据
val token = sharedPreferences.getString("token", null)
避坑指南:我曾经把用户 token 直接存 SharedPreferences,结果测试机被同事 root 后抓到了明文 token。后来全项目统一改用 EncryptedSharedPreferences,再也没出过类似问题。
53.4 代码混淆:ProGuard 和 R8
加密做得再好,代码逻辑被反编译看光也是白搭。混淆的作用就是让反编译后的代码难以阅读。Android 默认用 R8 替代了 ProGuard,但配置方式类似。
混淆配置要点
# proguard-rules.pro 示例
# 保留数据类,防止序列化/反序列化出错
-keep class com.example.data.** { *; }
# 保留反射调用的类
-keep class com.example.reflection.** { *; }
# 保留 JNI 方法
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
注意:混淆不是万能的。它只是重命名类名、方法名和变量名,并不能加密字符串。像 API Key、加密密钥这些敏感信息,即使混淆了,通过字符串搜索还是能找到。所以密钥一定要放 KeyStore,别指望混淆来保护。
53.5 知识体系总览
下面这张图,是我梳理的安全与加密知识体系。你可以把它当作一个检查清单,做项目时对照着来。
53.6 实战建议:一个完整的安全方案
说了这么多,我总结一下在实际项目中怎么落地。我个人习惯按这个顺序来:
- 通信加密:所有 API 请求走 HTTPS,证书固定(Certificate Pinning)防止中间人攻击。
- 本地存储:敏感数据用 EncryptedSharedPreferences 或 SQLCipher 加密数据库。
- 密钥管理:所有密钥通过 KeyStore 生成和存储,绝不硬编码。
- 代码保护:开启 R8 混淆,配置好保留规则,防止核心逻辑被轻易逆向。
- 运行时检测:检测 root、模拟器、调试状态,发现异常及时退出或降级。
最后说一句:安全没有银弹。加密算法再强,密钥管理不当也是白搭。代码混淆再复杂,字符串明文暴露也是徒劳。我建议你把这一章的内容当作一个 checklist,每做一个新项目,都对照着检查一遍。嗯,这样至少能挡住 90% 的初级攻击者。
公众号:蓝海资料掘金营,微信 deep3321