使用Android KeyStore加密DataStore时遇javax.crypto.BadPaddingException
Android Preference DataStore加密解密BadPaddingException排查
问题描述
在为Preference DataStore实现加密功能时,解密阶段抛出javax.crypto.BadPaddingException异常。已确认提取后的密文长度符合AES-CBC块大小要求(16的倍数),但仍触发错误。
实现代码
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import android.util.Log import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.IvParameterSpec class CryptoManager { private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } 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" } 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("myAppSecret", null) as? KeyStore.SecretKeyEntry return existingKey?.secretKey ?: createKey() } private fun createKey(): SecretKey { return KeyGenerator.getInstance(ALGORITHM).apply { init( KeyGenParameterSpec.Builder( "myAppSecret", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(BLOCK_MODE) .setEncryptionPaddings(PADDING) .setUserAuthenticationRequired(false) .setRandomizedEncryptionRequired(true) .build() ) }.generateKey() } private fun getSizeBytes(size: Int): ByteArray { return byteArrayOf( (size shr 24).toByte(), (size shr 16).toByte(), (size shr 8).toByte(), size.toByte() ) } private fun getSizeFromBytes(bytes: ByteArray): Int { return (bytes[0].toInt() shl 24) or ((bytes[1].toInt() and 0xFF) shl 16) or ((bytes[2].toInt() and 0xFF) shl 8) or (bytes[3].toInt() and 0xFF) } fun encrypt(bytes: ByteArray): ByteArray { val encrypted = encryptCipher.doFinal(bytes) val sizeBytes = getSizeBytes(encrypted.size) val iv = encryptCipher.iv Log.d("myDebug", "encrypt iv: ${iv.map { it.toInt().toByte() }.joinToString(" ")}") return Base64.encode(byteArrayOf(encryptCipher.iv.size.toByte()) + iv + sizeBytes + encrypted, Base64.DEFAULT) } fun decrypt(bytes64: ByteArray): ByteArray { // ByteArray should look like IV_SIZE | IV | MESSAGE_SIZE_BYTES | MESSAGE val bytes = Base64.decode(bytes64, Base64.DEFAULT) Log.d("myDebug", "decrypt bytes: ${bytes.map { it.toInt().toByte() }.joinToString(" ")}") // Pointer for moving, and extracting data from ByteArray var pointer = 0 val ivSize = bytes[pointer++].toInt() val iv = bytes.sliceArray(pointer until ivSize + pointer) Log.d("myDebug", "iv: ${iv.map { it.toInt().toByte() }.joinToString(" ")}") pointer += ivSize // size of the message is coded over 4 bytes (int) val messageSize = getSizeFromBytes(bytes.sliceArray(pointer until pointer + 4)) Log.d("myDebug", "messageSize: $messageSize") pointer += 4 val encryptedMessage = bytes.sliceArray(pointer until pointer + messageSize) Log.d("myDebug", "message: ${encryptedMessage.map { it.toInt().toByte() }.joinToString(" ")}") Log.d("myDebug", "extracted message size: ${encryptedMessage.size}") return getDecryptCipherForIv(iv).doFinal(encryptedMessage) } }
异常栈信息
javax.crypto.BadPaddingException at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:624) at javax.crypto.Cipher.doFinal(Cipher.java:2056)
排查与修复方案
1. 验证密钥一致性
BadPaddingException最常见的原因是加密和解密使用了不同的密钥。在getKey()方法中添加日志,确认是否每次解密都重新生成了密钥:
private fun getKey(): SecretKey { val existingKey = keyStore.getEntry("myAppSecret", null) as? KeyStore.SecretKeyEntry Log.d("myDebug", "Existing key exists: ${existingKey != null}") return existingKey?.secretKey ?: createKey().also { Log.d("myDebug", "Created new secret key") } }
- 如果日志频繁打印
Created new secret key,说明密钥未被正确持久化到AndroidKeyStore。检查是否有应用数据清除、密钥权限配置错误等情况。 - 注意:AndroidKeyStore中的密钥与应用UID绑定,卸载重装应用后密钥会丢失,此前加密的数据无法解密。
2. 校验IV一致性
对比加密阶段和解密阶段的IV日志(encrypt iv和iv日志),确认两者完全一致:
- 如果IV不一致,说明加密后的字节数组在存储/传输过程中被篡改,或拼接/截取逻辑有误。
- AES-CBC的IV长度固定为16字节,若日志中
ivSize不是16,需检查encryptCipher.iv.size是否异常。
3. 修复Base64编码/解码问题
Base64.DEFAULT会自动添加换行符,可能导致存储时字节丢失或篡改。改用Base64.NO_WRAP避免换行,并直接使用字符串存储加密结果:
// 修改encrypt方法返回String fun encrypt(bytes: ByteArray): String { val encrypted = encryptCipher.doFinal(bytes) val sizeBytes = getSizeBytes(encrypted.size) val iv = encryptCipher.iv Log.d("myDebug", "encrypt iv: ${iv.joinToString(" ")}") val combined = byteArrayOf(iv.size.toByte()) + iv + sizeBytes + encrypted return Base64.encodeToString(combined, Base64.NO_WRAP) } // 修改decrypt方法接收String参数 fun decrypt(encryptedString: String): ByteArray { val bytes = Base64.decode(encryptedString, Base64.NO_WRAP) Log.d("myDebug", "decrypt bytes: ${bytes.joinToString(" ")}") var pointer = 0 val ivSize = bytes[pointer++].toInt() val iv = bytes.sliceArray(pointer until pointer + ivSize) Log.d("myDebug", "iv: ${iv.joinToString(" ")}") pointer += ivSize val messageSize = getSizeFromBytes(bytes.sliceArray(pointer until pointer + 4)) Log.d("myDebug", "messageSize: $messageSize") pointer += 4 val encryptedMessage = bytes.sliceArray(pointer until pointer + messageSize) Log.d("myDebug", "extracted message size: ${encryptedMessage.size}") return getDecryptCipherForIv(iv).doFinal(encryptedMessage) }
4. 确认密文截取正确性
对比加密时encrypted.size、解密时messageSize和encryptedMessage.size三个值:
- 若三者不一致,说明拼接或截取逻辑存在错误,需检查指针计算是否正确。
5. 排查密文篡改
若上述步骤均无问题,需确认加密数据在存储过程中是否被篡改(如字节数变化、额外字符插入)。可打印加密后的完整Base64字符串,与解密前接收的字符串对比,确保完全一致。
内容的提问来源于stack exchange,提问作者bmach01
相关产品推荐
相关产品推荐

