第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 知识体系总览

下面这张图,是我梳理的安全与加密知识体系。你可以把它当作一个检查清单,做项目时对照着来。

安全与加密 加密算法 • AES(对称加密) • RSA(非对称加密) • 混合加密方案 密钥管理 • Android KeyStore • 硬件安全模块 • 密钥别名引用 安全存储 • EncryptedSharedPrefs • 文件级加密 • 数据库加密 代码混淆 • ProGuard / R8 • 混淆规则配置 • 保留规则 最佳实践 • 密钥不硬编码 • 传输层用 HTTPS • 定期轮换密钥

53.6 实战建议:一个完整的安全方案

说了这么多,我总结一下在实际项目中怎么落地。我个人习惯按这个顺序来:

  1. 通信加密:所有 API 请求走 HTTPS,证书固定(Certificate Pinning)防止中间人攻击。
  2. 本地存储:敏感数据用 EncryptedSharedPreferences 或 SQLCipher 加密数据库。
  3. 密钥管理:所有密钥通过 KeyStore 生成和存储,绝不硬编码。
  4. 代码保护:开启 R8 混淆,配置好保留规则,防止核心逻辑被轻易逆向。
  5. 运行时检测:检测 root、模拟器、调试状态,发现异常及时退出或降级。

最后说一句:安全没有银弹。加密算法再强,密钥管理不当也是白搭。代码混淆再复杂,字符串明文暴露也是徒劳。我建议你把这一章的内容当作一个 checklist,每做一个新项目,都对照着检查一遍。嗯,这样至少能挡住 90% 的初级攻击者。


公众号:蓝海资料掘金营,微信 deep3321