通過 Android keystore 和 fingerprint 結合實現資料加密和解密
本文目標是通過結合 Android Keystore 和 Fingerprint 來安全的對資料進行加密,並且能夠通過指紋身份驗證之後對資料進行解密。
瞭解 Keystore
Android Keystore 系統可以在一個安全的容器中(如:藉助於系統晶片中提供的可信執行環境 TEE)儲存加密金鑰,在我們的加密金鑰進入 Keystore 之後,可以在不用匯出金鑰的前提下完成加密操作,Keystore 支援的操作有:
- 生成金鑰
- 匯入和匯出非對稱金鑰
- 匯入原始對稱金鑰
- 使用適當的填充模式(padding modes)進行非對稱加密和解密
- 使用摘要和適當的填充模式(padding modes)進行非對稱簽名和驗證
- 以適當模式(包括 AEAD 模式)進行對稱加密和解密
- 生成和驗證對稱訊息驗證碼(Message Authentication Codes, MAC)
另外,我們可以結合身份驗證系統(如指紋驗證)來實現只有在使用者完成身份驗證之後才能使用金鑰,即可以讓應用指定金鑰的授權使用方式,一旦生成或匯入金鑰,授權方式將無法更改,並且之後每次需要使用金鑰時,都會由 Android Keystore 庫強制執行授權。
本文的程式碼實現主要基於 Android 6.0 以上,因為從 Android 6.0 開始不僅增加了對稱加密演算法(AES 和 HMAC)的支援,還增加了針對硬體支援的金鑰訪問控制系統,訪問控制的邏輯會在金鑰生成期間指定,並在整個生命週期內被強制執行。
其他參考:
我們下面就接著先看下 Android 的身份驗證。
身份驗證
在 Android 中,主要通過 Gatekeeper(用於 PIN 碼/解鎖圖案/密碼身份驗證)和 Fingerprint(用於指紋身份驗證)來實現身份驗證。
並且 Gatekeeper 和 Fingerprint 會通過與 Keystore 元件進行溝通身份驗證的狀態。
從 Android 9 開始使用 BiometricPrompt 統一封裝了指紋和其他生物識別技術。
下圖展示了 Fingerprint,Keystore 等等一起參與的整個的身份驗證流程:

- 使用者可以通過 PIN/Pattern/Password 或 指紋 方式進行驗證:
- 對於 PIN/解鎖圖案/密碼,主要通過 LockSettingsService 向 gatekeeperd 發出驗證請求;
- 對於 指紋,會通過 FingerprintService 向 fingerprintd 發起驗證請求;
- gatekeeperd/fingerprintd 守護程序會將待驗證資料傳送至 TEE 中的副本 gatekeeper/fingerprint,並由相關副本生成 AuthToken(並使用了 AuthToken HMAC 簽名)併發送到 gatekeeperd/fingerprintd 守護程序;
- 守護程序 gatekeeperd/fingerprintd 收到經過簽名的 AuthToken 之後,再通過 KeyStore 服務 Binder 介面的擴充套件程式將 AuthToken 傳入 Keystore Service;
- Keystore Service 將 AuthToken 傳給 TEE 中的 Keymaster,並使用與 gatekeeper 和 fingerprint 共用的金鑰進行驗證;
關於 Fingerprint
下面我們具體看下指紋,現在很多的手機上都已配備有指紋感測器,在這些手機上我們可以註冊一個或多個指紋,然後使用指紋來解鎖手機或執行其他任務(例如:很多涉及轉賬類的 App 中的免密支付功能),Android 通過利用 Fingerprint HAL 來連線到供應商的專用庫(FP vendor library)和指紋感測器。
指紋匹配的流程是:
- 使用者將“註冊”過的手指放在指紋感測器上;
- 供應商專用庫會根據當前已註冊的指紋模板集進行匹配;
- 匹配結果傳至 Fingerprint HAL,然後進一步將指紋身份驗證結果通知給 Fingerprint 守護程序 fingerprintd;
指紋身份驗證的概要互動資料流程如下圖:

- 每個應用都可以有一個 FingerprintManager 例項,該例項負責與 FingerprintService 進行通訊;而 FingerprintService 為在系統中執行的單例服務,負責與 fingerprintd 守護進行通訊;
- fingerprintd 守護程序會封裝和呼叫 Fingerprint HAL 供應商的專用庫,來實現指紋的註冊、驗證等等操作,如下圖:

回到示例
上面大致瞭解了 Keystore,身份驗證流程以及指紋識別匹配的流程,下面看下如何結合 Keystore 和 Fingerprint 來實現資料的加密和解密:
生成(對稱)金鑰
首先我們需要通過 Keystore 庫來生成(用於後續對資料進行加密的)金鑰,這裡採用 AES 加密演算法,並且設定這個金鑰為強制授權訪問方式。
這裡通過 KeyStore 和 KeyGenerator 類,以及 AndroidKeyStore 提供程式來生成金鑰:
public void generateKey(View view) { // AES + CBC + PKCS7 final KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); mKeyStore.load(null); generator.init(new KeyGenParameterSpec.Builder("FirstWallet", KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setUserAuthenticationRequired(true) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .build()); // Generate (symmetric) key, and store to KeyStore final SecretKey sk = generator.generateKey(); Toast.makeText(this, String.format("Generate key success %s", sk.getAlgorithm()), Toast.LENGTH_LONG).show(); }
在生成的過程中設定了要求使用者身份驗證的選項,後續步驟中的加密和解密步驟我們可以通過指紋識別來完成使用者身份驗證並通過相應的金鑰進行加解密。
其中 mKeyStore Provider 在初始時已經生成,如下程式碼:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); try { // Register provider mKeyStore = KeyStore.getInstance("AndroidKeyStore"); } catch (KeyStoreException e) { e.printStackTrace(); finish(); return; } // Fingerprint service mFpManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE); if (null == mFpManager) { finish(); return; } ... }
這裡的 AndroidKeyStore 是各個 App 需要訪問 Keystore 功能的 Android Framework API 和元件,也是作為 JCA API 的擴充套件程式實現的,是在各個 App 自有程序空間中執行的,
另外,這裡也獲取了與 FingerprintService 互動的 FingerprintManager 例項。
指紋加密
通過指紋識別驗證通過後,就可以使用儲存在 Keystore 中對應的 key (通過生成時指定的 key 名稱獲取)對內容進行加密:
public void encryptData(View view) throws CertificateException, NoSuchAlgorithmException, IOException, UnrecoverableKeyException, KeyStoreException, NoSuchPaddingException, InvalidKeyException { mKeyStore.load(null); final SecretKey sk = (SecretKey) mKeyStore.getKey("FirstWallet", null); if (null == sk) { Toast.makeText(this, "Can not get key", Toast.LENGTH_LONG).show(); return; } final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); cipher.init(Cipher.ENCRYPT_MODE, sk); // Need authenticate by fingerprint final FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher); mFpManager.authenticate(cryptoObject, null, 0, new FingerprintManager.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, CharSequence errString) { super.onAuthenticationError(errorCode, errString); Toast.makeText(MainActivity.this, "Fp auth error: " + errString, Toast.LENGTH_LONG).show(); } @Override public void onAuthenticationHelp(int helpCode, CharSequence helpString) { super.onAuthenticationHelp(helpCode, helpString); } @Override public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { super.onAuthenticationSucceeded(result); Toast.makeText(MainActivity.this, "Fp auth succ", Toast.LENGTH_LONG).show(); // Encrypt data by cipher final String plainText = etPlain.getText().toString(); final Cipher cipher = result.getCryptoObject().getCipher(); try { byte [] encrypted = cipher.doFinal(plainText.getBytes()); mIV = cipher.getIV(); final String encryptedWithBase64 = Base64.encodeToString(encrypted, Base64.URL_SAFE); tvEncrypted.setText(encryptedWithBase64); } catch (IllegalBlockSizeException | BadPaddingException e) { e.printStackTrace(); } } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); Toast.makeText(MainActivity.this, "Fp auth failed", Toast.LENGTH_LONG).show(); } }, new Handler()); }
在上面程式碼中:
- 加密演算法採用 AES+CBC+PKCS7;
- mFpManager.authenticate 等待使用者驗證指紋;
- 在指紋驗證通過後回撥 onAuthenticationSucceeded 方法,並且在輸入引數 AuthenticationResult result 中攜帶加密演算法物件,通過此加密演算法物件完成加密工作;
- 這裡還通過 cipher.getIV() 儲存了本次加密時的初始化向量(IV);
指紋解密
在指紋驗證通過後通過相同的金鑰對上面加密的內容進行解密。
public void decryptWithFingerprint(View view) { mKeyStore.load(null); final SecretKey sk = (SecretKey) mKeyStore.getKey("FirstWallet", null); if (null == sk) { Toast.makeText(this, "Can not get key", Toast.LENGTH_LONG).show(); return; } final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); cipher.init(Cipher.DECRYPT_MODE, sk, new IvParameterSpec(mIV)); // First need authenticate by fingerprint final FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher); mFpManager.authenticate(cryptoObject, null, 0, new FingerprintManager.AuthenticationCallback() { @Override public void onAuthenticationError(int errorCode, CharSequence errString) { super.onAuthenticationError(errorCode, errString); Toast.makeText(MainActivity.this, "Fp auth error: " + errString, Toast.LENGTH_LONG).show(); } @Override public void onAuthenticationHelp(int helpCode, CharSequence helpString) { super.onAuthenticationHelp(helpCode, helpString); } @Override public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { super.onAuthenticationSucceeded(result); Toast.makeText(MainActivity.this, "Fp auth succ", Toast.LENGTH_LONG).show(); // Decrypt data by cipher final String encryptedWithBase64 = tvEncrypted.getText().toString(); final byte [] encryptedBytes = Base64.decode(encryptedWithBase64, Base64.URL_SAFE); final Cipher cipher = result.getCryptoObject().getCipher(); try { byte [] decryptedBytes = cipher.doFinal(encryptedBytes); String decryptedText = new String(decryptedBytes); tvDecrypted.setText(decryptedText); } catch (IllegalBlockSizeException | BadPaddingException e) { e.printStackTrace(); } } @Override public void onAuthenticationFailed() { super.onAuthenticationFailed(); Toast.makeText(MainActivity.this, "Fp auth failed", Toast.LENGTH_LONG).show(); } }, new Handler()); }
- 解密步驟中的 cipher 例項初始化時需要指定與加密時相同的 IV;