You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 12:12:02