這陣子幫專案寫指紋登入的功能
花了一點時間看文件後,決定做一個 helper 來簡化流程
要在程式中使用 Fingerprint API 其實很簡單
只要滿足以下三個步驟
Step1. 在 AndroidManifest.xml 加入 FingerPrint 權限。
Step2. 確認硬體裝置是否支援以及環境設定是否完成。
Step3. 透過 FingerprintManager 執行 Authenticate 的動作
第一步很簡單,加入下列權限即可
<!--指紋-->
<uses-permission
android:name="android.permission.USE_FINGERPRINT" />
第二步和第三步用我們的 helper 可以簡單實現
先 call helper.checkAndReturnStatus()
若 return status = PASS
則 call helper.authenticate(AuthenticationCallback callback)
最後就是在 callback 做處理
就是這麼簡單。
若要停止驗證 helper.cancellAuthenticate() 可以幫
============以上就是基本的指紋應用==============
進階版的話需要加入一個步驟
『 Setp2-2. 建立加密物件 CryptoObject 』
此物件是用來做安全性防護的
以免第三方在我們進行指紋辯識時動手腳。
建立的方法在這就不細說
下方 code 裡面我有加註解 ( 或點我看官方範例解說 )
CryptoObject 在 init 階段就已經幫忙建好了
大家 new Helper 後,依然可以直接使用 authenticate() 進行驗證
import android.app.KeyguardManager; import android.content.Context; import android.hardware.fingerprint.FingerprintManager; import android.os.Build; import android.os.CancellationSignal; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore.KeyProperties; import android.support.annotation.RequiresApi; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; /** * 2018/11/23 * Create by Zewei */ public class FingerprintHelper { private KeyguardManager keyguardManager; private FingerprintManager fingerprintManager; // 儲存 Key 的物件 private KeyStore mKeyStore; // 產生 key 的物件 ( 使用 AES ) private KeyGenerator mKeyGenerator; // Generator 需要的 KeyName static final String KEY_NAME = "MMB_Fingerprint_Key"; // 取消驗證需要此物件 private CancellationSignal mCancellationSignal; // 驗證用的加密物件 private FingerprintManager.CryptoObject cryptoObject; // 建立 CryptoObject 的演算法參數 private Cipher cipher; // 此 class 需要 M 以上調用,否則會 crush @RequiresApi(api = Build.VERSION_CODES.M) public FingerprintHelper(Context context) { keyguardManager = context.getSystemService(KeyguardManager.class); fingerprintManager = context.getSystemService(FingerprintManager.class); try { mKeyStore = KeyStore.getInstance("AndroidKeyStore"); mKeyGenerator = KeyGenerator .getInstance( KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); } catch (KeyStoreException e) { throw new RuntimeException( "Failed to get an instance of KeyStore", e); } catch (NoSuchAlgorithmException | NoSuchProviderException e) { throw new RuntimeException( "Failed to get an instance of KeyGenerator", e); } if(checkAndReturnStatus() == status.PASS) init(); } @RequiresApi(api = Build.VERSION_CODES.M) private void init() { // 以 KeyGenerator 建立一組 AES Key createKey(KEY_NAME); // 初始化 cipher 並以建立驗證用的加密物件 CryptoObject try { // 演算法皆是固定值 cipher = Cipher.getInstance( KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException( "Failed to get an instance of Cipher", e); } if (initCipher(cipher, KEY_NAME)) { // CryptoObject = null 也可驗證只是有機會被第三方看到 cryptoObject = new FingerprintManager.CryptoObject(cipher); } // 取消驗證需要此物件 mCancellationSignal = new CancellationSignal(); } /** * 檢查是不是可以使用指紋辨識,回傳staus * <p> * NEED_SET_UP_LOCK_SCREEM, // 需要設定螢幕鎖 * NEED_ENROLL_FINGERPRINT, // 需要註冊指紋 * NOT_FIND_HARDWARE, // 找不到指紋感應器 * VERSION_NOT_SUPPORT, // Android 版本過低 * PASS // 通過檢測 * * @return */ public status checkAndReturnStatus() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!isHardwareDetected()) { return status.NOT_FIND_HARDWARE; } if (!hasEnrolledFingerprints()) { return status.NEED_ENROLL_FINGERPRINT; } if (!isSetUpLockScreen()) { return status.NEED_SET_UP_LOCK_SCREEM; } } else { // 由於 new class 時就需判斷版本,故不可能走到這。 return status.VERSION_NOT_SUPPORT; } return status.PASS; } /** * 驗證,等待 User 感應指紋 */ public void authenticate( FingerprintManager.AuthenticationCallback callback) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { fingerprintManager.authenticate( cryptoObject, mCancellationSignal, 0, callback, null); } } /** * 取消驗證,終止感應等待 */ public void cancellAuthenticate() { if (!mCancellationSignal.isCanceled()) mCancellationSignal.cancel(); } @RequiresApi(api = Build.VERSION_CODES.M) private void createKey(String keyName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { createKey(keyName, true); } else { createKey(keyName, false); } } /** * Creates a symmetric key in the Android Key Store which * can only be used after the user has authenticated with * fingerprint. * * @param keyName the name of the key to be created * @param invalidatedByBiometricEnrollment * if {@code false} is passed, the created key will not * be invalidated even if a new fingerprint is enrolled. * The default value is {@code true}, so passing * {@code true} doesn't change the behavior * (the key will be invalidated if a new fingerprint is * enrolled.). Note that this parameter is only valid if * the app works on Android N developer preview. */ @RequiresApi(api = Build.VERSION_CODES.M) private void createKey(String keyName, boolean invalidatedByBiometricEnrollment) { // The enrolling flow for fingerprint. // This is where you ask the user to set up fingerprint // for your flow. Use of keys is necessary // if you need to know if the set of // enrolled fingerprints has changed. try { mKeyStore.load(null); // Set the alias of the entry in Android KeyStore // where the key will appear // and the constrains in the constructor of the Builder KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(keyName, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT ) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) // Require the user to authenticate with // a fingerprint to authorize every use // of the key .setUserAuthenticationRequired(true) .setEncryptionPaddings( KeyProperties.ENCRYPTION_PADDING_PKCS7); // This is a workaround to avoid crashes on devices // whose API level is < 24 // because KeyGenParameterSpec.Builder#setInvalidatedByBiometricEnrollment is only // visible on API level +24. // Ideally there should be a compat library for KeyGenParameterSpec.Builder but // which isn't available yet. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { builder.setInvalidatedByBiometricEnrollment( invalidatedByBiometricEnrollment); } mKeyGenerator.init(builder.build()); mKeyGenerator.generateKey(); } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e) { throw new RuntimeException(e); } } /** * Initialize the {@link Cipher} instance with the created * key in the {@link #createKey(String, boolean)} method. * * @param keyName the key name to init the cipher * @return {@code true} if initialization is successful, * {@code false} if the lock screen has * been disabled or reset after the key was generated, * or if a fingerprint got enrolled after * the key was generated. */ @RequiresApi(api = Build.VERSION_CODES.M) private boolean initCipher(Cipher cipher, String keyName) { try { mKeyStore.load(null); SecretKey key = (SecretKey) mKeyStore.getKey(keyName, null); cipher.init(Cipher.ENCRYPT_MODE, key); return true; } catch (KeyPermanentlyInvalidatedException e) { return false; } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException("Failed to init Cipher", e); } } /** * 是否設定過螢幕安全鎖 * 使用指紋必需設定螢幕安全鎖, * Google 認為搭配傳統安全鎖可以補強指紋的不足處 * * @return */ private boolean isSetUpLockScreen() { return keyguardManager.isKeyguardSecure(); } /** * 是否有註冊過指紋,至少一組 * * @return */ @RequiresApi(api = Build.VERSION_CODES.M) private boolean hasEnrolledFingerprints() { return fingerprintManager.hasEnrolledFingerprints(); } /** * 是否有指紋感應器 * * @return */ @RequiresApi(api = Build.VERSION_CODES.M) private boolean isHardwareDetected() { return fingerprintManager.isHardwareDetected(); } public enum status { NEED_SET_UP_LOCK_SCREEM, // 需要設定螢幕鎖 NEED_ENROLL_FINGERPRINT, // 需要註冊指紋 NOT_FIND_HARDWARE, // 找不到指紋感應器 VERSION_NOT_SUPPORT, // Android 版本過低 PASS // 通過檢測 } }