首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS但KeyPairGenerator.initialize() => java.lang.IllegalStateException on API 29

BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS但KeyPairGenerator.initialize() => java.lang.IllegalStateException on API 29
EN

Stack Overflow用户
提问于 2022-10-06 10:50:37
回答 1查看 216关注 0票数 2

很抱歉有这么长的问题。我试着把所有相关的信息都包括进去,这是相当多的。我已经在这个问题上工作了几个星期了,我非常需要帮助。

一般信息

我正在开发一个颤振应用程序,它需要对某些功能使用CryptoObject进行身份验证。这意味着对于Android来说,setUserAuthenticationRequired(true)需要设置在用于创建密钥的KeyGenParameterSpec上。在Android >=30上,这很好,我可以使用指纹或设备凭据(PIN、模式、密码)来验证自己。

问题所在

问题是,我不能让setUserAuthenticationRequired(true)的生物特征处理API 29的仿真器,即使它们设置了指纹。我无法用更低的API在仿真器上进行测试,所以我不知道这样做是否有效。

按下面的方式调用BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS将返回true。从Build.VERSION_CODES.R = API 30开始运行其他情况。根据文档 of BiometricPrompt.authenticate(),API <30的设备只允许BIOMETRIC_STRONG

代码语言:javascript
复制
fun canAuthenticate(context: Context): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            BiometricManager.from(context)
                .canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
        } else {
            BiometricManager.from(context)
                .canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS // <----- this returns true!
        }
    }

但是,即使在模拟器和BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS中注册了指纹,调用keyPairGenerator.initialize()也会抛出java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use

这是代码(restricted是真的,所以setUserAuthenticationRequired(true)被设置了):

代码语言:javascript
复制
private fun initializeKeyPairGenerator(withStrongBox: Boolean = true): KeyPairGenerator {
    val keyPairGenerator = KeyPairGenerator.getInstance(keyGenAlgorithm, provider)

    try {
        val parameterSpec = createParameterSpec(withStrongBox)
        keyPairGenerator.initialize(parameterSpec) // <-------- It throws the exception here
    } catch (e: Exception) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && e is StrongBoxUnavailableException) {
            val parameterSpec = createParameterSpec(false)
            keyPairGenerator.initialize(parameterSpec)
        } else {
            throw Exception("Cannot create key", e)
        }
    }

    return keyPairGenerator
}

private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
    val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
    return KeyGenParameterSpec.Builder(alias, purposes)
        .run {
            setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
            setDigests(KeyProperties.DIGEST_SHA256)
            setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
            setBlockModes(encryptionBlockMode)
            setEncryptionPaddings(encryptionPadding)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                setIsStrongBoxBacked(withStrongBox)
            }

            if (restricted) {
                setUserAuthenticationRequired(true)
            }
            build()
        }
}

这个问题似乎与这个问题非常相关,https://issuetracker.google.com/issues/147374428

我尝试过的一些东西和一种用两个生物识别提示来让它工作的丑陋方法

setUserAuthenticationValidityDurationSeconds(10)上设置KeyGenParameterSpec使keyPairGenerator.initialize()不会抛出异常。

代码语言:javascript
复制
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
    val purposes = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
    return KeyGenParameterSpec.Builder(alias, purposes)
        .run {
            setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
            setDigests(KeyProperties.DIGEST_SHA256)
            setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
            setBlockModes(encryptionBlockMode)
            setEncryptionPaddings(encryptionPadding)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                setIsStrongBoxBacked(withStrongBox)
            }

            if (restricted) {
                setUserAuthenticationRequired(true)

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    setUserAuthenticationParameters(
                        0 /* duration */,
                        KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
                    )
                }
                else { // API <= Q
                    // parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
                    // parameter "-1" default to AUTH_BIOMETRIC_STRONG
                    // source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
                    setUserAuthenticationValidityDurationSeconds(10) // <-- Allow device credential authentication
                }
            }

            build()
        }
}

但是,它在调用initSign(privateKey)时抛出以下异常:((PlatformException(SIGNING_FAILED, User not authenticated, android.security.keystore.UserNotAuthenticatedException: User not authenticated, null))。

以下是代码:

代码语言:javascript
复制
val signature: Signature
    get() = Signature.getInstance(signAlgorithm)
        .apply {
            val privateKey = asymmetricKeyPair.privateKey
            initSign(privateKey) <--- Throws an exception 
        }

此行为与setUserAuthenticationValidityDurationSeconds()文档相匹配。

涉及授权在成功的用户身份验证事件之后使用一段时间的密钥的密码操作只能使用安全的锁定屏幕身份验证。如果需要对用户进行身份验证,这些加密操作将在初始化期间抛出UserNotAuthenticatedException。

这些文件还包括:

这种情况可以通过用户解锁Android的安全锁定屏幕或通过查看由KeyguardManager.createConfirmDeviceCredentialIntent(CharSequence,CharSequence发起的确认凭证流来解决。一旦解决,使用此密钥(或任何其他授权在用户身份验证后的固定时间内使用的密钥)初始化新的加密操作应该成功,只要用户身份验证流成功完成。

按照这些说明显示生物识别提示并在执行initSign(privateKey)之前监听结果,如果用户通过指纹在提示中验证自己,则initSign(privateKey)不会抛出异常。

守则:

代码语言:javascript
复制
private fun triggerBiometricPrompt() {
    val bio = BiometricAuthenticator()
    val intent = bio.createConfirmDeviceCredentialIntent(activity)
    activity.startActivityForResult(intent, 0)
}

在FlutterFragmentActivity()类中

代码语言:javascript
复制
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (IdNowMethodCallHandler.handler.onActivityResult(requestCode, resultCode, data)) {
        return
    }

    if (resultCode == Activity.RESULT_OK) {
        handler.signWithRestrictedKey(handler.methodCall, handler.methodResult) // <-- The result gets handled here
    }

    super.onActivityResult(requestCode, resultCode, data)
}

但是,这意味着用户需要对自己进行两次身份验证,因为在调用BiometricPrompt.authenticate()时当然会显示第二个提示。

守则:

代码语言:javascript
复制
private fun authenticate(
    activity: FragmentActivity,
    promptInfo: BiometricPrompt.PromptInfo = createPromptInfo(),
    signature: Signature?,
    onError: (Int, CharSequence) -> Unit,
    onSuccess: (BiometricPrompt.AuthenticationResult) -> Unit,
) {
    val callback = object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) = onError(errorCode, errString)

        override fun onAuthenticationFailed() {
            // Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as belonging to the user.
            // We want to omit it because the fingerprint maybe just failed to be read in which case the user retries.
            // Also, after multiple attempts, the user can use credentials instead.
        }

        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = onSuccess(result)
    }
    val executor = ContextCompat.getMainExecutor(activity)
    val prompt = BiometricPrompt(activity, executor, callback)

    if (signature == null) {
        prompt.authenticate(promptInfo) // TODO: We never do this since signature is never null.
    } else {
        prompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(signature)) // <-- Another prompt is shown here to authenticate 
    }
}

