AndroidXの生体認証(BiometricPrompt)の使い方

TL; DR

  • AndroidXの生体認証コンポネントBiometricPromptが正式リリースされてv1.0になった!
  • サポート対象はAndroid6.0以上!
  • FingerPrintManagerは非推奨になった!
  • ↓みたいな画面が簡単に作れる!

f:id:xterm256color:20191202215111p:plain

目次

はじめに

2019/11/7のリリースで生体認証コンポネントのバージョンが1.0になってたので使ってみました。UIも含めて簡単に利用できるようになってるのでBiometricPromptの利用方法をシェアします。

ちなみにこれまではFingerprintManagerというコンポネントがありましたが今は非推奨です。BiometricPromptを利用するようにとリファレンスに書かれています。

FingerprintManagerのかわりにBiometricPromptが導入された背景は、生体認証をカスタムしてある端末であってもアプリ開発者が意識せずに実装できるようにというGoogleの優しさです。興味のある方はAndroid Developers Blogのこのポストを読んでみてください。

開発環境

実装方法

できるだけシンプルな実装で解説します。

依存関係の追加

app/build.gradle を編集します。

dependencies {
    ...(省略)...

    def biometric_version = "1.0.0"
    implementation "androidx.biometric:biometric:$biometric_version"
}

生体認証が使えるかのチェック

BiometricManager#canAuthenticate()を使って生体認証が使えるかチェックする関数を実装します。生体認証が利用できないときは原因を返り値で区別することが出来ます。詳しくは次のセクションを読んでください。

fun checkBiometric(): Boolean {
    val biometricManager = BiometricManager.from(context!!)
    return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
}

生体認証APIの呼び出し

上で実装したcheckBiometric()をパスしたら、BiometricPrompt#authenticate()を使って生体認証プロンプトを起動します。 BiometricPrompt#AuthenticationCallback() には認証成功時、失敗時のメソッドを実装できます。

  • onAuthenticationSucceeded - ユーザーが認証されたときに呼び出されます。
  • onAuthenticationError - 回復不能なエラーが発生したときに呼び出されます。原因はエラーコードで区別することが出来ます。詳しくは次のセクションを読んでください。
  • onAuthenticationFailed - 回復可能な失敗が発生したときに呼び出されます。
fun authenticate() {
    // 生体認証の利用可否をチェックします。
    if (!checkBiometric()) {
        Snackbar.make(
            view!!,
            "This device can't be authenticated with biometric",
            Snackbar.LENGTH_INDEFINITE
        ).show()
        return
    }

    // BiometricPromptにわたすExecutor。
    // Android Pより前ではActivity#getMainExecutorが使えないので分岐があります。
    val mainExecutor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        activity!!.mainExecutor
    } else {
        object: Executor {
            val handler = Handler(Looper.getMainLooper())
            override fun execute(command: Runnable) { handler.post(command) }
        }
    }

    val biometricPrompt = BiometricPrompt(
        this,
        mainExecutor,
        object: BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                // 認証が成功。次の画面への遷移させます。
                findNavController().navigate(R.id.action_startFragment_to_mainFragment)
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                // 回復不能なエラーが発生。スナックバーを表示しておきます。
                Snackbar.make(
                    view!!,
                    "An error occurred during biometric authentication",
                    Snackbar.LENGTH_INDEFINITE
                ).show()
            }

            override fun onAuthenticationFailed() {
                // 回復不能なエラーが発生。認証を続けることができるのでロギングだけしておきます。
                Log.e("StartFragment", "Failed to authenticate with biometric information")
            }
        })


    // 生体認証のプロンプトに表示するタイトルとキャンセルボタンのテキストの設定。
    val info = BiometricPrompt.PromptInfo.Builder()
        .setTitle(context!!.getString(R.string.title_biometric_authentication))
        .setNegativeButtonText(context!!.getText(R.string.cancel))
        .build()

    // 生体認証の実行
    biometricPrompt.authenticate(info)
}

初回起動時に設定

画面が表示されたときと、ボタンが押されたときに生体認証が立ち上がるようにしておきます。

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    authenticate()
    button_show_authentication.setOnClickListener { authenticate() }
}

エラーの区別

ここまではなるべくシンプルな実装を書いてきました。

しかし実際の製品ではエラーが起こった際に、生体認証情報が登録されてないケースや、生体認証デバイスがそもそも存在しないケースで処理を変えたいというのが現実的でしょう。(たとえば試行回数が多すぎてエラーが発生した時は、しばらく待つようにユーザーに伝えるなど)

