加密与密钥管理:对称加密、非对称加密、哈希算法、密钥存储安全、随机数生成

各位同学,今天我们来聊聊加密与密钥管理。说实话,这是Android安全开发里最容易被忽视,但又最容易出大问题的一块。我见过太多应用,功能做得花里胡哨,结果密钥硬编码在代码里,加密算法用错了模式,随机数生成器用了不安全的种子……嗯,这些坑我基本都踩过。

你想想看,一个App如果连数据加密都做不好,那用户数据基本就是裸奔。今天我就把对称加密、非对称加密、哈希算法、密钥存储和随机数生成这五个核心知识点,结合我自己的实战经验,给你讲透。

一、对称加密:又快又简单,但密钥怎么传?

对称加密,说白了就是加密和解密用同一把钥匙。最常见的算法是AES。我个人习惯用AES-GCM模式,因为它不仅加密,还能校验数据完整性,一步到位。

核心要点:Android中推荐使用AES/GCM/NoPadding,不要用ECB模式。ECB模式有严重的安全问题,相同明文会生成相同密文,这很危险。

// 正确的AES-GCM加密示例
fun encryptAesGcm(plainText: ByteArray, key: SecretKey): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val iv = ByteArray(12) // GCM推荐12字节IV
    SecureRandom().nextBytes(iv)
    val spec = GCMParameterSpec(128, iv)
    cipher.init(Cipher.ENCRYPT_MODE, key, spec)
    val cipherText = cipher.doFinal(plainText)
    // 返回 IV + 密文,解密时需要IV
    return iv + cipherText
}

我在项目中遇到过一个问题:有个同事用了AES/CBC/PKCS5Padding,但IV每次都写死成0。结果呢?同样的数据加密出来一模一样,攻击者一看就知道哪些数据是重复的。避坑指南:IV必须随机生成,而且每次加密都要不同。

警告:千万不要在代码里硬编码密钥!我曾经在一个金融类App里发现密钥直接写在strings.xml里,这等于把家门钥匙挂在门口。

二、非对称加密:公钥加密,私钥解密

非对称加密解决了对称加密的密钥分发问题。最常用的是RSA。但RSA有个缺点——慢。所以实际项目中,我们通常用「混合加密」:用RSA加密AES密钥,再用AES加密实际数据。

为什么会这样?因为RSA加密长度有限制,1024位密钥最多只能加密117字节数据。你想想看,如果直接加密一张图片,那得切成多少块?

// RSA加密AES密钥的典型做法
fun encryptAesKeyWithRsa(aesKey: SecretKey, publicKey: PublicKey): ByteArray {
    val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding")
    cipher.init(Cipher.ENCRYPT_MODE, publicKey)
    return cipher.doFinal(aesKey.encoded)
}

我记得有一次做支付SDK,对方要求用RSA加密敏感信息。我建议用混合加密方案,但对方坚持直接用RSA加密整个请求体。结果呢?数据稍微大一点就报错,最后还得改回来。所以,设计阶段就要想清楚数据量的问题。

三、哈希算法:不是加密,是摘要

哈希算法很多人误以为是加密,其实不是。它是单向的,不可逆。常用的是SHA-256。哈希主要用于校验数据完整性、存储密码(配合盐值)。

提示:存储密码时,千万不要直接用SHA-256。一定要加盐(salt),而且每个用户的盐值要不同。推荐用bcrypt、scrypt或Argon2这类专门为密码设计的算法。

// 正确的密码哈希存储方式
fun hashPassword(password: String, salt: ByteArray): ByteArray {
    val spec = PBEKeySpec(password.toCharArray(), salt, 10000, 256)
    val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
    return factory.generateSecret(spec).encoded
}

我曾经审计过一个App,它用MD5哈希用户密码,而且没加盐。MD5啊,现在用GPU跑MD5,每秒能算几十亿次,这种密码跟明文存储没啥区别。避坑指南:密码哈希一定要用慢速算法,迭代次数至少10000次以上。

四、密钥存储安全:Android Keystore是底线

密钥存哪儿?这是个大问题。Android提供了Keystore系统,密钥可以存储在硬件安全模块(TEE/SE)里,应用进程根本拿不到明文密钥。

我个人习惯:所有敏感密钥都放Keystore。包括AES密钥、RSA私钥。Keystore生成的密钥可以设置用途,比如只用于加密,不能导出。这样就算App被逆向,攻击者也拿不到密钥。

// 在Android Keystore中生成AES密钥
fun generateAesKeyInKeystore(alias: String) {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
    )
    val spec = KeyGenParameterSpec.Builder(
        alias,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setKeySize(256)
        .build()
    keyGenerator.init(spec)
    keyGenerator.generateKey()
}

警告:SharedPreferences存储密钥?绝对不行!我见过有人把密钥用Base64编码后存到SharedPreferences里,这跟写在脸上没区别。Keystore是唯一推荐的方式。

五、随机数生成:SecureRandom是唯一选择

随机数看似简单,但坑很多。Java的Random类不是密码学安全的,生成的随机数可以被预测。一定要用SecureRandom。

为什么会这样?因为Random基于线性同余算法,给定前几个数就能推算出后续所有数。而SecureRandom会从系统熵源(如硬件噪声、传感器数据)获取种子,不可预测。

// 正确的随机数生成方式
fun generateSecureRandomBytes(length: Int): ByteArray {
    val random = SecureRandom()
    val bytes = ByteArray(length)
    random.nextBytes(bytes)
    return bytes
}

我记得有一次做Token生成,同事用了Random.nextInt()来生成会话ID。结果呢?测试环境没问题,但上线后攻击者通过分析几个Token就预测出了后续所有Token,直接导致会话劫持。从那以后,我要求所有随机数相关代码必须用SecureRandom。

提示:Android 4.2以上版本,SecureRandom默认已经使用/dev/urandom作为熵源,安全性足够。但如果你需要生成高强度密钥,建议调用SecureRandom.getInstanceStrong(),它会使用硬件随机数生成器。

知识体系总览

下面这张图总结了加密与密钥管理的核心知识点和它们之间的关系。你可以看到,从随机数生成开始,到密钥存储,再到各种加密算法的应用,是一条完整的安全链路。

加密与密钥管理知识体系 密钥管理 对称加密 (AES-GCM) 非对称加密 (RSA-OAEP) 哈希算法 (SHA-256) 密钥存储 (Keystore) 随机数生成 (SecureRandom) 混合加密方案 核心原则:密钥永不离开Keystore,随机数必须安全,算法选择要谨慎 混合加密 = 非对称加密传输密钥 + 对称加密处理数据

总结一下

加密与密钥管理,说白了就是三件事:选对算法、管好密钥、用好随机数。对称加密用AES-GCM,非对称加密用RSA-OAEP,哈希用SHA-256加盐,密钥存Keystore,随机数用SecureRandom。这五个点做到位,你的App在数据安全这块就及格了。

嗯,今天就到这里。这些内容看起来多,但实际用起来就是几行代码的事。关键是要养成习惯,每次写加密相关代码时,多问自己一句:密钥安全吗?随机数安全吗?算法对吗?


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