Android Fingerprint API:指紋認証を添付します

こんにちは、Habr! Android向けFingerprint APIが登場してからかなりの時間が経過し、その実装と使用のためにネットワーク上にさまざまなコードのサンプルが多数ありますが、何らかの理由でこのトピックはHabréでバイパスされました。 私の意見では、この誤解を修正する時が来ました。 猫に興味がある人は誰でも聞いてください。







最短の教育プログラム



では、指紋APIとは何ですか? APIを使用すると、ユーザーは明らかに指紋を介して認証できます。 センサーを操作するために、APIはFingerprintManagerを提供します。これは非常に簡単に学習できます。



使い方は? しかし、これはすでに興味深いものです。 パスワード認証が必要とされるほぼすべての場所で、指紋認証をねじ込むことができます。 LoginActivityMainActivityで構成されるアプリケーションを想像してください。 起動時に、ログイン画面が表示され、PINコードを入力して、データに移動します。 しかし、ピンによる入力を指紋による入力に置き換えたいと思います。 ちなみに、完全に置き換えることはできません。以前に保存したPINコード(パスワードをサーバーに送信する必要があるクライアントサーバーアプリケーションを意味する)を使用して、ユーザーがPINコードを手動で入力するのを防ぐことができます。



始めましょう。



センサーはどこにありますか?



新しいAPIから利益を得るには、最初にマニフェストに権限を追加する必要があります。



<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
      
      





もちろん、指紋APIは、それをサポートするデバイスでのみ使用できます。したがって、これらはセンサー付きのAndroid 6以降のデバイスです。



互換性は、メソッドを使用して簡単に確認できます。



 public static boolean checkFingerprintCompatibility(@NonNull Context context) { return FingerprintManagerCompat.from(context).isHardwareDetected(); }
      
      





FingerprintManagerCompatは、通常のFingerprintManagerの便利なラッパーです。APIバージョンチェックをカプセル化することにより、デバイスの互換性テストを簡素化します。 この場合、 isHardwareDetected()は、APIが23未満の場合にfalseを返します



次に、センサーを使用する準備ができているかどうかを理解する必要があります。 これを行うには状態の列挙を定義します。



 public enum SensorState { NOT_SUPPORTED, NOT_BLOCKED, //     ,    NO_FINGERPRINTS, //      READY }
      
      





そして、メソッドを使用します:



 public static SensorState checkSensorState(@NonNull Context context) { if (checkFingerprintCompatibility(context)) { KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE); if (!keyguardManager.isKeyguardSecure()) { return SensorState.NOT_BLOCKED; } FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); if (!fingerprintManager.hasEnrolledFingerprints()) { return SensorState.NO_FINGERPRINTS; } return SensorState.READY; } else { return SensorState.NOT_SUPPORTED; } }
      
      





コードは非常に簡単です。 わずかな誤解により、デバイスがロックされているかどうかを確認する瞬間が生じることがあります。 Androidでは保護されていないデバイスへの指紋の追加は許可されていませんが、一部のメーカーはこれをバイパスしているため、安全性を損なうことがないため、このチェックが必要です。



さまざまな状態を使用して、ユーザーに何が起こっているのかを理解させ、真の道筋に導くことができます。



準備する



そのため、ピンコードの有効性の確認に焦点を当てずに、次の単純化されたアクションロジックを推定しましょう。





スキームが1つの目的に当てはまらない場合、このスキームは十分に単純です。Googleは、プライベートユーザーデータをクリアテキストで保存しないことを強くお勧めします。 したがって、ストレージと使用のために、それぞれ暗号化と復号化のメカニズムが必要です。 やってみましょう。



暗号化と復号化に必要なもの:



  1. 安全なキーストア。
  2. 暗号化キー。
  3. 暗号作成者


保管



指紋を処理するために、システムはキーストアである「AndroidKeyStore」を提供し、不正アクセスに対する保護を保証します。 私たちはそれを使用します:



 private static KeyStore sKeyStore; private static boolean getKeyStore() { try { sKeyStore = KeyStore.getInstance("AndroidKeyStore"); sKeyStore.load(null); return true; } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { e.printStackTrace(); } return false; }
      
      





キーストアは暗号化キーのみを保存することを受け入れ、理解し、許容する必要があります 。 パスワード、PIN、その他の個人データはそこに保存できません。



キー



選択できるオプションは2つあります。対称キーと、公開キーと秘密キーのペアです。 UXの理由から、カップルを使用します。 これにより、指紋入力とPIN暗号化を分離できます。



キーストアからキーを取得しますが、まずキーをそこに配置する必要があります。 キーを作成するには、 ジェネレーターを使用します。



 private static KeyPairGenerator sKeyPairGenerator; private static boolean getKeyPairGenerator() { try { sKeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); return true; } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); } return false; }
      
      





初期化中に、生成されたキーがどのキーストアに行くか、このキーがどのアルゴリズムに向けられているかを示します。



生成自体は次のとおりです。



 private static boolean generateNewKey() { if (getKeyPairGenerator()) { try { sKeyPairGenerator.initialize(new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .setUserAuthenticationRequired(true) .build()); sKeyPairGenerator.generateKeyPair(); return true; } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); } } return false; }
      
      





ここでは、2つの場所に注意する必要があります。





次のようにキーを確認します。



 private static boolean isKeyReady() { try { return sKeyStore.containsAlias(KEY_ALIAS) || generateNewKey(); } catch (KeyStoreException e) { e.printStackTrace(); } return false; }
      
      





暗号作成者



Javaでの暗号化と復号化は、 Cipherオブジェクトによって行われます。



それを初期化します:



 private static Cipher sCipher; private static boolean getCipher() { try { sCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); return true; } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); } return false; }
      
      





