1. 程式人生 > >Android中的指紋識別

Android中的指紋識別

評論中很多朋友反映,根據我給出的方案,拿不到指紋資訊這個問題,在這裡統一說明一下。

首先,這篇文章中涉及到的程式碼,我在一部魅族手機和一部三星手機上進行測試過,能獲取到資訊。其他手機機型我沒有測試,不知道詳細情況。

其次,我在部落格中也說明了,在不同手機廠商的定製系統裡面獲取到的指紋資訊很可能是不同的,我測試的魅族手機和三星手機返回的資訊格式就不一樣。按照本文的方法獲取到的指紋資訊是一個比較雞肋的功能,作用有限,我目前也沒有能力給出一個完美的解決方案,如果有哪位找到了完美的解決方案,歡迎分享。

另外,指紋資訊相關 API 是用 @hide 修飾的,這說明 Google 官方是不希望開發者使用這類 API ,使用這類 API 可能會有風險,如方法引數改變、在後續版本中被移除等等。

因此,對於指紋識別這一塊,建議讀者使用本文中提及的指紋識別功能,儘可能避免使用本文後面提及的“獲取指紋資訊”這一功能。

最近專案需要使用到指紋識別的功能,查閱了相關資料後,整理成此文。

指紋識別是在Android 6.0之後新增的功能,因此在使用的時候需要先判斷使用者手機的系統版本是否支援指紋識別。另外,實際開發場景中,使用指紋的主要場景有兩種:

  • 純本地使用。即使用者在本地完成指紋識別後,不需要將指紋的相關資訊給後臺。
  • 與後臺互動。使用者在本地完成指紋識別後,需要將指紋相關的資訊傳給後臺。

由於使用指紋識別功能需要一個加密物件(CryptoObject)該物件一般是由對稱加密或者非對稱加密獲得。上述兩種開發場景的實現大同小異,主要區別在於加密過程中金鑰的建立和使用,一般來說,純本地的使用指紋識別功能,只需要對稱加密

即可;而與後臺互動則需要使用非對稱加密:將私鑰用於本地指紋識別,識別成功後將加密資訊傳給後臺,後臺開發人員用公鑰解密,以獲得使用者資訊。

下面先簡單介紹一下對稱加密和非對稱加密的相關概念,然後對兩種開發方式的實現分別進行講解。

對稱加密、非對稱加密和簽名

在正式使用指紋識別功能之前,有必要先了解一下對稱加密和非對稱加密的相關內容。

  • 對稱加密:所謂對稱,就是採用這種加密方法的雙方使用方式用同樣的金鑰進行加密和解密。金鑰是控制加密及解密過程的指令。演算法是一組規則,規定如何進行加密和解密。因此加密的安全性不僅取決於加密演算法本身,金鑰管理的安全性更是重要。因為加密和解密都使用同一個金鑰,如何把金鑰安全地傳遞到解密者手上就成了必須要解決的問題。

  • 非對稱加密:非對稱加密演算法需要兩個金鑰:公開金鑰(publickey)和私有金鑰(privatekey)。公開金鑰與私有金鑰是一對,如果用公開金鑰對資料進行加密,只有用對應的私有金鑰才能解密;如果用私有金鑰對資料進行加密,那麼只有用對應的公開金鑰才能解密。因為加密和解密使用的是兩個不同的金鑰,所以這種演算法叫作非對稱加密演算法。 非對稱加密演算法實現機密資訊交換的基本過程是:甲方生成一對金鑰並將其中的一把作為公用金鑰向其它方公開;得到該公用金鑰的乙方使用該金鑰對機密資訊進行加密後再發送給甲方;甲方再用自己儲存的另一把專用金鑰對加密後的資訊進行解密。

  • 簽名:在資訊的後面再加上一段內容,可以證明資訊沒有被修改過。一般是對資訊做一個hash計算得到一個hash值,注意,這個過程是不可逆的,也就是說無法通過hash值得出原來的資訊內容。在把資訊傳送出去時,把這個hash值加密後做為一個簽名和資訊一起發出去。

