前言
目前指紋領域無論從產品角度還是技術角度都已經趨于成熟,但是當各位開發者準備深入探究的時候,卻發現網上很多文章都是皮毛,很難有較深的啟示。本文將著重介紹指紋驗證開發整個過程,包括技術選型、產品的設計方案邏輯、代碼的架構以及后續測試中遇到的兼容性問題等幾個方面。在這里拋磚引玉,希望能給予大家一些啟發。
技術選型
產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:不能,一堆兼容問題。
產品:咱們 Android 端能做指紋驗證嗎?
開發:我……我試試吧……
著手調研,開發前肯定先拿市面上競品的功能來瞧瞧。我們同比了支付寶、微信支付和招商App。
產品:怎么支付寶和微信就沒兼容問題了?
開發:那是因為支付寶和騰迅有自己的協議?。ㄒ宦犜趺碭XX支持,怎么XXX沒問題,升起無名火)這個標準直接和設備廠商合作,而應用方只有微信和支付寶自己。支付寶指紋支付標準是 IFAA ,騰訊的指紋支付標準是 SOTER,也就是說沒有其他應用方會使用這個標準。所以很看應用方和設備廠商的協商程度。現在 IFAA 沒有開源,只有 SOTER 是開源的了,如果接入,我們能省去兼容性測試的工作量,而且有些 6.0 以下的機型 SOTER 也支持。還有?。ㄐ切茄郏┟總€指紋將會有唯一 ID,也就是說,我們能把賬號和指紋綁定起來,更加安全。
產品:不行不行!這 SOTER 壓根沒支持華為,華為用戶是我們的主要用戶群,而且以后機型的擴展受第三方支持的限制。
開發:之前小米和華為就沒有支持 SOTER 標準,現在小米是支持了,華為不見得會支持,因為 SOTER 和廠商合作,出廠的時候就將私鑰存儲在 TEE 中,華為目前多 TEE 系統開發尚未成熟,只能支持一個 TEE ,顯然華為不愿意將唯一的 TEE 交給騰訊掌控。其他手機廠商一般使用高通或第三方的 TEE 系統方案,這些系統目前都支持多 TEE 運行環境,即使將其中一個 TEE 的公共密鑰交給騰訊運營,并不影響手機廠商運營自己的 TEE 平臺。
產品:不接入了,我們用 Google API。
開發:那好,來制定下條件先:
- 設備硬件不支持直接沒得玩
- 手機要有除了指紋外的安全認證方式(比如密碼、圖案) ,這是安卓系統的雙重鎖規則。
- 用戶手機至少錄入了一個指紋,沒錄入指紋說明平時沒有用過指紋驗證功能,這種用戶我們就不管了。
- 使用 Google API,不管什么情況,只要驗證的指紋是系統指紋列表里存在的,就驗證通過,Google API 是沒有提供指紋唯一ID的,所以想要根據本機上的指紋索引來區別不同手指無法做到,也就無法實現指紋和賬號綁定。
- 僅支持 Android 6.0 以上系統,Google 官方支持指紋識別的標準接口是在 Android6.0 開始的,如果廠商在這之前就已經做了指紋識別,那我們就不管了。(開發者也可以使用廠商提供的第三方指紋識別SDK)
產品:(點頭)可以,開干吧!用 Google API 兼容性問題處理和測試量較大,所以我們支持的機型做成可配置,控制風險。第一期先支持幾個機型。
- Google官方Sample
- SOTER 介紹
SOTER 支持機型
SOTER SDK地址- 阿里指紋
- IFAA暫無開源
2018.12.10 更新
SOTER 已支持部分華為機型SOTER 支持機型 wiki
架構
好了,demo 寫完了,看下了產品文檔。啥?場景這么復雜?!分支繁多,還需要結合到之前存在的手勢驗證功能(用戶有兩種安全方式可選:指紋驗證和手勢驗證)。
業務場景有四個:
- 冷啟動app的指紋驗證
- 切換賬號登陸后的引導設置
- 在設置頁用戶手動開啟指紋登陸
- 設置頁手動關閉指紋登陸
每一次驗證的狀態,都會通過 AuthenticationCallback 回調,我們可以理解為是指紋驗證的生命周期。
public class MyAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
//驗證過程中遇到不可恢復的錯誤
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
//驗證過程中遇到可恢復錯誤
super.onAuthenticationHelp(helpMsgId, helpString);
}
}
onAuthenticationSucceeded 和 onAuthenticationError 的回調意味著本次的認證結束,會根據當前所處業務場景給予用戶不同的引導。
而 onAuthenticationFailed 和 onAuthenticationHelp 的情況,四個業務場景都是一樣的,都是在界面上提示用戶,我們可以合并一起處理。
所以我們根本不需要一個業務場景就對應一個 AuthenticationCallback 回調類,我們可以只用一個 AuthenticationCallback 回調類來根據當前所處的業務場景分發行為。但是我又不想在 onAuthenticationSucceeded 和 onAuthenticationError 的回調中有 Switch 邏輯。所以對于四個場景各不相同的 onAuthenticationSucceeded 和 onAuthenticationError 的回調方法,我們用狀態模式來分離,這樣把與特定狀態相關的行為局部化,并且將不同場景下的行為分割開來。(需要給用戶什么提示,什么操作,包括驗證次數超限的處理,取決于當前所處的場景狀態)
另外一點:需要在運行時刻根據狀態來改變行為,比如說用戶從一個正常態,轉移到驗證過程異?;蛘唑炞C過程被劫持的狀態。
驗證過程異常情況,也即是說,受用戶 root 或自定制情況,通過測試的同一個機型有可能驗證過程異常。
驗證過程被劫持,因為 Google API 只返回 true 或 false,我們當然不能無條件相信這個驗證結果,所以需要在應用內產生一對非對稱的密鑰,保證驗證過程不會被篡改。如果拿到驗證結果解密失敗,就進入了被劫持的狀態了。
驗證過程異常和驗證被劫持的狀態基本處理一致,都是屬于用戶無法再繼續驗證的場景,我們可以把這兩個狀態合為一。按照開發的思路,有異常,被劫持,那肯定是失敗了,是吧? 但是按照產品的思路,其他 3 個業務場景按失敗處理,但如果是關閉指紋的場景下(4. 設置頁手動關閉指紋登陸),就算是失敗了,也要讓他去關閉成功,不然可能會出現用戶手機中途 root 或極端情況下,無法關閉指紋,從而引起客訴。
按照分析我們可以發現,被劫持和驗證過程異常的情況的處理,依賴于當時所處的場景,所以呢,我們無法把被劫持和驗證過程異常當做一個獨立的狀態了。只能抽出作為一個公共方法。
為了不和業務邏輯耦合在一起,工具類包裝了一層,主要封裝了驗證條件的判斷,指紋類的初始化等等,最主要的是封裝了加密類 CryptoObjectCreatorHelper ,我們考慮到安全因素,如果不加密的話,就意味著App 無條件信任認證的結果,這個過程可能被攻擊,數據可以被篡改,這是 App 在這種情況下必須承擔的風險。但是這個加密過程和業務是無關的,我們不想讓 Activity 層感知到,所以密鑰和加密對象的銷毀,會統一由工具類來把控。
為了安全,每次驗證過程的密鑰都不同,驗證過程一結束,也就是回調 onAuthenticationSucceeded 和 onAuthenticationError 時,都需要銷毀掉密鑰,但是我們不想讓業務層來操作,所以工具類也有自己的一個 AuthenticationCallback ,在 AuthenticationCallback 里做一些和業務無關的操作,再回調 Activity 的 AuthenticationCallbackListener 。
工具類的 CallBack 是 FingerprintManagerCompat.AuthenticationCallback 實現類,業務層的 AuthenticationCallbackListener 是自定義接口,因為不想把和業務無關的往上傳遞,比如說,驗證成功的 AuthenticationResult ,驗證錯誤的 typeId,這些業務并不關心。Activity 的 AuthenticationCallbackListener 會把請求統一轉發給控制器 FingerPrintTypeController,在轉發給控制器的前后,我們可以做一些通用的業務操作,比如說停止界面的掃描動畫,發一些異步的請求等等,這個就是代理模式的應用了。
那控制器 FingerPrintTypeController 和四個場景的關系又是如何?我們看看類圖。
可以看到,四個場景,對應四個狀態類,控制器和狀態類實現了同一個接口,在內部根據當前場景轉發給對應的類, 那怎么根據場景轉發給對應類?我們建立一個映射表,把場景和類對應起來。每次匹配的話只要 O(1) 復雜度。
private interface FingerPrintType {
void onAuthenticationSucceeded();
void onAuthenticationError(String content);
}
private class LoginAuthType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class ClearType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class LoginSettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class SettingType implements FingerPrintType {
@Override
public void onAuthenticationSucceeded() { }
@Override
public void onAuthenticationError(String content) { }
}
private class FingerPrintTypeController implements FingerPrintType {
private Map<String, FingerPrintType> typeMappingMap = new HashMap<>();
public FingerPrintTypeController() {
typeMappingMap.put(GESTURE_FINGER_SETTING, new SettingType());
typeMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, new LoginSettingType());
typeMappingMap.put(GESTURE_FINGER_CLEAR, new ClearType());
typeMappingMap.put(GESTURE_FINGER_LOGIN, new LoginAuthType());
}
@Override
public void onAuthenticationSucceeded() {
typeMappingMap.get(mType).onAuthenticationSucceeded();
}
@Override
public void onAuthenticationError(String content) {
typeMappingMap.get(mType).onAuthenticationError(content);
}
}
這個時候產品又說了,同樣是異常情況,但是被劫持和異常過程異常的提示文案要不一樣,ok,那我們將提示語和操作分離開來,提示和業務場景的對應關系也預先緩存在 Map 里,直接 get 獲取具體提示,作為參數傳入就可以了。
//普通異常情況提示
exceptionTipsMappingMap = new HashMap<>();
exceptionTipsMappingMap.put(GESTURE_FINGER_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN_SETTING, getString(R.string.fingerprint_no_support_fingerprint_gesture));
exceptionTipsMappingMap.put(GESTURE_FINGER_CLEAR, null);
exceptionTipsMappingMap.put(GESTURE_FINGER_LOGIN, getString(R.string.fingerprint_no_support_fingerprint_account));
兼容問題
1. 明明符合條件,isHardwareDetected() 返回 false?
表現機型:MI 5s、vivo X9
在同一機型上調用 FingerprintManagerCompat 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時候,返回的都是 false,但是調用 FingerprintManager 的 isHardwareDetected()
和 hasEnrolledFingerprints() 時,卻是返回 true。
解決:是否符合指紋條件可以多加一層判斷。
2. Letv X500 Android 6.0,API23 不按正常的套路回調
onAuthenticationError 和 onAuthenticationFailed,理論上應該是識別失敗的情況,但是該機型點擊取消指紋識別也會先回調一次Error,如果遇到這種情況,只能根據具體項目環境中去進行規避適配了。
3. 魅族上遇到的坑
onAuthenticationHelp 回調不按套路出牌,正常官網文檔解釋,這個方法的回調時機是在指紋認證期間發生可恢復性的錯誤時回調。結果在魅族上,啟動指紋識別認證的時候就會回調這個方法,里面傳遞回來的信息提示是“等待按下手指”,也就是說,它的 onAuthenticationHelp 回調跟官網時機不一樣,而且方法的作用也變了,它在正常的情況回調了 onAuthenticationHelp。
解決:不影響驗證流程,無需解決
4. 小米 鎖屏和切后臺生命周期不一致
產品需求:用戶鎖屏或切到后臺時(onStop)自動停止指紋驗證,回到界面時(onResume)自動調起驗證。
所以我在指紋回調方法中加入了標志位 isInAuth。onStop時保存 isInAuth,onResume時 isInAuth == true 則自動調起驗證。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
isInAuth = false;
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
isInAuth = false;
}
@Override
public void onAuthenticationFailed() {
isInAuth = true;
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
isInAuth = true;
}
然而小米6、米mix2 鎖屏時的生命周期是 onAuthenticationError -> onStop;切到后臺是 onStop -> onAuthenticationError。導致不同流程下拿到 isInAuth 標志位不一致,無法自動調起驗證。
解決:界面指紋按鈕可以手動調起驗證,無需兼容處理。
小米5生命周期同上,但是無論是自動還是手動調起驗證,馬上就回調了 onAuthenticationError,也就是說 MI5 從后臺切回來后,指紋驗證流程中斷。
解決:用一個棧來存儲調用方法順序,如果驗證方法調起,馬上就回調 onAuthenticationError 方法,則判定是屬于兼容問題,按驗證失敗來解決。
5. 密鑰解密失敗
三星SM-A9100 、Nexus 6P密鑰解密失敗
解決:暫無法解決
其他兼容解決方案:
- 三星passSdk(不過從2018下半年開始,Pass SDK 將不再提供 DEVICE_FINGERPRINT_UNIQUE_ID 。也就是不再為每個已注冊的指紋提供索引了。因此將無法通過 SDK 區分使用哪個指紋來驗證用戶。)
- 魅族 flyme開發平臺提供了指紋驗證官方api
非兼容問題
1. 新注冊指紋密鑰解密失敗
系統中注冊了一個新的指紋的情況下,即使指紋在系統指紋列表里,驗證也不通過。
解決:刪除了當前無效的key,然后根據參數再次生成密鑰。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
...
/**
* doFinal方法會檢查結果是不是會攔截或者篡改過,
* 如果是的話會拋出一個異常,異常的時候都將認證當做是失敗來處理
*/
try {
result.getCryptoObject().getCipher().doFinal();
mCustomCallback.onAuthenticationSucceeded(true);
} catch (IllegalBlockSizeException e) {
//如果是新錄入的指紋,會拋出該異常,需要重新生成密鑰對重新驗證,這里加個次數限制,避免進入驗證異常->重新驗證->又驗證異常的死循環
if (happenCount == 0) {
beginAuthenticate();
happenCount++;
return;
}
mCustomCallback.onAuthenticationSucceeded(false);
} catch (Exception e) {
mCustomCallback.onAuthenticationSucceeded(false);
}
...
}
2. 設備已有指紋,生成密鑰卻異常提示沒有指紋
非復現,和設備無關,懷疑是谷歌 API 的坑。
java.lang.IllegalStateException: At least one fingerprint must be enrolled to create keys requiring user authentication for every use
解決:暫時只想到針對這個特定異常,直接使用無密鑰驗證,有一定的安全風險,有更好方案歡迎補充。
本文完整 Demo 地址
Demo 僅供參考架構和兼容處理,如果后續接入魅族和三星 SDK,可以考慮用策略模式替換Goolge API。
更新:
Android P 引入了若干可提升應用和運行應用的設備安全性的功能。
其中一項:
我是 FeelsChaotic,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛,致力于追求代碼優雅、架構設計和 T 型成長。
歡迎關注 FeelsChaotic 的簡書和掘金,如果我的文章對你哪怕有一點點幫助,歡迎 ??!你的鼓勵是我寫作的最大動力!
最最重要的,請給出你的建議或意見,有錯誤請多多指正!