使用Keystore解密时遇IllegalBlockSizeException及BadPaddingException问题
问题分析与修复方案
问题背景
我正在构建一个加密解密自定义User类的工具类,需求是加密User类的所有字段并以字符串形式存储,解密时还原字段。目前遇到两个问题:
- 将字节数组解码为字符串再编码回字节数组后,块大小与原16字节不符,触发
IllegalBlockSizeException - 即使块大小为16的倍数,仍触发
BadPaddingException,纯字节数组操作时也出现相同错误
核心错误原因
- 二进制转字符串编码错误:加密后的字节数组(包含IV和密文)属于二进制数据,直接用
decodeToString()和encodeToByteArray()做UTF-8编码转换会丢失数据——并非所有二进制值都是合法UTF-8字符,转回的字节数组与原数据不一致,必然导致解密失败。 - IV长度获取逻辑错误:解密时通过
encryptCipher.iv.size获取IV长度,但encryptCipher是get属性,每次调用都会创建新Cipher实例并生成新IV,这会导致解密时读取的IV长度与加密时实际长度不匹配(虽AES CBC的IV固定16字节,但写法逻辑错误)。
修复后的代码
CryptoManager类
package com.plcoding.androidcrypto import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import androidx.annotation.RequiresApi import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec @RequiresApi(Build.VERSION_CODES.M) class CryptoManager { private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } private val encryptCipher get() = Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.ENCRYPT_MODE, getKey()) } private fun getDecryptCipherForIv(iv: ByteArray): Cipher { return Cipher.getInstance(TRANSFORMATION).apply { init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv)) } } private fun getKey(): SecretKey { val existingKey = keyStore.getEntry("secret", null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createKey() } private fun createKey(): SecretKey { return KeyGenerator.getInstance(ALGORITHM).apply { init( KeyGenParameterSpec.Builder( "secret", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(BLOCK_MODE) .setEncryptionPaddings(PADDING) .setUserAuthenticationRequired(false) .setRandomizedEncryptionRequired(true) .build() ) }.generateKey() } private fun encrypt(input: ByteArray): String { val encryptedBytes = encryptCipher.doFinal(input) // 将IV和密文拼接后转Base64字符串 val combined = ByteArray(encryptCipher.iv.size + encryptedBytes.size) System.arraycopy(encryptCipher.iv, 0, combined, 0, encryptCipher.iv.size) System.arraycopy(encryptedBytes, 0, combined, encryptCipher.iv.size, encryptedBytes.size) return Base64.encodeToString(combined, Base64.DEFAULT) } private fun decrypt(input: String): String { // 先将Base64字符串转字节数组 val combined = Base64.decode(input, Base64.DEFAULT) // AES CBC的IV固定为16字节 val iv = ByteArray(16) val encryptedBytes = ByteArray(combined.size - iv.size) System.arraycopy(combined, 0, iv, 0, iv.size) System.arraycopy(combined, iv.size, encryptedBytes, 0, encryptedBytes.size) return getDecryptCipherForIv(iv).doFinal(encryptedBytes).decodeToString() } fun encryptUser(user: User): User { val encryptedName = user.name?.encodeToByteArray()?.let { encrypt(it) } val encryptedApp = user.app?.encodeToByteArray()?.let { encrypt(it) } val encryptedPassword = user.password?.encodeToByteArray()?.let { encrypt(it) } val encryptedNotes = user.notes?.encodeToByteArray()?.let { encrypt(it) } return User(encryptedName, encryptedApp, encryptedPassword, encryptedNotes) } fun decryptUser(user: User?): User { val decryptedName = user?.name?.let { decrypt(it) } val decryptedApp = user?.app?.let { decrypt(it) } val decryptedPassword = user?.password?.let { decrypt(it) } val decryptedNotes = user?.notes?.let { decrypt(it) } return User(decryptedName, decryptedApp, decryptedPassword, decryptedNotes) } companion object { private const val ALGORITHM = KeyProperties.KEY_ALGORITHM_AES private const val BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC private const val PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7 private const val TRANSFORMATION = "$ALGORITHM/$BLOCK_MODE/$PADDING" } }
User类
class User( val name: String? = null, val app: String? = null, val password: String? = null, var notes: String? = null, val id: String? = null, )
关键修复点
- 替换UTF-8编码为Base64:加密后的二进制数据用
Base64.encodeToString()转为安全字符串,解密时用Base64.decode()还原字节数组,确保数据完整无丢失。 - 固定IV长度:AES CBC模式下IV长度固定为16字节,解密时直接使用该长度拆分IV和密文,避免依赖动态生成的Cipher实例的IV长度。
- 简化解密方法参数:直接接收加密后的Base64字符串,不需要手动转字节数组,减少出错环节。
内容的提问来源于stack exchange,提问作者Raz tamim
相关产品推荐
相关产品推荐

