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

Android Key Store解密密码失败问题求助

Android密码加解密解密失败问题排查

问题描述

正在学习Android加密技术,尝试实现用户密码的加解密功能。加密功能运行正常,但解密功能始终报错。曾尝试复用加密配置进行解密但无效,恳请帮忙排查问题。

CryptoManager类代码

package edu.xfoleks.komodoro.presentation.utils

import android.security.keystore.KeyProperties
import android.security.keystore.KeyProtection
import android.util.Base64
import java.nio.charset.StandardCharsets
import java.security.KeyStore
import java.util.Calendar
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec

class CryptoManager {

    init {
        generateSecretKey()
    }
    private fun generateSecretKey() {

        val keyStore = KeyStore.getInstance(KEY_STORE).apply {
            load(null)
        }

        val keyGenerator = KeyGenerator.getInstance(ALGORITHM).apply {
            init(KEY_SIZE)
        }

        val secretKey = keyGenerator.generateKey()

        val startTime = Calendar.getInstance()
        val expireTime = Calendar.getInstance().apply {
            add(Calendar.YEAR, 2)
        }

        val entry = KeyStore.SecretKeyEntry(secretKey)
        val protectionParameter =
            KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
                .setKeyValidityStart(startTime.time)
                .setKeyValidityEnd(expireTime.time)
                .setBlockModes(BLOCK_MODE)
                .setEncryptionPaddings(PADDING)
                .build()

        keyStore.setEntry(ALIAS, entry, protectionParameter)
    }

    fun encrypt(plainText: String): String {
        val keyStore = KeyStore.getInstance(KEY_STORE).apply { load(null) }

        val secretKey = keyStore.getKey(ALIAS, null)

        return try {
            val cipher = Cipher.getInstance(TRANSFORMATION)
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)

            val cipherText = Base64.encodeToString(cipher.doFinal(plainText.toByteArray()), Base64.DEFAULT)
            val iv = Base64.encodeToString(cipher.iv, Base64.DEFAULT)

            "${cipherText}.$iv"

        } catch (ex: Exception) {
            ex.printStackTrace()
            ""
        }
    }

    fun decrypt(cipherText: String): String? {
        val keyStore = KeyStore.getInstance(KEY_STORE).apply { load(null) }

        val secretKey = keyStore.getKey(ALIAS, null)

        val array = cipherText.split(".")
        val cipherData = Base64.decode(array[0], Base64.DEFAULT)
        val iv = Base64.decode(array[1], Base64.DEFAULT)

        return try {
            val cipher = Cipher.getInstance(TRANSFORMATION)
            val spec = IvParameterSpec(iv)

            cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

            val clearText = cipher.doFinal(cipherData)

            val encoded = String(clearText, 0, clearText.size, StandardCharsets.UTF_8)

            encoded
        } catch (ex: Exception) {
            ex.printStackTrace()
            null
        }
    }

    companion object {
        private const val KEY_STORE = "AndroidKeyStore"
        private const val KEY_SIZE = 256
        private const val ALIAS = "secret"
        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"
    }
}

加解密相关ViewModel代码

LogInViewModel

class LogInViewModel @Inject constructor(
    private val userInteractor: UserInteractor
) : ViewModel() {

    private var _user = MutableLiveData<UserInfoItem>()

    private fun decryptPassword(password: String): String? {
        val cryptoManager = CryptoManager()
        return cryptoManager.decrypt(password)
    }

    fun logIn(userName: String, password: String): Boolean {
        val user = UserInfoItem(userName, password)
        _user.value = user
        val existingUser = userInteractor.getUser(user.toUser()).toUserInfoItem()
        val decryptedPassword = decryptPassword(existingUser.password)

        return user.userName.equals(existingUser.userName) && user.password.equals(decryptedPassword)
    }
}

RegistrationViewModel

class RegistrationViewModel @Inject constructor(
    private val userInteractor: UserInteractor
) : ViewModel() {

    private var _user = MutableLiveData<UserInfoItem>()

    private fun encryptPassword(password: String): String {
        if (password.isEmpty()) {
            throw EmptyPasswordException()
        }

        val cryptoManager = CryptoManager()
        return cryptoManager.encrypt(password)
    }

    fun registerUser(userName: String, password: String) {
        if (userName.isEmpty() || password.isEmpty()) { throw UserNotCreatedException() }
        val encryptedPassword = encryptPassword(password)
        val user = UserInfoItem(userName, encryptedPassword)
        _user.value = user
        userInteractor.registerUser(user.toUser())
    }

}

错误日志(中文翻译)

