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

使用生物识别API在Android Keystore存储密码的位置疑问

Android Keystore密码存储位置疑问

我希望将密码存储到Android Keystore中,但不清楚加密后的密码实际存储的位置,想确认是应该存在SharedPreference还是File中?

以下是我目前尝试的代码:

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class MainActivity : AppCompatActivity() {

    private val KEY_ALIAS = "my_secure_key"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val keyStore = KeyStore.getInstance("AndroidKeyStore")
        keyStore.load(null)

        val secretKey = if (keyStore.containsAlias(KEY_ALIAS)) {
            keyStore.getKey(KEY_ALIAS, null) as SecretKey
        } else {
            val keyGenerator = KeyGenerator.getInstance(
                KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
            )

            val keyGenParameterSpec = KeyGenParameterSpec.Builder(
                KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
            )
                .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                .setUserAuthenticationRequired(true) // Requires biometric authentication
                .build()

            keyGenerator.init(keyGenParameterSpec)
            keyGenerator.generateKey()
        }

        val biometricPrompt = BiometricPrompt(
            this,
            ContextCompat.getMainExecutor(this),
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    super.onAuthenticationSucceeded(result)
                    val cipher = result.cryptoObject?.cipher
                    if (cipher != null) {
                        val encryptedUsername = cipher.doFinal("my_username".toByteArray())
                        val encryptedPassword = cipher.doFinal("my_password".toByteArray())

                        // Store encryptedUsername and encryptedPassword as needed
                    }
                }

                override fun onAuthenticationError(
                    errorCode: Int, errString: CharSequence
                ) {
                    super.onAuthenticationError(errorCode, errString)
                    // Handle authentication errors
                }

                override fun onAuthenticationFailed() {
                    super.onAuthenticationFailed()
                    // Handle authentication failures
                }
            }
        )

        val cipher = Cipher.getInstance(
            KeyProperties.KEY_ALGORITHM_AES + "/"
                    + KeyProperties.BLOCK_MODE_GCM + "/"
                    + KeyProperties.ENCRYPTION_PADDING_NONE
        )

        val secretKeyEntry = keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry
        val secretKey = secretKeyEntry.secretKey

        val gcmSpec = GCMParameterSpec.Builder()
            .setKeySize(256)
            .setNonce(ByteArray(12)) // You need to generate a random nonce for each encryption
            .build()

        cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec)

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Authenticate to access credentials")
            .setSubtitle("Using your biometric")
            .setDescription("Authenticate using your fingerprint to access your credentials.")
            .setNegativeButtonText("Cancel")
            .build()

        biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
    }
}

核心解答

1. Android Keystore的本质作用

Android Keystore不存储你的明文或加密后密码,它只负责安全托管你生成的AES加密密钥(代码中的my_secure_key)。这个密钥由系统底层保护,你的App无法直接导出明文密钥,只能通过授权的Cipher操作调用它,从根源降低密钥泄露风险。

2. 加密后密码的存储选择

加密后的用户名/密码需要你自行选择存储介质,SharedPreference或File都可行:

  • SharedPreference:适合存储小体积敏感数据,操作便捷。推荐使用EncryptedSharedPreferences(AndroidX官方封装),它会自动用Keystore中的密钥加密存储内容,无需手动处理Cipher逻辑,安全性更高。
  • File:适合存储较大数据,建议将加密后的字节数组写入App私有目录(getFilesDir()或getNoBackupFilesDir()),绝对不要存到外部存储(防止被其他App或用户直接访问)。

3. 代码优化关键要点

  • 随机Nonce必须存储:GCM模式要求每次加密使用唯一的12字节随机Nonce,你代码中用空字节数组是严重安全隐患。需要生成随机Nonce,并且和加密数据一起存储(解密时必须使用同一个Nonce)。
  • 避免重复获取密钥:代码中两次获取secretKey,可以合并为一次操作,减少冗余。
  • 存储逻辑示例:在认证成功后添加SharedPreference存储代码:
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
    super.onAuthenticationSucceeded(result)
    val cipher = result.cryptoObject?.cipher
    if (cipher != null) {
        // 获取Cipher使用的随机Nonce
        val nonce = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java).nonce
        val encryptedUsername = cipher.doFinal("my_username".toByteArray())
        val encryptedPassword = cipher.doFinal("my_password".toByteArray())

        // 存储加密数据和Nonce到私有SharedPreference
        val securePref = getSharedPreferences("SecureCreds", MODE_PRIVATE)
        securePref.edit()
            .putByteArray("encrypted_user", encryptedUsername)
            .putByteArray("encrypted_pwd", encryptedPassword)
            .putByteArray("gcm_nonce", nonce)
            .apply()
    }
}

内容的提问来源于stack exchange,提问作者Abhijit Chakra

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 06:36:39