由以上內容可以瞭解到,對稱加密和非對稱加密的特點如下:

  • 對稱加密的優點是速度快,適合於本地資料和本地資料庫的加密,安全性不如非對稱加密。常見的對稱加密演算法有DES、3DES、AES、Blowfish、IDEA、RC5、RC6。
  • 非對稱加密的安全性比較高,適合對需要網路傳輸的資料進行加密,速度不如對稱加密。非對稱加密應用於SSH, HTTPS, TLS,電子證書,電子簽名,電子身份證等等

指紋識別的對稱加密實現

使用指紋識別的對稱加密功能的主要流程如下:

  1. 使用 KeyGenerator 建立一個對稱金鑰,存放在 KeyStore 裡。
  2. 設定 KeyGenParameterSpec.Builder.setUserAuthenticationRequired() 為true,
  3. 使用建立好的對稱金鑰初始化一個Cipher物件,並用該物件呼叫 FingerprintManager.authenticate() 方法啟動指紋感測器並開始監聽。
  4. 重寫 FingerprintManager.AuthenticationCallback 的幾個回撥方法,以處理指紋識別成功(onAuthenticationSucceeded())、失敗(onAuthenticationFailed()onAuthenticationError())等情況。

建立金鑰

建立金鑰要涉及到兩個類:KeyStore 和 KeyGenerator。

KeyStore 是用於儲存、獲取金鑰(Key)的容器,獲取 KeyStore的方法如下:

try {
    mKeyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
    throw new RuntimeException("Failed to get an instance of KeyStore", e);
}

而生成 Key,如果是對稱加密,就需要 KeyGenerator 類。獲取一個 KeyGenerator 物件比較簡單,方法如下:

// 對稱加密, 建立 KeyGenerator 物件
try {
    mKeyGenerator = KeyGenerator
            .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
    throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
}

獲得 KeyGenerator 物件後,就可以生成一個 Key 了:

 try {
    keyStore.load(null);
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(defaultKeyName,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setUserAuthenticationRequired(true)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        builder.setInvalidatedByBiometricEnrollment(true);
    }
    keyGenerator.init(builder.build());
    keyGenerator.generateKey();
} catch (CertificateException | NoSuchAlgorithmException | IOException | InvalidAlgorithmParameterException e) {
    e.printStackTrace();
}

建立並初始化 Cipher 物件

Cipher 物件是一個按照一定的加密規則,將資料進行加密後的一個物件。呼叫指紋識別功能需要使用到這個物件。建立 Cipher 物件很簡單,如同下面程式碼那樣:

Cipher defaultCipher;
try {
    defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
    throw new RuntimeException("建立Cipher物件失敗", e);
}

然後使用剛才建立好的金鑰,初始化 Cipher 物件:

 try {
    keyStore.load(null);
    SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return true;
} catch (IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyStoreException | InvalidKeyException e) {
    throw new RuntimeException("初始化 cipher 失敗", e);
}

使用指紋識別功能

真正到了使用指紋識別功能的時候,你會發現其實很簡單,只是呼叫 FingerprintManager 類的的方法authenticate()而已,然後系統會有相應的回撥反饋給我們,該方法如下:

public void authenticate(CryptoObject crypto, CancellationSignal cancel, int flags, AuthenticationCallback callback, Handler handler) 

該方法的幾個引數解釋如下:

  • 第一個引數是一個加密物件。還記得之前我們大費周章地建立和初始化的Cipher物件嗎?這裡的 CryptoObject 物件就是使用 Cipher 物件建立創建出來的:new FingerprintManager.CryptoObject(cipher)
  • 第二個引數是一個 CancellationSignal 物件,該物件提供了取消操作的能力。建立該物件也很簡單,使用 new CancellationSignal() 就可以了。
  • 第三個引數是一個標誌,預設為0。
  • 第四個引數是 AuthenticationCallback 物件,它本身是 FingerprintManager 類裡面的一個抽象類。該類提供了指紋識別的幾個回撥方法,包括指紋識別成功、失敗等。需要我們重寫。
  • 最後一個 Handler,可以用於處理回撥事件,可以傳null。

