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要更加簡單。