close

這陣子幫專案寫指紋登入的功能

花了一點時間看文件後,決定做一個 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                        // 通過檢測
    }


}

 

 

 

 

 


arrow
arrow
    創作者介紹
    創作者 顏澤偉 的頭像
    顏澤偉

    Willy's Fish教學筆記』

    顏澤偉 發表在 痞客邦 留言(0) 人氣()