完成指紋識別後,還要記得將 AuthenticationCallback 關閉掉:

public void stopListening() {
    if (cancellationSignal != null) {
        selfCancelled = true;
        cancellationSignal.cancel();
        cancellationSignal = null;
    }
}

重寫回調方法

呼叫了 authenticate() 方法後,系統就會啟動指紋感測器,並開始掃描。這時候根據掃描結果,會通過FingerprintManager.AuthenticationCallback類返回幾個回撥方法:

// 成功
onAuthenticationSucceeded()
// 失敗
onAuthenticationFaile()
// 錯誤
onAuthenticationError()

一般我們需要重寫這幾個方法,以實現我們的功能。關於onAuthenticationFaile()onAuthenticationError()的區別,後面會講到。

指紋識別的非對稱加密實現

其實流程和上面的流程差不多:

  1. 使用 KeyPairGenerator 建立一個非對稱金鑰。
  2. 使用建立好的私鑰進行簽名,使用該簽名建立一個加密物件,並將該物件作為 FingerprintManager.authenticate() 方法的一個引數,啟動指紋感測器並開始監聽。
  3. 重寫 FingerprintManager.AuthenticationCallback 類的幾個回撥方法,以處理指紋識別成功(onAuthenticationSucceeded())、失敗(onAuthenticationFailed()onAuthenticationError())等情況。

可以看見,指紋識別的非對稱加密方式和對稱加密方式的實現流程是差不多的,它們之間最明顯的差別是在於金鑰的生成與使用。

建立金鑰

這裡要使用 KeyPairGenerator 來建立一組非對稱金鑰,首先是獲取 KeyPairGenerator 物件:

// 非對稱加密,建立 KeyPairGenerator 物件
try {
    mKeyPairGenerator =  KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
    throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
}

得到了 KeyPairGenerator 物件後,就可以建立 KeyPair(金鑰對)了:

try {
    // Set the alias of the entry in Android KeyStore where the key will appear
    // and the constrains (purposes) in the constructor of the Builder
    mKeyPairGenerator.initialize(
            new KeyGenParameterSpec.Builder(KEY_NAME,
                    KeyProperties.PURPOSE_SIGN)
                    .setDigests(KeyProperties.DIGEST_SHA256)
                    .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
                    // Require the user to authenticate with a fingerprint to authorize
                    // every use of the private key
                    .setUserAuthenticationRequired(true)
                    .build());
    mKeyPairGenerator.generateKeyPair();
} catch (InvalidAlgorithmParameterException e) {
    throw new RuntimeException(e);
}

簽名

指紋識別的對稱加密實現中使用了Cipher物件來建立CryptoObject物件,而在這裡,我們將會使用私鑰進行簽名,用簽名物件來建立CryptoObject物件:

// 使用私鑰簽名
try {
    mKeyStore.load(null);
    PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
    mSignature.initSign(key);
    return true;
} catch (KeyPermanentlyInvalidatedException e) {
    return false;
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
        | NoSuchAlgorithmException | InvalidKeyException e) {
    throw new RuntimeException("Failed to init Cipher", e);
}

同樣的,呼叫new FingerprintManager.CryptoObject(mSignature)方法建立一個CryptoObject物件。

呼叫指紋識別方法

這裡的使用方法和前面“指紋識別的對稱加密實現”中的呼叫方法是一樣的,都是呼叫FingerprintManager.authenticate()方法。這裡就不再敘述。

監聽回撥

監聽回撥也和之前的類似,唯一不同的是,我們在識別成功後需要和後臺進行互動,也就是onAuthenticationSucceeded()中處理的邏輯不一樣。

實際應用中的注意事項

判斷使用者是否可以使用指紋識別功能

一般來說,為了增加安全性,要求使用者在手機的“設定”中開啟了密碼鎖屏功能。當然,使用指紋解鎖的前提是至少錄入了一個指紋。

