【Android】BiometricPromptと指紋認証機能の実装方法について

# 1. BiometricPrompt
`BiometricPrompt`とはAndroidPから導入されたAPIで、これは指紋認証や顔認証、音声認証、虹彩認証といった様々な生体認証をアプリ内で簡単に利用することが可能になります。

去年2018年にAndroidPとともにこのAPIが発表された際「ついに顔認証や虹彩認証もできるようになったか！」と奮起して調べたのですが、残念ながらalpha版で出来ることは限られていた印象でした。

しかし、2019/8/29にはbeta版になり`FingerprintManager`がdeprecatedになったこともあって最近触ってみたのですが、以前に比べてだいぶ成熟してきた印象を受けました。
安定版がリリースされるまではもう少し時間がかかりそうですが、この辺りで一度整理したくこの記事を書きました。

# 2. 必要APIとサポートライブラリ
`BiometricPrompt`のパッケージは`android.hardware.biometrics.BiometricPrompt`となります。
しかし、必要APILevelが28のためほとんどのアプリではまだ導入できないと思います。

これじゃ使えないじゃないかと思いましたがJetpackのAndroidXでも`BiometricPrompt`ライブラリが提供されており、こちらは例えばminSdkVersionが19でも導入可能です。
（導入が可能なだけでAndroidM以上でなければ生体認証を利用することはできません。）
パッケージは`androidx.biometrics.BiometricsPrompt`となります。

