Kotlin字符串加密扩展函数实现咨询:代码检查与完善
你的AES加密实现存在的问题及完整优化方案
先帮你梳理下当前代码里的几个关键问题,这些问题不仅会导致解密失败,还可能带来安全隐患:
- SHA1PRNG兼容性问题:
SecureRandom.getInstance("SHA1PRNG")不是Java标准算法,不同平台(比如Android和Oracle JDK)的实现逻辑差异很大,用同一个seed生成的密钥在不同平台可能不一样,直接导致跨平台解密失败。 - CBC模式缺失IV:你用的
AES/CBC/PKCS5Padding必须搭配随机初始化向量(IV)使用,IV需要和密文一起保存,解密时必须用同一个IV。你的代码完全没处理IV,这会直接导致解密失败,还容易引发安全漏洞。 - 密钥生成不安全:直接把seed作为SecureRandom的种子生成AES密钥,相当于把seed直接当作密钥材料。如果seed是用户输入的短密码,熵值极低,很容易被暴力破解。应该用专业的密钥派生算法(比如PBKDF2)来从seed生成安全的AES密钥。
- 代码未完成:
cipher.init(Cipher.ENCRY...没写完,也没处理IV和密文的组合存储,根本无法正常工作。
完整的Kotlin AES加密解密实现
下面是一套符合安全规范的完整实现,包含加密、解密函数,解决了上面所有问题:
import javax.crypto.Cipher import javax.crypto.SecretKeyFactory import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.SecretKeySpec import java.security.SecureRandom import java.util.Base64 // 可根据需求调整的常量 private const val AES_KEY_SIZE = 128 // 256位需JCE无限制权限,Android 7.0+默认支持 private const val PBKDF2_ITERATIONS = 65536 // 迭代次数越高,破解难度越大 private const val SALT_LENGTH = 16 // 随机盐的长度 private const val IV_LENGTH = 16 // AES-CBC的IV固定为16字节 private const val DELIMITER = ":" // 用于分隔盐、IV、密文的字符 /** * 字符串AES加密 * @param seed 加密密钥(建议用用户输入的强密码) * @return 包含盐、IV、密文的Base64字符串 */ fun String.encryptAES(seed: String): String { // 生成随机盐 val salt = ByteArray(SALT_LENGTH).apply { SecureRandom.getInstanceStrong().nextBytes(this) } // 用PBKDF2从seed派生AES密钥 val keySpec = PBEKeySpec(seed.toCharArray(), salt, PBKDF2_ITERATIONS, AES_KEY_SIZE) val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") val rawKey = keyFactory.generateSecret(keySpec).encoded val secretKey = SecretKeySpec(rawKey, "AES") // 生成随机IV val iv = ByteArray(IV_LENGTH).apply { SecureRandom.getInstanceStrong().nextBytes(this) } val ivSpec = IvParameterSpec(iv) // 初始化加密器 val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec) val encryptedBytes = cipher.doFinal(this.toByteArray(Charsets.UTF_8)) // 将盐、IV、密文转成Base64并拼接,方便存储/传输 val saltBase64 = Base64.getEncoder().encodeToString(salt) val ivBase64 = Base64.getEncoder().encodeToString(iv) val encryptedBase64 = Base64.getEncoder().encodeToString(encryptedBytes) return "$saltBase64$DELIMITER$ivBase64$DELIMITER$encryptedBase64" } /** * AES解密字符串 * @param seed 解密密钥(需和加密时一致) * @return 解密后的原始字符串 * @throws IllegalArgumentException 加密字符串格式错误 * @throws Exception 解密失败(如密钥错误、密文被篡改等) */ fun String.decryptAES(seed: String): String { // 拆分盐、IV、密文 val parts = this.split(DELIMITER) require(parts.size == 3) { "加密字符串格式无效" } val salt = Base64.getDecoder().decode(parts[0]) val iv = Base64.getDecoder().decode(parts[1]) val encryptedBytes = Base64.getDecoder().decode(parts[2]) // 派生和加密时相同的密钥 val keySpec = PBEKeySpec(seed.toCharArray(), salt, PBKDF2_ITERATIONS, AES_KEY_SIZE) val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") val rawKey = keyFactory.generateSecret(keySpec).encoded val secretKey = SecretKeySpec(rawKey, "AES") // 初始化解密器 val ivSpec = IvParameterSpec(iv) val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) val decryptedBytes = cipher.doFinal(encryptedBytes) return String(decryptedBytes, Charsets.UTF_8) }
关键细节说明
- PBKDF2密钥派生:用
PBKDF2WithHmacSHA256算法结合随机盐和高迭代次数,把用户输入的seed转换成安全的AES密钥。即使seed是短密码,也能大幅提升破解难度。 - 随机IV:每次加密都生成新的随机IV,IV不需要保密,但必须和密文一起保存。这样相同的明文每次加密都会得到不同的密文,避免被攻击者通过模式识别破解。
- Base64编码:把二进制的盐、IV、密文转换成字符串,方便存储在数据库或传输。用分隔符
:把三者分开,解密时能准确拆分。 - 异常处理:示例中用
require检查加密字符串格式,实际项目中可以添加try-catch块处理NoSuchAlgorithmException、BadPaddingException等异常(比如密钥错误时会抛出BadPaddingException,可以据此提示用户密码错误)。
使用注意事项
- 如果要使用256位AES密钥,JVM环境需要安装Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files,Android 7.0及以上版本默认支持。
- seed建议使用强密码(长度≥12位,包含大小写字母、数字、特殊字符),不要用简单的字符串作为seed。
- 不要硬编码盐、IV或密钥,必须每次加密生成随机盐和IV。
- 解密时如果出现异常,大概率是密钥错误或密文被篡改,要合理提示用户,不要泄露具体异常信息给攻击者。
内容的提问来源于stack exchange,提问作者MarcForn
相关产品推荐
相关产品推荐

