前言
目前指紋領(lǐng)域無論從產(chǎn)品角度還是技術(shù)角度都已經(jīng)趨于成熟杰捂,但是當(dāng)各位開發(fā)者準(zhǔn)備深入探究的時候舆床,卻發(fā)現(xiàn)網(wǎng)上很多文章都是皮毛,很難有較深的啟示琼娘。本文將著重介紹指紋驗證開發(fā)整個過程峭弟,包括技術(shù)選型附鸽、產(chǎn)品的設(shè)計方案邏輯脱拼、代碼的架構(gòu)以及后續(xù)測試中遇到的兼容性問題等幾個方面。在這里拋磚引玉坷备,希望能給予大家一些啟發(fā)熄浓。
技術(shù)選型
產(chǎn)品:咱們 Android 端能做指紋驗證嗎?
開發(fā):不能省撑,一堆兼容問題赌蔑。
產(chǎn)品:咱們 Android 端能做指紋驗證嗎?
開發(fā):不能竟秫,一堆兼容問題娃惯。
產(chǎn)品:咱們 Android 端能做指紋驗證嗎?
開發(fā):不能肥败,一堆兼容問題趾浅。
產(chǎn)品:咱們 Android 端能做指紋驗證嗎?
開發(fā):我……我試試吧……
著手調(diào)研馒稍,開發(fā)前肯定先拿市面上競品的功能來瞧瞧皿哨。我們同比了支付寶、微信支付和招商App纽谒。
產(chǎn)品:怎么支付寶和微信就沒兼容問題了证膨?
開發(fā):那是因為支付寶和騰迅有自己的協(xié)議!(一聽怎么XXX支持鼓黔,怎么XXX沒問題央勒,升起無名火)這個標(biāo)準(zhǔn)直接和設(shè)備廠商合作不见,而應(yīng)用方只有微信和支付寶自己。支付寶指紋支付標(biāo)準(zhǔn)是 IFAA 订歪,騰訊的指紋支付標(biāo)準(zhǔn)是 SOTER脖祈,也就是說沒有其他應(yīng)用方會使用這個標(biāo)準(zhǔn)。所以很看應(yīng)用方和設(shè)備廠商的協(xié)商程度∷⒔現(xiàn)在 IFAA 沒有開源盖高,只有 SOTER 是開源的了,如果接入眼虱,我們能省去兼容性測試的工作量喻奥,而且有些 6.0 以下的機(jī)型 SOTER 也支持。還有D笮(星星眼)每個指紋將會有唯一 ID撞蚕,也就是說,我們能把賬號和指紋綁定起來过牙,更加安全甥厦。
產(chǎn)品:不行不行!這 SOTER 壓根沒支持華為寇钉,華為用戶是我們的主要用戶群刀疙,而且以后機(jī)型的擴(kuò)展受第三方支持的限制。
開發(fā):之前小米和華為就沒有支持 SOTER 標(biāo)準(zhǔn)扫倡,現(xiàn)在小米是支持了谦秧,華為不見得會支持,因為 SOTER 和廠商合作撵溃,出廠的時候就將私鑰存儲在 TEE 中疚鲤,華為目前多 TEE 系統(tǒng)開發(fā)尚未成熟,只能支持一個 TEE 缘挑,顯然華為不愿意將唯一的 TEE 交給騰訊掌控集歇。其他手機(jī)廠商一般使用高通或第三方的 TEE 系統(tǒng)方案,這些系統(tǒng)目前都支持多 TEE 運(yùn)行環(huán)境语淘,即使將其中一個 TEE 的公共密鑰交給騰訊運(yùn)營诲宇,并不影響手機(jī)廠商運(yùn)營自己的 TEE 平臺。
產(chǎn)品:不接入了亏娜,我們用 Google API焕窝。
開發(fā):那好,來制定下條件先:
- 設(shè)備硬件不支持直接沒得玩
- 手機(jī)要有除了指紋外的安全認(rèn)證方式(比如密碼维贺、圖案) 它掂,這是安卓系統(tǒng)的雙重鎖規(guī)則。
- 用戶手機(jī)至少錄入了一個指紋,沒錄入指紋說明平時沒有用過指紋驗證功能虐秋,這種用戶我們就不管了榕茧。
- 使用 Google API,不管什么情況客给,只要驗證的指紋是系統(tǒng)指紋列表里存在的用押,就驗證通過,Google API 是沒有提供指紋唯一ID的靶剑,所以想要根據(jù)本機(jī)上的指紋索引來區(qū)別不同手指無法做到蜻拨,也就無法實(shí)現(xiàn)指紋和賬號綁定。
- 僅支持 Android 6.0 以上系統(tǒng)桩引,Google 官方支持指紋識別的標(biāo)準(zhǔn)接口是在 Android6.0 開始的缎讼,如果廠商在這之前就已經(jīng)做了指紋識別,那我們就不管了坑匠。(開發(fā)者也可以使用廠商提供的第三方指紋識別SDK)
產(chǎn)品:(點(diǎn)頭)可以血崭,開干吧!用 Google API 兼容性問題處理和測試量較大厘灼,所以我們支持的機(jī)型做成可配置夹纫,控制風(fēng)險。第一期先支持幾個機(jī)型设凹。
- Google官方Sample
- SOTER 介紹
SOTER 支持機(jī)型
SOTER SDK地址- 阿里指紋
- IFAA暫無開源
2018.12.10 更新
SOTER 已支持部分華為機(jī)型SOTER 支持機(jī)型 wiki
架構(gòu)
好了舰讹,demo 寫完了,看下了產(chǎn)品文檔围来。啥跺涤?場景這么復(fù)雜匈睁?监透!分支繁多,還需要結(jié)合到之前存在的手勢驗證功能(用戶有兩種安全方式可選:指紋驗證和手勢驗證)航唆。
業(yè)務(wù)場景有四個:
- 冷啟動app的指紋驗證
- 切換賬號登陸后的引導(dǎo)設(shè)置
- 在設(shè)置頁用戶手動開啟指紋登陸
- 設(shè)置頁手動關(guān)閉指紋登陸
每一次驗證的狀態(tài)胀蛮,都會通過 AuthenticationCallback 回調(diào),我們可以理解為是指紋驗證的生命周期糯钙。
public class MyAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
//驗證過程中遇到不可恢復(fù)的錯誤
super.onAuthenticationError(errMsgId, errString);
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
@Override
public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
//驗證過程中遇到可恢復(fù)錯誤
super.onAuthenticationHelp(helpMsgId, helpString);
}
}
onAuthenticationSucceeded 和 onAuthenticationError 的回調(diào)意味著本次的認(rèn)證結(jié)束粪狼,會根據(jù)當(dāng)前所處業(yè)務(wù)場景給予用戶不同的引導(dǎo)。
而 onAuthenticationFailed 和 onAuthenticationHelp 的情況任岸,四個業(yè)務(wù)場景都是一樣的再榄,都是在界面上提示用戶,我們可以合并一起處理享潜。
所以我們根本不需要一個業(yè)務(wù)場景就對應(yīng)一個 AuthenticationCallback 回調(diào)類困鸥,我們可以只用一個 AuthenticationCallback 回調(diào)類來根據(jù)當(dāng)前所處的業(yè)務(wù)場景分發(fā)行為。但是我又不想在 onAuthenticationSucceeded 和 onAuthenticationError 的回調(diào)中有 Switch 邏輯。所以對于四個場景各不相同的 onAuthenticationSucceeded 和 onAuthenticationError 的回調(diào)方法疾就,我們用狀態(tài)模式來分離澜术,這樣把與特定狀態(tài)相關(guān)的行為局部化,并且將不同場景下的行為分割開來猬腰。(需要給用戶什么提示鸟废,什么操作,包括驗證次數(shù)超限的處理姑荷,取決于當(dāng)前所處的場景狀態(tài))
另外一點(diǎn):需要在運(yùn)行時刻根據(jù)狀態(tài)來改變行為盒延,比如說用戶從一個正常態(tài),轉(zhuǎn)移到驗證過程異呈竺幔或者驗證過程被劫持的狀態(tài)兰英。
驗證過程異常情況,也即是說供鸠,受用戶 root 或自定制情況畦贸,通過測試的同一個機(jī)型有可能驗證過程異常。
驗證過程被劫持楞捂,因為 Google API 只返回 true 或 false薄坏,我們當(dāng)然不能無條件相信這個驗證結(jié)果,所以需要在應(yīng)用內(nèi)產(chǎn)生一對非對稱的密鑰寨闹,保證驗證過程不會被篡改胶坠。如果拿到驗證結(jié)果解密失敗,就進(jìn)入了被劫持的狀態(tài)了繁堡。
驗證過程異常和驗證被劫持的狀態(tài)基本處理一致沈善,都是屬于用戶無法再繼續(xù)驗證的場景,我們可以把這兩個狀態(tài)合為一椭蹄。按照開發(fā)的思路闻牡,有異常,被劫持绳矩,那肯定是失敗了罩润,是吧? 但是按照產(chǎn)品的思路翼馆,其他 3 個業(yè)務(wù)場景按失敗處理割以,但如果是關(guān)閉指紋的場景下(4. 設(shè)置頁手動關(guān)閉指紋登陸),就算是失敗了应媚,也要讓他去關(guān)閉成功严沥,不然可能會出現(xiàn)用戶手機(jī)中途 root 或極端情況下,無法關(guān)閉指紋中姜,從而引起客訴消玄。
按照分析我們可以發(fā)現(xiàn),被劫持和驗證過程異常的情況的處理,依賴于當(dāng)時所處的場景莱找,所以呢酬姆,我們無法把被劫持和驗證過程異常當(dāng)做一個獨(dú)立的狀態(tài)了。只能抽出作為一個公共方法奥溺。
為了不和業(yè)務(wù)邏輯耦合在一起,工具類包裝了一層浮定,主要封裝了驗證條件的判斷相满,指紋類的初始化等等,最主要的是封裝了加密類 CryptoObjectCreatorHelper 桦卒,我們考慮到安全因素立美,如果不加密的話,就意味著App 無條件信任認(rèn)證的結(jié)果方灾,這個過程可能被攻擊建蹄,數(shù)據(jù)可以被篡改,這是 App 在這種情況下必須承擔(dān)的風(fēng)險裕偿。但是這個加密過程和業(yè)務(wù)是無關(guān)的洞慎,我們不想讓 Activity 層感知到,所以密鑰和加密對象的銷毀嘿棘,會統(tǒng)一由工具類來把控劲腿。
為了安全,每次驗證過程的密鑰都不同鸟妙,驗證過程一結(jié)束焦人,也就是回調(diào) onAuthenticationSucceeded 和 onAuthenticationError 時,都需要銷毀掉密鑰重父,但是我們不想讓業(yè)務(wù)層來操作花椭,所以工具類也有自己的一個 AuthenticationCallback ,在 AuthenticationCallback 里做一些和業(yè)務(wù)無關(guān)的操作坪郭,再回調(diào) Activity 的 AuthenticationCallbackListener 个从。
工具類的 CallBack 是 FingerprintManagerCompat.AuthenticationCallback 實(shí)現(xiàn)類脉幢,業(yè)務(wù)層的 AuthenticationCallbackListener 是自定義接口歪沃,因為不想把和業(yè)務(wù)無關(guān)的往上傳遞,比如說嫌松,驗證成功的 AuthenticationResult 沪曙,驗證錯誤的 typeId,這些業(yè)務(wù)并不關(guān)心萎羔。Activity 的 AuthenticationCallbackListener 會把請求統(tǒng)一轉(zhuǎn)發(fā)給控制器 FingerPrintTypeController液走,在轉(zhuǎn)發(fā)給控制器的前后,我們可以做一些通用的業(yè)務(wù)操作,比如說停止界面的掃描動畫缘眶,發(fā)一些異步的請求等等嘱根,這個就是代理模式的應(yīng)用了。
那控制器 FingerPrintTypeController 和四個場景的關(guān)系又是如何巷懈?我們看看類圖该抒。
可以看到,四個場景顶燕,對應(yīng)四個狀態(tài)類凑保,控制器和狀態(tài)類實(shí)現(xiàn)了同一個接口,在內(nèi)部根據(jù)當(dāng)前場景轉(zhuǎn)發(fā)給對應(yīng)的類涌攻, 那怎么根據(jù)場景轉(zhuǎn)發(fā)給對應(yīng)類欧引?我們建立一個映射表,把場景和類對應(yīng)起來恳谎。每次匹配的話只要 O(1) 復(fù)雜度芝此。
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);
}
}
這個時候產(chǎn)品又說了,同樣是異常情況因痛,但是被劫持和異常過程異常的提示文案要不一樣癌蓖,ok,那我們將提示語和操作分離開來婚肆,提示和業(yè)務(wù)場景的對應(yīng)關(guān)系也預(yù)先緩存在 Map 里租副,直接 get 獲取具體提示,作為參數(shù)傳入就可以了较性。
//普通異常情況提示
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?
表現(xiàn)機(jī)型:MI 5s赞咙、vivo X9
在同一機(jī)型上調(diào)用 FingerprintManagerCompat 的 isHardwareDetected() 和 hasEnrolledFingerprints() 時候责循,返回的都是 false,但是調(diào)用 FingerprintManager 的 isHardwareDetected()
和 hasEnrolledFingerprints() 時,卻是返回 true攀操。
解決:是否符合指紋條件可以多加一層判斷院仿。
2. Letv X500 Android 6.0,API23 不按正常的套路回調(diào)
onAuthenticationError 和 onAuthenticationFailed,理論上應(yīng)該是識別失敗的情況速和,但是該機(jī)型點(diǎn)擊取消指紋識別也會先回調(diào)一次Error歹垫,如果遇到這種情況,只能根據(jù)具體項目環(huán)境中去進(jìn)行規(guī)避適配了颠放。
3. 魅族上遇到的坑
onAuthenticationHelp 回調(diào)不按套路出牌排惨,正常官網(wǎng)文檔解釋,這個方法的回調(diào)時機(jī)是在指紋認(rèn)證期間發(fā)生可恢復(fù)性的錯誤時回調(diào)碰凶。結(jié)果在魅族上暮芭,啟動指紋識別認(rèn)證的時候就會回調(diào)這個方法鹿驼,里面?zhèn)鬟f回來的信息提示是“等待按下手指”,也就是說辕宏,它的 onAuthenticationHelp 回調(diào)跟官網(wǎng)時機(jī)不一樣畜晰,而且方法的作用也變了,它在正常的情況回調(diào)了 onAuthenticationHelp瑞筐。
解決:不影響驗證流程舷蟀,無需解決
4. 小米 鎖屏和切后臺生命周期不一致
產(chǎn)品需求:用戶鎖屏或切到后臺時(onStop)自動停止指紋驗證,回到界面時(onResume)自動調(diào)起驗證面哼。
所以我在指紋回調(diào)方法中加入了標(biāo)志位 isInAuth野宜。onStop時保存 isInAuth,onResume時 isInAuth == true 則自動調(diào)起驗證魔策。
@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 鎖屏?xí)r的生命周期是 onAuthenticationError -> onStop;切到后臺是 onStop -> onAuthenticationError闯袒。導(dǎo)致不同流程下拿到 isInAuth 標(biāo)志位不一致虎敦,無法自動調(diào)起驗證。
解決:界面指紋按鈕可以手動調(diào)起驗證政敢,無需兼容處理其徙。
小米5生命周期同上,但是無論是自動還是手動調(diào)起驗證喷户,馬上就回調(diào)了 onAuthenticationError唾那,也就是說 MI5 從后臺切回來后,指紋驗證流程中斷褪尝。
解決:用一個棧來存儲調(diào)用方法順序闹获,如果驗證方法調(diào)起,馬上就回調(diào) onAuthenticationError 方法河哑,則判定是屬于兼容問題避诽,按驗證失敗來解決。
5. 密鑰解密失敗
三星SM-A9100 璃谨、Nexus 6P密鑰解密失敗
解決:暫無法解決
其他兼容解決方案:
- 三星passSdk(不過從2018下半年開始沙庐,Pass SDK 將不再提供 DEVICE_FINGERPRINT_UNIQUE_ID 。也就是不再為每個已注冊的指紋提供索引了佳吞。因此將無法通過 SDK 區(qū)分使用哪個指紋來驗證用戶拱雏。)
- 魅族 flyme開發(fā)平臺提供了指紋驗證官方api
非兼容問題
1. 新注冊指紋密鑰解密失敗
系統(tǒng)中注冊了一個新的指紋的情況下,即使指紋在系統(tǒng)指紋列表里容达,驗證也不通過古涧。
解決:刪除了當(dāng)前無效的key,然后根據(jù)參數(shù)再次生成密鑰花盐。
@Override
public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
...
/**
* doFinal方法會檢查結(jié)果是不是會攔截或者篡改過羡滑,
* 如果是的話會拋出一個異常,異常的時候都將認(rèn)證當(dāng)做是失敗來處理
*/
try {
result.getCryptoObject().getCipher().doFinal();
mCustomCallback.onAuthenticationSucceeded(true);
} catch (IllegalBlockSizeException e) {
//如果是新錄入的指紋算芯,會拋出該異常柒昏,需要重新生成密鑰對重新驗證,這里加個次數(shù)限制熙揍,避免進(jìn)入驗證異常->重新驗證->又驗證異常的死循環(huán)
if (happenCount == 0) {
beginAuthenticate();
happenCount++;
return;
}
mCustomCallback.onAuthenticationSucceeded(false);
} catch (Exception e) {
mCustomCallback.onAuthenticationSucceeded(false);
}
...
}
2. 設(shè)備已有指紋职祷,生成密鑰卻異常提示沒有指紋
非復(fù)現(xiàn),和設(shè)備無關(guān)届囚,懷疑是谷歌 API 的坑有梆。
java.lang.IllegalStateException: At least one fingerprint must be enrolled to create keys requiring user authentication for every use
解決:暫時只想到針對這個特定異常,直接使用無密鑰驗證意系,有一定的安全風(fēng)險泥耀,有更好方案歡迎補(bǔ)充。
本文完整 Demo 地址
Demo 僅供參考架構(gòu)和兼容處理蛔添,如果后續(xù)接入魅族和三星 SDK痰催,可以考慮用策略模式替換Goolge API。
更新:
Android P 引入了若干可提升應(yīng)用和運(yùn)行應(yīng)用的設(shè)備安全性的功能迎瞧。
其中一項:
我是 FeelsChaotic夸溶,一個寫得了代碼 p 得了圖,剪得了視頻畫得了畫的程序媛凶硅,致力于追求代碼優(yōu)雅缝裁、架構(gòu)設(shè)計和 T 型成長。
歡迎關(guān)注 FeelsChaotic 的簡書和掘金足绅,如果我的文章對你哪怕有一點(diǎn)點(diǎn)幫助压语,歡迎 ??!你的鼓勵是我寫作的最大動力编检!
最最重要的胎食,請給出你的建議或意見,有錯誤請多多指正允懂!