fun createPromptInfo(
    title: String = "Authorize", 
    subtitle: String = "Please, authorise yourself", 
    description: String = "This is needed to perform cryptographic operations.", 
): BiometricPrompt.PromptInfo {
    val builder = BiometricPrompt.PromptInfo.Builder()
        .setTitle(title)
        .setSubtitle(subtitle)
        .setDescription(description)
        .setConfirmationRequired(true)

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        builder.apply {
            setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        }
    } else {
        builder.setNegativeButtonText("Cancel")
    }

    return builder.build()
}

当然,需要用户连续两次使用生物识别技术进行身份验证是非常糟糕的用户体验。如果用户在第一个提示符中使用设备凭据进行身份验证,它甚至无法工作,而且我也没有找到隐藏该选项的方法。

问题

  1. 为什么KeyPairGenerator.initialize()会将异常java.lang.IllegalStateException: At least one biometric must be enrolled to create keys requiring user authentication for every use抛到带有API 29的仿真器上,并且设置了指纹,即使是BiometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS?这仅仅是Android系统中的一个bug吗?
  2. 是否有一种方法可以使setUserAuthenticationRequired(true) (基于密码的身份验证)在API 29 (或API <30)上工作?

我对能得到的任何帮助深表感谢。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-10-19 15:09:32

多亏了https://www.iedigital.com/resources/archive/create-rsa-key-pair-on-android/,终于找到了解决方案。

基本上,对于API <30,诀窍是使用keyGuardManager.createConfirmDeviceCredentialIntent()而不是使用BiometricPrompt.authenticate()。本文解释得最好,但下面是一些代码的基本步骤:

  1. 创建密钥时执行setUserAuthenticationValidityDurationSeconds(0)
代码语言:javascript
复制
private fun createParameterSpec(withStrongBox: Boolean): KeyGenParameterSpec {
    val purposes = KeyProperties.PURPOSE_SIGN
    return KeyGenParameterSpec.Builder(alias, purposes)
        .run {
            setAlgorithmParameterSpec(ECGenParameterSpec(ecCurveName))
            setDigests(KeyProperties.DIGEST_SHA256)
            setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PSS)
            setBlockModes(encryptionBlockMode)
            setEncryptionPaddings(encryptionPadding)

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                setIsStrongBoxBacked(withStrongBox)
            }

            if (restricted) {
                setUserAuthenticationRequired(true)

                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    setUserAuthenticationParameters(
                        0 /* duration */,
                        KeyProperties.AUTH_BIOMETRIC_STRONG or KeyProperties.AUTH_DEVICE_CREDENTIAL
                    )
                }
                else { // API <= Q
                    // parameter "0" defaults to AUTH_BIOMETRIC_STRONG | AUTH_DEVICE_CREDENTIAL
                    // parameter "-1" default to AUTH_BIOMETRIC_STRONG
                    // source: https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/keystore/java/android/security/keystore/KeyGenParameterSpec.java;l=1236-1246;drc=a811787a9642e6a9e563f2b7dfb15b5ae27ebe98
                    setUserAuthenticationValidityDurationSeconds(0)
                }
            }

            build()
        }
}
  1. 使用不推荐的keyGuardManager.createConfirmDeviceCredentialIntent()来显示提示符,并让用户验证自己。
代码语言:javascript
复制
fun createConfirmDeviceCredentialIntent(context: Context): Intent {
    val keyGuardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager

    return keyGuardManager.createConfirmDeviceCredentialIntent(
        "Authorize", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
        "Please, authorise yourself", // TODO: Add and use Phrase string https://jimplan.atlassian.net/browse/FS-946
    )
}
  1. 在数据上签名
代码语言:javascript
复制
fun sign(signature: Signature, data: ByteArray): ByteArray {
    val signedData = signature.run {
        update(data)
        sign()
    }
    return signedData
}

令人讨厌的是,并不是所有的API都使用BiometricPrompt.authenticate()。我希望这在文件中更清楚!

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/73972579

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档