じゃあAndroidXとそうでないので何が違うんだ？という疑問になると思います。
AndroidXに入っている`BiometricPrompt`ライブラリはおそらくAndroid Developers Blogの[Better Biometrics in Android P](https://android-developers.googleblog.com/2018/06/better-biometrics-in-android-p.html)に載っている図の`BiometricPrompt Compat Library`に相当するものではないかと思われます。
<img width="600" alt="01_biometric_library.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38780/b01c8814-e1ab-4590-6f06-1bd45ad16b18.png">
引用: https://android-developers.googleblog.com/2018/06/better-biometrics-in-android-p.html

「思われます」と書いた理由は確証が取れなかったためです。
検索してみると「この図のSupport libraryに相当するライブラリがないんだけど」というStack overflowへの質問投稿がいくつか出てくるのですが、そいつはAndroidXのことだよという人とそれに相当するライブラリはまだ提供されていないという人がまちまちでした。

ただ、AndroidXはSupportLibraryに相当する役割を担っているはずなので多分この図のSupport libraryという認識で良いのかなと個人的に思っています。

参考： AndroidXの概要(https://developer.android.com/jetpack/androidx?hl=ja)

基本的には`androidx.biometrics.BiometricsPrompt`を使っていけば問題ないと思います。

# 3. BiometricPromptでの顔認証や虹彩認証の対応状況について
AndroidXの`BiometricPrompt`は[リファレンス](https://developer.android.com/reference/androidx/biometric/BiometricPrompt)を読む限りAndroidPでも顔認証や虹彩認証が利用できそうなことが書いてあります。
>On devices running P and above, this will show a system-provided authentication prompt, using a device's supported biometric (fingerprint, iris, face, etc). On devices before P, this will show a dialog prompting for fingerprint authentication.

対して[android.hardware.biometricsのリファレンス](https://source.android.com/security/biometric/)にはAndroidPは指紋認証しか対応していないと書いてあります。
>Android9 Includes fingerprint integration only for BiometricPrompt.

以下のStack overflowの質問でも顔認証できないような回答がついていました。
https://stackoverflow.com/questions/50702737/biometricprompt-faceunlock-not-works

一方でこういう質問も飛んでいるのでひょっとしたら虹彩認証は出来る？のかもしれません・・ :thinking:
https://stackoverflow.com/questions/55145785/android-biometricprompt-shows-iris-scanner-on-samsung-running-android-pie
Galaxy端末やPixel2では指紋以外の生体認証機能が搭載されているようなので試してみたかったですが、残念ながら私はNexus5XとPixel3しか持っていないため試せませんでした。
おそらく調べた限り結構な割合で「できない」としている方が多いので基本的にはまだ指紋のみなのかなと自分の中で結論付けています。

他のキャリア端末では指紋以外の生体認証を取り入れているし、何よりGoogleがなぜPixel2で採用していた顔認証をPixel3で取りやめて指紋認証のみに戻したのか疑問であったため少し理由を調べてみました。

Android Developers Blogの[Better Biometrics in Android P](https://android-developers.googleblog.com/2018/06/better-biometrics-in-android-p.html)を読むと、生体認証の採用はSAR(詐称受諾率)とIAR(なりすまし受入率)を指標に測定した強度要件に合格した認証方式のみ採用しており、指紋以外の認証の合格がなかなか難しいようです。

このSARとIARという指標は上記ブログに`we introduced two new metrics〜`と記載があることからGoogle独自の指標だと思われます。検索してもヒットした内容がほぼAndroid関連の記事でした。
生体認証で一般的に出てくる指標はFAR(本人拒否率)とFRR(他人受入率)のようですが、これらを使わず独自の指標を用意した理由も上記ブログに記載があるため興味がある方は読んでみてください。

一般人の私から見ても、確かに顔認証の場合はその人の写真や寝ているところをこっそり映されてロック解除されたという話を聞いたことがあるし、もう少しテクノロジーの進歩が必要なのかもしれませんね。

ただ、このBlogは書かれたのが2018年で、Android10は再び顔認証機能が実装されると噂されているため基準値を合格する精度まで向上させたのかもしれません。Pixel4に期待したいです。

まとめると、`BiometricPrompt`の各OS対応状況は現状で以下のような認識で良いかなと思います。

  - AndroidL以前: 生体認証利用不可（具体的には後半に書いた実装箇所を参照ください）
  - AndroidM〜AndroidP: 指紋認証のみ対応
  - AndroidQ: 指紋認証に加え、顔認証も対応予定
 
前置きがとても長くなってしまいましたが、以降はAndroidXの`BiometricPrompt`を使用し指紋認証機能を実装する方法をまとめていきます。

# 4. Biometric Promptの実装
まずお馴染みのbuild.gradleへの依存関係追加と`AndroidManifest.xml`への権限追加を行い、それからサンプルコードと共に実装方法を記載します。

注意点として、AndroidStudioなどのコード補完では必ず`androidx`の方を使ってください。
用意されているメソッドや使い方がほとんど同じですので気付きにくく、`androidx`ではない方の参照をしてしまい実装途中に「あれ？引数がおかしい・・」みたいな現象に陥ることがありました。

## 4-1. build.gradleとAndroidManifest.xml
記事を書いた時点でのバージョンでサンプルコードを載せています。
`BiometricPrompt`の現バージョンやリリースノートは以下を参照ください。
https://developer.android.com/jetpack/androidx/releases/biometric

```build.gradle
// 記事を書いた時はbeta01でしたが、2019/10/23にRC02が出たので更新しました。
implementation 'androidx.biometric:biometric:1.0.0-rc02'
```

```AndroidManifest.xml
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
```

## 4-2. 生体認証が行えるかのチェック処理
`1.0.0-beta01`では新たに`BiometricManager`が追加され、デバイスが生体認証をサポートしているかどうかをチェックできるようになりました。
https://developers-jp.googleblog.com/2019/06/whats-new-in-android-q-security.html

これまで`!FingerprintManager.hasEnrolledFingerprints && context.packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)`
などとデバイスの指紋認証ハードウェアの利用有無を自分で判定する必要がありましたが、こういったチェックは`BiometricManager.canAuthenticate()`で出来るようになりました。

注意点としては`canAuthenticate()`からの戻り値はBooleanかと思いきや`BiometricManager`の定数（Int型）なので判定処理を実装する必要があります。
戻り値の定数は[BiometricManagerのcanAuthenticateメソッドリファンレス](https://developer.android.com/reference/androidx/biometric/BiometricManager#canAuthenticate())を参照ください。実装例は以下の通りです。
なお、elseには基本来ないはずなのでExceptionを投げていますがここはAPI準拠なので議論の余地があるところかなと思います。

```kotlin
when (BiometricManager.from(context).canAuthenticate()) {
    BiometricManager.BIOMETRIC_SUCCESS -> {/* 生体認証が利用可能 */}
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {/* 生体情報が端末に登録されていない */}
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE, BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {/* 生体認証ハードウェアが利用不可 */}
    else -> throw IllegalStateException("ここには入らないはず。")
}
```
`FingerprintManager`を使用して実装していた時はRequiresApiアノテーションなどでAndroidM以上かを判定する必要があったので`Build.VERSION.SDK_INT >= Build.VERSION_CODES.M`といった判定も必要でした。
`BiometricPrompt`のメソッドはバージョン関係なく呼べるので（実際にはライブラリ内でバージョン判定して処理してくれるので）実装者は本来考えないといけないロジックに注力できるようになりました。

## 4-3. 認証処理
実装内容は大きく2つ、表示する生体認証のダイアログ設定と認証結果のコールバック実装です。

### 4-3-1. 生体認証ダイアログ
ダイアログのレイアウトは決められていていじることはできません。ダイアログに表示するタイトルや説明文、キャンセルボタンラベルといった文言といくつかの挙動の設定のみ可能です。
必須項目は`title`と、`setDeviceCredentialAllowed`or`setNegativeButtonText`となります。
`setDeviceCredentialAllowed`は、デバイスに設定しているPINやパターンなどを認証として使用するかのボタンを表示するフラグになります。デフォルトはfalseです。
実装例は以下の通りです。

```kotlin
val promptInfo = BiometricPrompt.PromptInfo
	.Builder()
	.setTitle("タイトル。設定必須項目")
	.setSubTitle("サブタイトル")
	.setDescription("説明文")
	.setNegativeButtonText("キャンセルボタン")
	.build()
```

### 4-3-2. 認証結果のコールバック
生体認証ダイアログでのユーザー操作やデバイスの生体認証ハードウェア、生体情報登録状況などによる結果をコールバックで実装します。
`BiometricPrompt`メソッドの引数は3つ「FragmentActivity/Fragment、Executor、AuthenticationCallback」となります。
ExecutorとAuthenticationCallbackについて少し説明します。

#### Executor
コールバック処理を実行するスレッドを指定します。よくサンプルでは`Executors.newSingleThreadExecutor()`でワーカースレッドを作ってそれをコールバック処理のハンドラに当てています。この記事のサンプルコードも同じ方法をとりました。
当然ですがコールバックの中でUIを操作する処理を入れたい場合はワーカースレッドだとダメなのでメインスレッドを当ててやる必要があります。

#### AuthenticationCallback
コールバックを実装します。overrideするメソッドは3つで`onAuthenticationSucceeded`は名前通り成功、`onAuthenticationError`と`onAuthenticationFailed`はエラー時です。
`onAuthenticationError`と`onAuthenticationFailed`の違いは回復可能なエラーかそうでないかの違いです。
回復可能なエラーは連続して生体認証が行えますが、回復不能なエラーはダイアログが閉じてしまいます。
指紋認証を前提にしますが、例えば以下のような場合でそれぞれ呼ばれることを確認しました。

  - `onAuthenticationError`(回復不能なエラー)
    - 端末に指紋認証ハードウェアが搭載されていない
    - 端末に指紋認証ハードウェアは搭載されているが指紋が1つも登録されていない
    - 指紋認証を一定回数間違えてロック状態になっている
    - ユーザーが生体認証ダイアログ外をタップしてダイアログを閉じた（キャンセル扱い）
    - キャンセルボタン（NegativeButton）をタップした
    - などなど...
  - `onAuthenticationFailed`(回復可能なエラー)
    - 別の指や指に埃などが付いていて認証に失敗した、など

`onAuthenticationError`はエラー内容がErrorCodeで詳細に判別可能です。アプリの作りに合わせて必要なエラー情報を取得しユーザーにフィードバックするのが良いと思います。詳細は[リファレンスのConstants](https://developer.android.com/reference/androidx/biometric/BiometricPrompt)をご確認ください。
実装例は以下の通りです。

```kotlin
val executor = Executors.newSingleThreadExecutor()
val biometricPrompt = BiometricPrompt(activity, executor, object: BiometricPrompt.AuthenticationCallback() {
	override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
		// 成功した場合の処理
	}
	override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
		// エラーになった場合の処理。ダイアログが閉じる。errorCodeで切り分ける
	}
	override fun onAuthenticationFailed() {
		// 認証に失敗した場合の処理。ダイアログは閉じない。
	}
})

// 「4-3-1. 生体認証ダイアログ」で作ったPromptInfoを指定。この処理が実行されるとダイアログが表示される。
biometricPrompt.authenticate(promptInfo)
```

# 5. AndroidOS毎の挙動の違い
冒頭でも書いた通り`BiometricPrompt`はAndroidPから導入されたAPIです。
じゃあ`BiometricPrompt`でこれまで紹介した指紋認証機能を実装し、AndroidO以下で実行した場合にどうなるのでしょうか？
自分が試した結果をそれぞれ書いていきます。

1. AndroidP以上
`BiometricPrompt`をサポートしているので良い感じのダイアログが表示されます。
全部こういう感じでダイアログが出てくれると嬉しかったのですが。。
<img width="250" alt="04_dialog_androidP.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38780/cc36809f-1344-817f-3eab-24cbb7f75c63.png">

2. AndroidM〜AndroidO
これらのバージョンは`BiometricPrompt`をサポートしていませんがAndroidXのライブラリを使えば旧FingerPrintの機能を提供してくれます。
指紋認証ダイアログは普通な感じになってしまいますが中央の指紋などはちゃんとアニメーションします。
<img width="250" alt="03_dialog_androidM.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38780/23a23956-e5d9-27a9-99be-300d2908a572.png">

3. AndroidM未満の場合
公式では指紋認証をはじめ生体認証をサポートしていないためダイアログ自体が出てきません。
`BiometricPrompt.authenticate()`を実行するとコールバックの`onAuthenticationError`が呼ばれます。
なお、表示されている画面はサンプル用に作ったアプリで`BiometricPrompt.authenticate()`を実行しているActivityです。
<img width="250" alt="02_dialog_androidL.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38780/aeaa087f-ef1c-0206-670f-ad411e8cfc93.png">

# 6. まとめ
個人的にはこれまでの`FingerprintManager`より圧倒的に簡単で実装しやすい印象でした。
`FingerprintManager`はdeprecatedになりましたし、今後、顔認証や虹彩認証、音声認証などがサポートされてくると`BiometricPrompt`の需要が高まるのではないかなと思います。

なお、この記事では説明のためにコードを抜粋したり省略しています。この記事用に作成したサンプルアプリは[GitHubのリポジトリ](https://github.com/hotdrop/biometric-prompt-sample)にあげましたので参考にしてみてください。