// 如果沒有設定密碼鎖屏,則不能使用指紋識別
if (!keyguardManager.isKeyguardSecure()) {
    Toast.makeText(this, "請在設定介面開啟密碼鎖屏功能",
            Toast.LENGTH_LONG).show();
}
// 如果沒有錄入指紋,則不能使用指紋識別
if (!fingerprintManager.hasEnrolledFingerprints()) {
    Toast.makeText(this, "您還沒有錄入指紋, 請在設定介面錄入至少一個指紋",
            Toast.LENGTH_LONG).show();
}

這裡用到了兩個類:KeyguardManagerFingerprintManager,前者是螢幕保護的相關類。後者是指紋識別的核心類。

關於指紋識別回撥方法

前面說到AuthenticationCallback類裡面的幾個回撥方法,其中有三個是我們開發中需要用到的:

onAuthenticationError()
onAuthenticationSucceeded()
onAuthenticationFailed()

關於這三個回撥方法,有幾點需要注意的:

  1. 當指紋識別失敗後,會呼叫onAuthenticationFailed()方法,這時候指紋感測器並沒有關閉,系統給我們提供了5次重試機會,也就是說,連續呼叫了5次onAuthenticationFailed()方法後,會呼叫onAuthenticationError()方法。

  2. 當系統呼叫了onAuthenticationError()onAuthenticationSucceeded()後,感測器會關閉,只有我們重新授權,再次呼叫authenticate()方法後才能繼續使用指紋識別功能。

  3. 當系統回調了onAuthenticationError()方法關閉感測器後,這種情況下再次呼叫authenticate()會有一段時間的禁用期,也就是說這段時間裡是無法再次使用指紋識別的。當然,具體的禁用時間由手機廠商的系統不同而有略微差別,有的是1分鐘,有的是30秒等等。而且,由於手機廠商的系統區別,有些系統上呼叫了onAuthenticationError()後,在禁用時間內,其他APP裡面的指紋識別功能也無法使用,甚至系統的指紋解鎖功能也無法使用。而有的系統上,在禁用時間內呼叫其他APP的指紋解鎖功能,或者系統的指紋解鎖功能,就能立即重置指紋識別功能。

示例程式碼

最後, Android Sample 裡面關於指紋的示例程式碼地址如下:

以下內容更新於 2017.6.5

獲取指紋資訊

越來越多的朋友開始關心指紋識別這一功能模組,並且通過各種渠道向我諮詢一些關於指紋識別的需求解決方案。這裡就統一說明一下,就不一一回復了。

前面已經說到了,監聽指紋識別成功之後會有一個 onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) 回撥方法。該方法會給我們一個 AuthenticationResult 類的物件 result,該類的原始碼如下:

public static class AuthenticationResult {
    private Fingerprint mFingerprint;
    private CryptoObject mCryptoObject;

    /**
     * Authentication result
     *
     * @param crypto the crypto object
     * @param fingerprint the recognized fingerprint data, if allowed.
     * @hide
     */
    public AuthenticationResult(CryptoObject crypto, Fingerprint fingerprint) {
        mCryptoObject = crypto;
        mFingerprint = fingerprint;
    }

    /**
     * Obtain the crypto object associated with this transaction
     * @return crypto object provided to {@link FingerprintManager#authenticate(CryptoObject,
     *     CancellationSignal, int, AuthenticationCallback, Handler)}.
     */
    public CryptoObject getCryptoObject() { return mCryptoObject; }

    /**
     * Obtain the Fingerprint associated with this operation. Applications are strongly
     * discouraged from associating specific fingers with specific applications or operations.
     *
     * @hide
     */
    public Fingerprint getFingerprint() { return mFingerprint; }
};

這個類裡面包含了一個 Fingerprint 物件,如果我們檢視 Fingerprint 類的原始碼,可以得知該類提供了指紋的一些屬性,包括指紋的名稱、GroupId、FingerId 和 DeviceId 等屬性。也就是說,通過 onAuthenticationSucceeded() 回撥方法,我們可以得到識別的指紋的一些資訊。