引数のHell Hashは、 algorithmブレンドモード、およびpaddingを含む変換文字列です。



Cipherを受け取った後、作業のために準備する必要があります。 キーを生成するときに、暗号化と復号化にのみ使用することを示しました。 したがって、 Cipherは次の目的にも使用されます。



 private static boolean initCipher(int mode) { try { sKeyStore.load(null); switch (mode) { case Cipher.ENCRYPT_MODE: initEncodeCipher(mode); break; case Cipher.DECRYPT_MODE: initDecodeCipher(mode); break; default: return false; //this cipher is only for encode\decode } return true; } catch (KeyPermanentlyInvalidatedException exception) { deleteInvalidKey(); } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | InvalidAlgorithmParameterException e) { e.printStackTrace(); } return false; }
      
      





initDecodeCipher()およびinitEncodeCiper()は次のとおりです。



 private static void initDecodeCipher(int mode) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeyException { PrivateKey key = (PrivateKey) sKeyStore.getKey(KEY_ALIAS, null); sCipher.init(mode, key); }
      
      





 private static void initEncodeCipher(int mode) throws KeyStoreException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException { PublicKey key = sKeyStore.getCertificate(KEY_ALIAS).getPublicKey(); PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm()).generatePublic(new X509EncodedKeySpec(key.getEncoded())); OAEPParameterSpec spec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT); sCipher.init(mode, unrestricted, spec); }
      
      





暗号化された暗号は、初期化するのが少し複雑であることが簡単にわかります。 これはGoogle自体の大枠であり、その本質は公開鍵がユーザーの確認を必要とすることです。 キーキャスト( 松葉杖 、ええ)でこの要件を回避します。



KeyPermanentlyInvalidatedExceptionが発生した瞬間-何らかの理由でキーを使用できない場合、この例外が発生します。 考えられる理由は、既存の指紋に新しい指紋を追加する、ロックを変更する、または完全に削除することです。 その後、キーは保存する意味がなくなり、削除します。



 public static void deleteInvalidKey() { if (getKeyStore()) { try { sKeyStore.deleteEntry(KEY_ALIAS); } catch (KeyStoreException e) { e.printStackTrace(); } } }
      
      





トレーニングチェーン全体を収集する方法:



 private static boolean prepare() { return getKeyStore() && getCipher() && isKeyReady(); }
      
      





暗号化と復号化



文字列引数を暗号化する方法を説明します。



 public static String encode(String inputString) { try { if (prepare() && initCipher(Cipher.ENCRYPT_MODE)) { byte[] bytes = sCipher.doFinal(inputString.getBytes()); return Base64.encodeToString(bytes, Base64.NO_WRAP); } } catch (IllegalBlockSizeException | BadPaddingException exception) { exception.printStackTrace(); } return null; }
      
      





その結果、アプリケーションの設定に安全に保存できるBase64文字列を取得します。



復号化には、次の方法を使用します。



 public static String decode(String encodedString, Cipher cipherDecrypter) { try { byte[] bytes = Base64.decode(encodedString, Base64.NO_WRAP); return new String(cipherDecrypter.doFinal(bytes)); } catch (IllegalBlockSizeException | BadPaddingException exception) { exception.printStackTrace(); } return null; }
      
      





ああ、彼は入力で暗号化された文字列だけでなく、 Cipherオブジェクトも受け取ります。 どこから来たのかは後で明らかになります。



間違った指



センサーを最終的に使用するには、 FingerprintManagerCompatメソッドを使用する必要があります。



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





ハンドラーとフラグは今は必要ありません。信号は指紋読み取りモードをキャンセルするために使用されます(アプリケーションを最小化する場合など)。コールバックは特定の読み取りの結果を返しますが、暗号オブジェクトについて詳しく見ていきましょう。



この場合のCryptoObjectは、 Cipherのラッパーとして使用されます。 取得するには、次のメソッドを使用します。



 public static FingerprintManagerCompat.CryptoObject getCryptoObject() { if (prepare() && initCipher(Cipher.DECRYPT_MODE)) { return new FingerprintManagerCompat.CryptoObject(sCipher); } return null; }
      
      





コードからわかるように、暗号オブジェクトはCipherの 復号化から作成されます。 このCipherがすぐにdecode()メソッドに送信されると、確認なしでキーを使用しようとしていることを通知する例外がスローされます。



厳密に言えば、暗号オブジェクトを作成し、これをまさに確認を得るためにauthenticate()入力に送信します。



getCryptoObject()nullを返した場合、それはChiperの初期化中にKeyPermanentlyInvalidatedExceptionが発生したことを意味します。 指紋入力が利用できないことと、PINコードを再入力する必要があることをユーザーに知らせること以外は、何もする必要はありません。



すでに述べたように、 コールバックメソッドでセンサーを読み取った結果を取得します 。 それらは次のようになります。



 @Override public void onAuthenticationHelp(int helpCode, CharSequence helpString) { // ,    //  helpString    } @Override public void onAuthenticationFailed() { // ,    } @Override public void onAuthenticationError(int errorCode, CharSequence errString) { //    (5) //        (30 ) } @Override public void onAuthenticationSucceeded(@NonNull FingerprintManagerCompat.AuthenticationResult result) { //   }
      
      





認識に成功した場合、 AuthenticationResultを取得し、そこからすでに確認済みのキーを持つCipherオブジェクトを取得できます。



 result.getCryptoObject().getCipher()
      
      





これで、明確な良心をもって、それをdecode()入力に送信し、PINコードを取得し、その入力をシミュレートし、そのデータをユーザーに表示できます。



今日は以上です。コメント、コメント、提案、質問は大歓迎です。

githubでコードの最も単純なバージョンを見ることができます。



ご清聴ありがとうございました。



All Articles