javax.crypto.BadPaddingException
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:609)
at javax.crypto.Cipher.doFinal(Cipher.java:2056)
at edu.xfoleks.komodoro.presentation.utils.CryptoManager.decrypt(CryptoManager.kt:83)
at edu.xfoleks.komodoro.presentation.screens.viewmodels.LogInViewModel.decryptPassword(LogInViewModel.kt:22)
at edu.xfoleks.komodoro.presentation.screens.viewmodels.LogInViewModel.logIn(LogInViewModel.kt:29)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment.onCreateView$lambda$5(LogInFragment.kt:47)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment.$r8$lambda$NaDtgxEMrBbrOBDtRje0oMjYmGs(Unknown Source:0)
at edu.xfoleks.komodoro.presentation.screens.fragments.LogInFragment$$ExternalSyntheticLambda0.onClick(Unknown Source:2)
at android.view.View.performClick(View.java:7506)
at com.google.android.material.button.MaterialButton.performClick(MaterialButton.java:1211)
at android.view.View.performClickInternal(View.java:7483)
at android.view.View.-$$Nest$mperformClickInternal(Unknown Source:0)
at android.view.View$PerformClick.run(View.java:29334)
at android.os.Handler.handleCallback(Handler.java:942)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
at android.app.ActivityThread.main(ActivityThread.java:7872)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
Caused by: android.security.KeyStoreException: 参数无效(内部密钥库代码:-38 消息:在 KeystoreOperation::finish 中

原因:
0: 在 finish 中:KeyMint::finish 失败。
1: 错误::Km(ErrorCode(-38)))(公开错误代码:10 内部密钥库代码:-38)
at android.security.KeyStore2.getKeyStoreException(KeyStore2.java:369)  
at android.security.KeyStoreOperation.handleExceptions(KeyStoreOperation.java:78)   
at android.security.KeyStoreOperation.finish(KeyStoreOperation.java:128)    
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer$MainDataStream.finish(KeyStoreCryptoOperationChunkedStreamer.java:228)
at android.security.keystore2.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:181)
at android.security.keystore2.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:603)
... 20 more

问题分析与解决方案

核心问题

每次实例化CryptoManager时,init方法都会调用generateSecretKey,而该方法会直接替换AndroidKeyStore中已有别名secret的密钥。也就是说:

  • 注册时加密密码用的是A密钥
  • 登录时解密密码用的是新生成的B密钥
    密钥不匹配导致解密时出现BadPaddingException,同时触发KeyStore的参数无效错误。

解决方案

1. 修改密钥生成逻辑,仅在无密钥时生成

修改generateSecretKey方法,先检查KeyStore中是否存在目标别名的密钥,不存在才生成新密钥:

private fun generateSecretKey() {
    val keyStore = KeyStore.getInstance(KEY_STORE).apply { load(null) }
    
    // 检查是否已有密钥,存在则直接返回
    if (keyStore.containsAlias(ALIAS)) {
        return
    }

    val keyGenerator = KeyGenerator.getInstance(ALGORITHM).apply {
        init(KEY_SIZE)
    }

    val secretKey = keyGenerator.generateKey()

    val startTime = Calendar.getInstance()
    val expireTime = Calendar.getInstance().apply {
        add(Calendar.YEAR, 2)
    }

    val entry = KeyStore.SecretKeyEntry(secretKey)
    val protectionParameter =
        KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setKeyValidityStart(startTime.time)
            .setKeyValidityEnd(expireTime.time)
            .setBlockModes(BLOCK_MODE)
            .setEncryptionPaddings(PADDING)
            .build()

    keyStore.setEntry(ALIAS, entry, protectionParameter)
}

2. 将CryptoManager设计为单例

避免重复实例化导致的重复密钥生成问题,可以将CryptoManager改为单例模式:

class CryptoManager private constructor() {

    init {
        generateSecretKey()
    }

    companion object {
        val instance: CryptoManager by lazy { CryptoManager() }
        // 原companion中的常量保持不变
        private const val KEY_STORE = "AndroidKeyStore"
        private const val KEY_SIZE = 256
        private const val ALIAS = "secret"
        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"
    }

    // 原generateSecretKey、encrypt、decrypt方法保持不变
}

然后在ViewModel中使用单例实例:

// RegistrationViewModel中修改
private fun encryptPassword(password: String): String {
    if (password.isEmpty()) {
        throw EmptyPasswordException()
    }

    return CryptoManager.instance.encrypt(password)
}

// LogInViewModel中修改
private fun decryptPassword(password: String): String? {
    return CryptoManager.instance.decrypt(password)
}

3. 可选:通过依赖注入提供单例

如果项目使用Dagger/Hilt等依赖注入框架,可以将CryptoManager标记为单例并注入到ViewModel中,避免手动管理单例。

验证方案

修改完成后,重新注册用户,然后登录,此时加密和解密使用的是同一密钥,即可正常解密密码。

内容的提问来源于stack exchange,提问作者Oleksandr Filonenko

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 16:54:58