java.lang.IllegalArgumentException:加密不支持设备凭据
java.lang.IllegalArgumentException: Device credential not supported with crypto
我正在尝试设置 BiometricPrompt,但我需要使用 CryptoObject 进行身份验证,当 https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder.html#setDeviceCredentialAllowed(boolean) 设置为 true 时,这似乎是不可能的。
try {
KeyGeneratorUtil.generateKeyPair("1", null);
} catch (Exception e) {
e.printStackTrace();
}
PrivateKey privateKey;
try {
privateKey = KeyGeneratorUtil.getPrivateKeyReference("test");
} catch (Exception e) {
return;
}
final Signature signature;
try {
signature = initSignature(privateKey);
} catch (Exception e) {
return;
}
final BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);
final BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(context)
.setTitle("Title")
.setDescription("Description")
.setDeviceCredentialAllowed(true)
.build();
...
biometricPrompt.authenticate(cryptoObject, new CancellationSignal(), executor, callback);
当我 运行 这样做时,我得到以下异常。
2019-07-03 13:50:45.140 13715-13715/kcvetano.com.biometricpromptpoc E/AndroidRuntime: FATAL EXCEPTION: main
Process: kcvetano.com.biometricpromptpoc, PID: 13715
java.lang.IllegalArgumentException: Device credential not supported with crypto
at android.hardware.biometrics.BiometricPrompt.authenticate(BiometricPrompt.java:556)
at kcvetano.com.biometricpromptpoc.BiometryAPI29.handleBiometry(BiometryAPI29.java:65)
at kcvetano.com.biometricpromptpoc.MainActivity.onClick(MainActivity.java:56)
at android.view.View.performClick(View.java:7251)
at android.view.View.performClickInternal(View.java:7228)
at android.view.View.access00(View.java:802)
at android.view.View$PerformClick.run(View.java:27843)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7116)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:925)
这应该可以解决问题:
biometricPrompt.authenticate(null, new CancellationSignal(), executor, callback);
错误消息中或多或少地写着(可以说隐藏):当使用setDeviceCredentialAllowed(true)
时,不要使用加密对象。
这一切都分解为如何配置 CryptoObject
中用于加密操作的私钥。
我假设您用于初始化签名对象的私钥是使用 setUserAuthenticationRequired(true)
构建的。使用该选项构建的密钥仅用于一次加密操作。此外,它们必须使用生物识别技术解锁,使用 BiometricPrompt.authenticate
或 FingerprintManager.authenticate
(现已弃用,取而代之的是 BiometricPrompt)。
official documentation 讨论了两种模式,如果仅在用户已通过身份验证后才授权使用密钥,即:
- 使用
setUserAuthenticationRequired(true)
构建的密钥必须使用 FingerprintManager.authenticate
解锁(现在 BiometricPrompt.authenticate
)
- 使用
setUserAuthenticationValidityDurationSeconds
构建的密钥必须使用 KeyguardManager.createConfirmDeviceCredentialIntent
流程解锁
official biometric auth training guide 末尾的注释建议从 KeyguardManager.createConfirmDeviceCredentialIntent
流切换到新的 BiometricPrompt
和 setDeviceCredentialAllowed(true)
。
但这并不像将密钥的 UserAuthenticationValidityDuration 设置为非零值那么简单,因为一旦初始化签名对象,这将在您的 initSignature(privateKey)
调用中触发 UserNotAuthenticatedException
。还有更多注意事项...请参阅下面的两个示例
生物识别密钥验证
fun biometric_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_BIOMETRIC_KEY
val keyAlias = "MY_BIOMETRIC_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val biometricKeyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (biometricKeyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// init signature else "IllegalStateException: Crypto primitive not initialized" is thrown
signature.initSign(biometricKeyEntry.privateKey)
val cryptoObject = BiometricPrompt.CryptoObject(signature)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded" + result.cryptoObject)
val sigBytes = signature.run {
update("hello world".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setSubtitle("Please authenticate to ...")
// negative button option required for biometric auth
.setNegativeButtonText("Cancel")
.build()
prompt.authenticate(promptInfo, cryptoObject)
}
PIN/Password/Pattern 授权
fun password_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_PIN_PASSWORD_PATTERN_KEY
val keyAlias = "MY_PIN_PASSWORD_PATTERN_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
// this would trigger an UserNotAuthenticatedException: User not authenticated when using the fingerprint
// .setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(10)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val keyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (keyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// this would fail with UserNotAuthenticatedException: User not authenticated
// signature.initSign(keyEntry.privateKey)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded " + result.cryptoObject)
// now it's safe to init the signature using the password key
signature.initSign(keyEntry.privateKey)
val sigBytes = signature.run {
update("hello password/pin/pattern".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setDeviceCredentialAllowed(true)
.build()
prompt.authenticate(promptInfo)
}
以上答案在解决方案和解释方面都不是很准确。
要同时使用生物识别身份验证和设备凭据以及加密对象,请按照以下步骤操作:
- 使用
setUserAuthenticationRequired(true)
和 setUserAuthenticationValidityDurationSeconds(x)
创建密钥。
private SecretKey createSecretKey(String keyName ){
KeyGenParameterSpec.Builder paramsBuilder = new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_SIGN);
paramsBuilder.setUserAuthenticationRequired(true);
paramsBuilder.setUserAuthenticationValidityDurationSeconds(5);
KeyGenParameterSpec keyGenParams = paramsBuilder.build();
KeyGenerator keyGenerator = null;
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256,
ANDROID_KEYSTORE);
keyGenerator.init(keyGenParams);
return keyGenerator.generateKey();
}// All exceptions unhandled
- 初始化加密对象
Mac mac=Mac.getInstance("HmacSHA256");
SecretKey secretKey = getOrCreateSecretKey(keyName);
mac.init(secretKey);
3.Use 使用 setDeviceCredentialAllowed(true)
的生物识别身份验证。不要在身份验证方法中传递加密对象参数 - 像这样 - biometricPrompt.authenticate(promptInfo)
在onAuthenticationSucceeded下
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
byte[] bytes = "secret-text".getBytes();
byte[] macResult = mac.doFinal(bytes);
Log.d("hashed data--",bytesToHex(macResult));
}
mac 对象仅在设备解锁时间不超过 x 秒后才有效。 (setUserAuthenticationValidityDurationSeconds(x)
)。
您可以在解锁设备 x 秒后尝试在 onAuthSucceeded 方法之外使用 mac 对象来验证。请注意,即使解锁 phone 也会使 mac 对象可用 x 秒。它不必在您的应用程序中解锁。
我正在尝试设置 BiometricPrompt,但我需要使用 CryptoObject 进行身份验证,当 https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder.html#setDeviceCredentialAllowed(boolean) 设置为 true 时,这似乎是不可能的。
try {
KeyGeneratorUtil.generateKeyPair("1", null);
} catch (Exception e) {
e.printStackTrace();
}
PrivateKey privateKey;
try {
privateKey = KeyGeneratorUtil.getPrivateKeyReference("test");
} catch (Exception e) {
return;
}
final Signature signature;
try {
signature = initSignature(privateKey);
} catch (Exception e) {
return;
}
final BiometricPrompt.CryptoObject cryptoObject = new BiometricPrompt.CryptoObject(signature);
final BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(context)
.setTitle("Title")
.setDescription("Description")
.setDeviceCredentialAllowed(true)
.build();
...
biometricPrompt.authenticate(cryptoObject, new CancellationSignal(), executor, callback);
当我 运行 这样做时,我得到以下异常。
2019-07-03 13:50:45.140 13715-13715/kcvetano.com.biometricpromptpoc E/AndroidRuntime: FATAL EXCEPTION: main
Process: kcvetano.com.biometricpromptpoc, PID: 13715
java.lang.IllegalArgumentException: Device credential not supported with crypto
at android.hardware.biometrics.BiometricPrompt.authenticate(BiometricPrompt.java:556)
at kcvetano.com.biometricpromptpoc.BiometryAPI29.handleBiometry(BiometryAPI29.java:65)
at kcvetano.com.biometricpromptpoc.MainActivity.onClick(MainActivity.java:56)
at android.view.View.performClick(View.java:7251)
at android.view.View.performClickInternal(View.java:7228)
at android.view.View.access00(View.java:802)
at android.view.View$PerformClick.run(View.java:27843)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7116)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:925)
这应该可以解决问题:
biometricPrompt.authenticate(null, new CancellationSignal(), executor, callback);
错误消息中或多或少地写着(可以说隐藏):当使用setDeviceCredentialAllowed(true)
时,不要使用加密对象。
这一切都分解为如何配置 CryptoObject
中用于加密操作的私钥。
我假设您用于初始化签名对象的私钥是使用 setUserAuthenticationRequired(true)
构建的。使用该选项构建的密钥仅用于一次加密操作。此外,它们必须使用生物识别技术解锁,使用 BiometricPrompt.authenticate
或 FingerprintManager.authenticate
(现已弃用,取而代之的是 BiometricPrompt)。
official documentation 讨论了两种模式,如果仅在用户已通过身份验证后才授权使用密钥,即:
- 使用
setUserAuthenticationRequired(true)
构建的密钥必须使用FingerprintManager.authenticate
解锁(现在BiometricPrompt.authenticate
) - 使用
setUserAuthenticationValidityDurationSeconds
构建的密钥必须使用KeyguardManager.createConfirmDeviceCredentialIntent
流程解锁
official biometric auth training guide 末尾的注释建议从 KeyguardManager.createConfirmDeviceCredentialIntent
流切换到新的 BiometricPrompt
和 setDeviceCredentialAllowed(true)
。
但这并不像将密钥的 UserAuthenticationValidityDuration 设置为非零值那么简单,因为一旦初始化签名对象,这将在您的 initSignature(privateKey)
调用中触发 UserNotAuthenticatedException
。还有更多注意事项...请参阅下面的两个示例
生物识别密钥验证
fun biometric_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_BIOMETRIC_KEY
val keyAlias = "MY_BIOMETRIC_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
.setUserAuthenticationRequired(true)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val biometricKeyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (biometricKeyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// init signature else "IllegalStateException: Crypto primitive not initialized" is thrown
signature.initSign(biometricKeyEntry.privateKey)
val cryptoObject = BiometricPrompt.CryptoObject(signature)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded" + result.cryptoObject)
val sigBytes = signature.run {
update("hello world".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setSubtitle("Please authenticate to ...")
// negative button option required for biometric auth
.setNegativeButtonText("Cancel")
.build()
prompt.authenticate(promptInfo, cryptoObject)
}
PIN/Password/Pattern 授权
fun password_auth() {
val myKeyStore = KeyStore.getInstance("AndroidKeyStore")
myKeyStore.load(null)
val keyGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
"AndroidKeyStore"
)
// build MY_PIN_PASSWORD_PATTERN_KEY
val keyAlias = "MY_PIN_PASSWORD_PATTERN_KEY"
val keyProperties = KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
val builder = KeyGenParameterSpec.Builder(keyAlias, keyProperties)
.setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
// this would trigger an UserNotAuthenticatedException: User not authenticated when using the fingerprint
// .setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(10)
keyGenerator.run {
initialize(builder.build())
generateKeyPair()
}
val keyEntry: KeyStore.Entry = myKeyStore.getEntry(keyAlias, null)
if (keyEntry !is KeyStore.PrivateKeyEntry) {
return
}
// create signature object
val signature = Signature.getInstance("SHA256withECDSA")
// this would fail with UserNotAuthenticatedException: User not authenticated
// signature.initSign(keyEntry.privateKey)
// create biometric prompt
// NOTE: using androidx.biometric.BiometricPrompt here
val prompt = BiometricPrompt(
this,
AsyncTask.THREAD_POOL_EXECUTOR,
object : BiometricPrompt.AuthenticationCallback() {
// override the required methods...
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.w(TAG, "onAuthenticationError $errorCode $errString")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
Log.d(TAG, "onAuthenticationSucceeded " + result.cryptoObject)
// now it's safe to init the signature using the password key
signature.initSign(keyEntry.privateKey)
val sigBytes = signature.run {
update("hello password/pin/pattern".toByteArray())
sign()
}
Log.d(TAG, "sigStr " + Base64.encodeToString(sigBytes, 0))
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.w(TAG, "onAuthenticationFailed")
}
})
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock your device")
.setDeviceCredentialAllowed(true)
.build()
prompt.authenticate(promptInfo)
}
以上答案在解决方案和解释方面都不是很准确。
要同时使用生物识别身份验证和设备凭据以及加密对象,请按照以下步骤操作:
- 使用
setUserAuthenticationRequired(true)
和setUserAuthenticationValidityDurationSeconds(x)
创建密钥。
private SecretKey createSecretKey(String keyName ){
KeyGenParameterSpec.Builder paramsBuilder = new KeyGenParameterSpec.Builder(keyName,
KeyProperties.PURPOSE_SIGN);
paramsBuilder.setUserAuthenticationRequired(true);
paramsBuilder.setUserAuthenticationValidityDurationSeconds(5);
KeyGenParameterSpec keyGenParams = paramsBuilder.build();
KeyGenerator keyGenerator = null;
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256,
ANDROID_KEYSTORE);
keyGenerator.init(keyGenParams);
return keyGenerator.generateKey();
}// All exceptions unhandled
- 初始化加密对象
Mac mac=Mac.getInstance("HmacSHA256");
SecretKey secretKey = getOrCreateSecretKey(keyName);
mac.init(secretKey);
3.Use 使用 setDeviceCredentialAllowed(true)
的生物识别身份验证。不要在身份验证方法中传递加密对象参数 - 像这样 - biometricPrompt.authenticate(promptInfo)
在onAuthenticationSucceeded下
public void onAuthenticationSucceeded(
@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
byte[] bytes = "secret-text".getBytes();
byte[] macResult = mac.doFinal(bytes);
Log.d("hashed data--",bytesToHex(macResult));
}
mac 对象仅在设备解锁时间不超过 x 秒后才有效。 (setUserAuthenticationValidityDurationSeconds(x)
)。
您可以在解锁设备 x 秒后尝试在 onAuthSucceeded 方法之外使用 mac 对象来验证。请注意,即使解锁 phone 也会使 mac 对象可用 x 秒。它不必在您的应用程序中解锁。