Android 6.0新出了指纹验证Fingerprint Authenticate和确认凭证Confirm Credential,都可以用来验证手机用户的身份。对于指纹验证,可以看该篇博客:FingerprintManager
在google官方提供的视频Fingerprint and Payment APIs中讲到Fingerprint Authenticate和Confirm Credential用在支付场景是比较方便的,对于Fingerprint Authenticate这种支付方式的话,谷歌官方提供了一个sample,当然楼主也写了一篇关于指纹支付的sample,可以看该篇文章:Android KeyStore + FingerprintManager 存储密码
其实指纹支付的主要思想就是用通过指纹去取AndroidKeyStore中的key去解密(或加密)一段支付时需要用到的文字,只有通过解密(或加密),这段文字才能用来作为支付的凭证。
那现在来看下如何用Confirm Credential,建议在看该篇文章前,需要先看下FingerprintManager和Android KeyStore + FingerprintManager 存储密码。对于Confirm Credential谷歌也提供了一个例子。
本文章就是根据谷歌提供的例子来讲解。
如果说指纹支付主要是通过指纹验证取key的话,那Confirm Credential就是用屏幕解锁的方式来取key,他不仅仅有指纹,还有其他的pattern(九宫格),PIN(四位密码),Password(英文加字母)的方法来验证用户的身份,从而取出key。
在这个sample一开始的时候,需要先判断当前手机是否有屏幕解锁的方式(滑动解锁不算,无安全性可言)。
private KeyguardManager mKeyguardManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
Button purchaseButton = (Button) findViewById(R.id.purchase_button);
if (!mKeyguardManager.isKeyguardSecure()) {
// Show a message that the user hasn't set up a lock screen.
Toast.makeText(this,
"Secure lock screen hasn't set up.\n"
+ "Go to 'Settings -> Security -> Screenlock' to set up a lock screen",
Toast.LENGTH_LONG).show();
purchaseButton.setEnabled(false);
return;
}
createKey();
findViewById(R.id.purchase_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Test to encrypt something. It might fail if the timeout expired (30s).
tryEncrypt();
}
});
}
判断的方式主要是通过KeyguardManager的isKeyguardSecure方法来判断。
之后就是创建key,创建key的方法和fingeprint sample类似,这里不再赘述。
private void createKey() {
// Generate a key to decrypt payment credentials, tokens, etc.
// This will most likely be a registration step for the user when they are setting up your app.
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
// Set the alias of the entry in Android KeyStore where the key will appear
// and the constrains (purposes) in the constructor of the Builder
keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
// Require that the user has unlocked in the last 30 seconds
.setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException
| InvalidAlgorithmParameterException | KeyStoreException
| CertificateException | IOException e) {
throw new RuntimeException("Failed to create a symmetric key", e);
}
}
重点看两个方法:setUserAuthenticationRequired(true)和setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS),这两个方法说明了使用该key的时候,需要通过用户验证,并且取出key的时间距离手机验证用户身份的时间不超过AUTHENTICATION_DURATION_SECONDS(自己设的值)。如果超时间,那么需要你重新验。
现在当点击支付按钮时,会先去AndroidKeyStore中取key,然后对预先的一段数据进行加密。
private boolean tryEncrypt() {
try {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null);
Cipher cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
// Try encrypting something, it will only work if the user authenticated within
// the last AUTHENTICATION_DURATION_SECONDS seconds.
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
cipher.doFinal(SECRET_BYTE_ARRAY);
// If the user has recently authenticated, you will reach here.
TextView textView = (TextView) findViewById(
R.id.already_has_valid_device_credential_message);
textView.setVisibility(View.VISIBLE);
textView.setText(getString(
R.string.already_confirmed_device_credentials_within_last_x_seconds,
AUTHENTICATION_DURATION_SECONDS));
findViewById(R.id.purchase_button).setEnabled(false);
return true;
} catch (UserNotAuthenticatedException e) {
// User is not authenticated, let's authenticate with device credentials.
Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
if (intent != null) {
startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
}
return false;
} catch (KeyPermanentlyInvalidatedException e) {
// This happens if the lock screen has been disabled or reset after the key was
// generated after the key was generated.
Toast.makeText(this, "Keys are invalidated after created. Retry the purchase\n"
+ e.getMessage(),
Toast.LENGTH_LONG).show();
return false;
} catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException |
CertificateException | UnrecoverableKeyException | IOException
| NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
在取Key的时候和Fingerprint不一样,在Fingerprint中需要通过调用fingerprintManager.authenticate()方法来取出,但是这里可以直接取,如果上次验证用户的时间距离现在未超过设定的时间,那么key可以直接取,但是超过了,取的时候,会抛出UserNotAuthenticatedException,这时候你需要去唤起解锁验证的页面。
通过如下语句来唤起:
Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null);
if (intent != null) {
startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) {
// Challenge completed, proceed with using cipher
if (resultCode == RESULT_OK) {
if (tryEncrypt()) {
showPurchaseConfirmation();
}
} else {
// The user canceled or didn’t complete the lock screen
// operation. Go to error/cancellation flow.
}
}
}
然后在onActivityResult函数中,重新去获取key,去加密文本。Confirm Credential比起Fingerprint要更加简单。