BiometricManager#canAuthenticate()ではその返り値で、BiometricPrompt#authenticate()ではBiometricPrompt#AuthenticationCallback#onAuthenticationError()に渡ってくるエラーコードでそれぞれエラーの原因を特定することが出来ます。

下にエラーコードの一覧をまとめたのでそれぞれのアプリを実装するときの参考にしてください。

生体認証の利用可否

返り値 概要
BIOMETRIC_SUCCESS エラーは検出されませんでした。
BIOMETRIC_ERROR_HW_UNAVAILABLE ハードウェアは利用できません。 あとでもう一度試してみてください。
BIOMETRIC_ERROR_NONE_ENROLLED ユーザーには生体認証が登録されていません。
BIOMETRIC_ERROR_NO_HARDWARE 生体認証ハードウェアはありません。

生体認証の回復不能なエラー

エラーコード 概要
ERROR_HW_UNAVAILABLE ハードウェアは利用できません。あとでもう一度試してみてください。
ERROR_UNABLE_TO_PROCESS センサーが現在の画像を処理できなかったときに返されるエラー状態。
ERROR_TIMEOUT 現在のリクエストの実行時間が長すぎる場合に返されるエラー状態。これは、プログラムが生体認証センサーを無期限に待機するのを防ぐことを目的としています。タイムアウトはプラットフォームおよびセンサー固有ですが、通常は30秒程度です。
ERROR_NO_SPACE 登録などの操作に対して返されるエラー状態。操作を完了するのに十分なストレージが残っていないため、操作を完了できません。
ERROR_CANCELED 生体認証センサーが利用できないため、操作はキャンセルされました。たとえば、これは、ユーザーが切り替えられたとき、デバイスがロックされたとき、または別の保留中の操作によって防止または無効化されたときに発生する可能性があります。
ERROR_UNABLE_TO_REMOVE ※ライブラリ内部エラー
ERROR_LOCKOUT 試行回数が多すぎるためAPIロックアウトされたため、操作はキャンセルされました。これは、5回失敗した後に発生し、30秒間続きます。
ERROR_VENDOR 上記のカテゴリのいずれにも該当しない条件がある場合、ハードウェアベンダーはこのリストを拡張できます。 ベンダーは、これらのエラーのエラー文字列を提供する責任があります。
これらのメッセージは通常、登録などの内部操作用に予約されていますが、他の方法ではカバーされないベンダーエラーを表すために使用される場合があります。 エラーが発生した場合、アプリケーションはエラーメッセージ文字列を表示することが期待されますが、デバイスおよびベンダー固有であるため、メッセージIDに依存しないことをお勧めします。
ERROR_LOCKOUT_PERMANENT ERROR_LOCKOUTが何度も発生したため、操作はキャンセルされました。 ユーザーが強力な認証(PIN /パターン/パスワード)でロックを解除するまで、生体認証は無効になります
ERROR_USER_CANCELED ユーザーが操作をキャンセルしました。 これを受信すると、アプリケーションは代替認証(パスワードなど)を使用する必要があります。 アプリケーションは、「を使用」ボタンなど、生体認証に戻る手段も提供する必要があります。
ERROR_NO_BIOMETRICS ユーザーには生体認証が登録されていません。
ERROR_HW_NOT_PRESENT バイスには生体認証センサーがありません。
ERROR_NEGATIVE_BUTTON ユーザーがネガティブボタンを押した。
ERROR_NO_DEVICE_CREDENTIAL バイスには、ピン、パターン、またはパスワードが設定されていません。
ERROR_VENDOR_BASE ※ライブラリ内部エラー

生体認証が利用できないときのフォールバック処理

下記のように実装すると、生体認証が扱えない場合のフォールバック処理としてPIN/Pattern/Passwordの認証を代わりに使うことも出来ます。 ただしBiometricPrompt#PromptInfo#Builder#setNegativeButtonText()と同時に使えないことに注意してください。

val info = BiometricPrompt.PromptInfo.Builder()
    .setTitle(context!!.getString(R.string.title_biometric_authentication))
    .setDeviceCredentialAllowed(true)
    .build()

サンプルアプリ

下のRepositoryに実行可能なアプリを置いてあります。シンプルな実装を見たい場合はsimplifyブランチに切り替えてみてください。

github.com

免責

今回はPixel3の指紋認証でしか試していないので、Pixel4などの顔認証でも同じAPIが使えるか未確認です。(ただ@hotdrop_77さんによるとPixel4の顔認証もOKのようです⇓)

参考