那為什麼我們之前不使用該返回給我們的 AuthenticationResult 類物件 result 呢?因為我們沒辦法通過正常途徑獲取到這些資訊。因為 mFingerprint 屬性是私有的,getFingerprint() 方法是被 @hide 修飾的,甚至連儲存指紋資訊的 Fingerprint 類也是被 @hide 修飾的。

在 Android SDK 中,有兩種 API 是我們無法直接獲取的,一種是位於包 com.android.internal 下面的 API,另一種是被 @hide 修飾的類和方法。

然而,針對第二種被隱藏的 API,我們可以通過反射的方式來呼叫相關的類和方法。以 onAuthenticationSucceeded() 回撥方法為例,看看如何獲取識別成功的指紋資訊:

@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
    try {
        Field field = result.getClass().getDeclaredField("mFingerprint");
        field.setAccessible(true);
        Object fingerPrint = field.get(result);

        Class<?> clzz = Class.forName("android.hardware.fingerprint.Fingerprint");
        Method getName = clzz.getDeclaredMethod("getName");
        Method getFingerId = clzz.getDeclaredMethod("getFingerId");
        Method getGroupId = clzz.getDeclaredMethod("getGroupId");
        Method getDeviceId = clzz.getDeclaredMethod("getDeviceId");

        CharSequence name = (CharSequence) getName.invoke(fingerPrint);
        int fingerId = (int) getFingerId.invoke(fingerPrint);
        int groupId = (int) getGroupId.invoke(fingerPrint);
        long deviceId = (long) getDeviceId.invoke(fingerPrint);

        Log.d(TAG, "name: " + name);
        Log.d(TAG, "fingerId: " + fingerId);
        Log.d(TAG, "groupId: " + groupId);
        Log.d(TAG, "deviceId: " + deviceId);
    } catch (NoSuchFieldException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
        e.printStackTrace();
    }
}

顯示的結果如下:

name: 
fingerId: 1765296462
groupId: 0
deviceId: 547946660160

其中,返回的 name 為空字串。

這裡要提醒一點,由於每個手機廠商都會對 Android 系統進行或多或少的定製,因此在不同的手機上呼叫上面的方法得到的結果可能會不一樣。比如在我的手機上,fingerId 是一個十位數的整數,在其他手機上面可能就是個一位數的整數。

得到了指紋資訊之後,我們就可以做一些額外的功能了,比如監聽使用者是否使用同一個指紋來解鎖,就可以用上面的方法來判斷。

除了在 onAuthenticationSucceeded() 回撥方法中獲取被識別的指紋資訊外,還可以利用 FingerprintManager 類的 getEnrolledFingerprints() 方法來獲取手機中儲存的指紋列表:

public void getFingerprintInfo() {
    try {
        FingerprintManager fingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE);
        Method method = FingerprintManager.class.getDeclaredMethod("getEnrolledFingerprints");
        Object obj = method.invoke(fingerprintManager);
        if (obj != null) {
            Class<?> clazz = Class.forName("android.hardware.fingerprint.Fingerprint");
            Method getFingerId = clazz.getDeclaredMethod("getFingerId");
            for (int i = 0; i < ((List) obj).size(); i++) {
                Object item = ((List) obj).get(i);
                if (null == item) {
                    continue;
                }

                Log.d(TAG, "fingerId: " + getFingerId.invoke(item));
            }
        }
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
        e.printStackTrace();
    }
}

在一些場景中,需要對使用者手機裡面錄入的指紋進行監聽,當用戶手機裡面的指紋列表發生了變化(比如使用者刪除了指紋或者新增了指紋),就需要使用者重新輸入密碼,這時就可以用上面的方法,記錄使用者手機裡面的指紋 ID,並進行前後對比。(當然,這種做法的安全係數並不高,也難以相容眾多裝置,這裡只是舉例說明用途)