生体認証ダイアログを表示する

アプリ内の機密情報やプレミアム コンテンツを保護する方法として、顔認証や指紋認証などの生体認証をリクエストする方法があります。このガイドでは、アプリ内で生体認証ログインフローをサポートする方法について説明します。

一般に、デバイスでの初回ログインには認証情報マネージャーを使用する必要があります。その後の再承認は、生体認証プロンプトまたは認証情報マネージャーで行うことができます。生体認証プロンプトを使用するメリットは、より多くのカスタマイズ オプションが提供されることです。認証情報マネージャーは、両方のフローで単一の実装を提供します。

アプリがサポートする認証の種類を宣言する

アプリがサポートする認証の種類を定義するには、BiometricManager.Authenticators インターフェースを使用します。次の種類の認証を宣言できます。

BIOMETRIC_STRONG
クラス 3 の生体認証を使用する認証。クラスは Android 互換性定義のページで定義されています。
BIOMETRIC_WEAK
クラス 2 の生体認証を使用する認証。クラスは Android 互換性定義のページで定義されています。
DEVICE_CREDENTIAL
画面ロックの認証情報(ユーザーの PIN、パターン、パスワード)を使用する認証。

認証システムの使用を開始するため、ユーザーは PIN、パターン、またはパスワードを作成する必要があります。まだ作成していないユーザーは、生体認証登録フローで作成するように求められます。

アプリが受け入れる生体認証のタイプを定義するには、単一の認証タイプ、またはビット単位で組み合わせた複数のタイプを setAllowedAuthenticators() メソッドに渡します。次のコード スニペットは、クラス 3 の生体認証または画面ロックの認証情報を使用した認証をサポートする方法を示しています。

Kotlin

// Lets the user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        .build()

Java

// Lets user authenticate using either a Class 3 biometric or
// their lock screen credential (PIN, pattern, or password).
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
        .build();

Android 10(API レベル 29)以前では、認証システムタイプ DEVICE_CREDENTIALBIOMETRIC_STRONG | DEVICE_CREDENTIAL の組み合わせはサポートされません。Android 10 以前で PIN、パターン、またはパスワードの有無を確認するには、KeyguardManager.isDeviceSecure() メソッドを使用します。

生体認証を利用できるかチェックする

アプリがサポートする認証要素を決定したら、その要素が使用可能かどうかを確認します。これを行うには、setAllowedAuthenticators() メソッドを使用して宣言した型の同じビット単位の組み合わせを canAuthenticate() メソッドに渡します。必要に応じて、ACTION_BIOMETRIC_ENROLL インテントのアクションを呼び出します。インテント エクストラには、アプリが受け入れる認証システムのセットを提示してください。このインテントでは、アプリが受け入れる認証システムの認証情報を登録するように、ユーザーに求めます。

Kotlin

val biometricManager = BiometricManager.from(this)
when (biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL)) {
    BiometricManager.BIOMETRIC_SUCCESS ->
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.")
    BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
        Log.e("MY_APP_TAG", "No biometric features available on this device.")
    BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE ->
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.")
    BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
        // Prompts the user to create credentials that your app accepts.
        val enrollIntent = Intent(Settings.ACTION_BIOMETRIC_ENROLL).apply {
            putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG or DEVICE_CREDENTIAL)
        }
        startActivityForResult(enrollIntent, REQUEST_CODE)
    }
}

Java

BiometricManager biometricManager = BiometricManager.from(this);
switch (biometricManager.canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)) {
    case BiometricManager.BIOMETRIC_SUCCESS:
        Log.d("MY_APP_TAG", "App can authenticate using biometrics.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE:
        Log.e("MY_APP_TAG", "No biometric features available on this device.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE:
        Log.e("MY_APP_TAG", "Biometric features are currently unavailable.");
        break;
    case BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED:
        // Prompts the user to create credentials that your app accepts.
        final Intent enrollIntent = new Intent(Settings.ACTION_BIOMETRIC_ENROLL);
        enrollIntent.putExtra(Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED,
                BIOMETRIC_STRONG | DEVICE_CREDENTIAL);
        startActivityForResult(enrollIntent, REQUEST_CODE);
        break;
}

ユーザーの認証方法を決定する

ユーザー認証の後、getAuthenticationType() を呼び出すと、認証されたユーザーがデバイスの認証情報と生体認証情報のどちらを使用したかを確認できます。

ログイン プロンプトを表示する

生体認証情報を使用した認証をユーザーに要求するシステム プロンプトを表示するには、生体認証ライブラリを使用します。システムが提供するこのダイアログは、ダイアログを使用するアプリの間で一貫しているため、信頼できるユーザー エクスペリエンスを実現します。ダイアログの例を図 1 に示します。

ダイアログのスクリーンショット
図 1. 生体認証を要求するシステム ダイアログ

生体認証ライブラリを使用してアプリに生体認証機能を追加する手順は次のとおりです。

  1. アプリ モジュールの build.gradle ファイルで、androidx.biometric ライブラリへの依存関係を追加します。

  2. 生体認証ログイン ダイアログをホストするアクティビティまたはフラグメントで、次のコード スニペットに示すロジックを使用してダイアログを表示します。

    Kotlin

    private lateinit var executor: Executor
    private lateinit var biometricPrompt: BiometricPrompt
    private lateinit var promptInfo: BiometricPrompt.PromptInfo
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)
        executor = ContextCompat.getMainExecutor(this)
        biometricPrompt = BiometricPrompt(this, executor,
                object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int,
                    errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                Toast.makeText(applicationContext,
                    "Authentication error: $errString", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationSucceeded(
                    result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                Toast.makeText(applicationContext,
                    "Authentication succeeded!", Toast.LENGTH_SHORT)
                    .show()
            }
    
            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                Toast.makeText(applicationContext, "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show()
            }
        })
    
        promptInfo = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build()
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        val biometricLoginButton =
                findViewById<Button>(R.id.biometric_login)
        biometricLoginButton.setOnClickListener {
            biometricPrompt.authenticate(promptInfo)
        }
    }

    Java

    private Executor executor;
    private BiometricPrompt biometricPrompt;
    private BiometricPrompt.PromptInfo promptInfo;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        executor = ContextCompat.getMainExecutor(this);
        biometricPrompt = new BiometricPrompt(MainActivity.this,
                executor, new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode,
                    @NonNull CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Toast.makeText(getApplicationContext(),
                    "Authentication error: " + errString, Toast.LENGTH_SHORT)
                    .show();
            }
    
            @Override
            public void onAuthenticationSucceeded(
                    @NonNull BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Toast.makeText(getApplicationContext(),
                    "Authentication succeeded!", Toast.LENGTH_SHORT).show();
            }
    
            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Toast.makeText(getApplicationContext(), "Authentication failed",
                    Toast.LENGTH_SHORT)
                    .show();
            }
        });
    
        promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("Biometric login for my app")
                .setSubtitle("Log in using your biometric credential")
                .setNegativeButtonText("Use account password")
                .build();
    
        // Prompt appears when user clicks "Log in".
        // Consider integrating with the keystore to unlock cryptographic operations,
        // if needed by your app.
        Button biometricLoginButton = findViewById(R.id.biometric_login);
        biometricLoginButton.setOnClickListener(view -> {
                biometricPrompt.authenticate(promptInfo);
        });
    }

認証に依存する暗号化ソリューションを使用する

アプリ内の機密情報の保護を強化するには、CryptoObject のインスタンスを使用して、生体認証ワークフローに暗号化を組み込みます。フレームワークは次の暗号化オブジェクトをサポートします: SignatureCipherMac

ユーザーが生体認証プロンプトを使用した認証に成功すると、アプリは暗号化オペレーションを実行できます。たとえば、Cipher オブジェクトを使用して認証する場合、アプリは SecretKey オブジェクトを使用して暗号化と復号を実行できます。

以下のセクションでは、Cipher オブジェクトと SecretKey オブジェクトを使用してデータを暗号化する例を紹介します。それぞれの例では、以下のメソッドを使用します。

Kotlin

private fun generateSecretKey(keyGenParameterSpec: KeyGenParameterSpec) {
    val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
    keyGenerator.init(keyGenParameterSpec)
    keyGenerator.generateKey()
}

private fun getSecretKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null)
    return keyStore.getKey(KEY_NAME, null) as SecretKey
}

private fun getCipher(): Cipher {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7)
}

Java

private void generateSecretKey(KeyGenParameterSpec keyGenParameterSpec) {
    KeyGenerator keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    keyGenerator.init(keyGenParameterSpec);
    keyGenerator.generateKey();
}

private SecretKey getSecretKey() {
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");

    // Before the keystore can be accessed, it must be loaded.
    keyStore.load(null);
    return ((SecretKey)keyStore.getKey(KEY_NAME, null));
}

private Cipher getCipher() {
    return Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/"
            + KeyProperties.ENCRYPTION_PADDING_PKCS7);
}

生体認証情報のみを使用して認証する

ロック解除に生体認証情報を要求する秘密鍵をアプリが使用している場合、ユーザーはアプリが鍵にアクセスする前に、毎回生体認証情報を認証する必要があります。

ユーザーが生体認証情報を使用して認証した後でのみ機密情報を暗号化する手順は次のとおりです。

  1. 次の KeyGenParameterSpec 構成を使用する鍵を生成します。

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build())

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
            KEY_NAME,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            // Invalidate the keys if the user has registered a new biometric
            // credential, such as a new fingerprint. Can call this method only
            // on Android 7.0 (API level 24) or higher. The variable
            // "invalidatedByBiometricEnrollment" is true by default.
            .setInvalidatedByBiometricEnrollment(true)
            .build());
  2. 暗号を組み込んだ生体認証ワークフローを開始します。

    Kotlin

    biometricLoginButton.setOnClickListener {
        // Exceptions are unhandled within this snippet.
        val cipher = getCipher()
        val secretKey = getSecretKey()
        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
        biometricPrompt.authenticate(promptInfo,
                BiometricPrompt.CryptoObject(cipher))
    }

    Java

    biometricLoginButton.setOnClickListener(view -> {
        // Exceptions are unhandled within this snippet.
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        biometricPrompt.authenticate(promptInfo,
                new BiometricPrompt.CryptoObject(cipher));
    });
  3. 生体認証コールバック内で、秘密鍵を使用して機密情報を暗号化します。

    Kotlin

    override fun onAuthenticationSucceeded(
            result: BiometricPrompt.AuthenticationResult) {
        val encryptedInfo: ByteArray = result.cryptoObject.cipher?.doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.toByteArray(Charset.defaultCharset())
        )
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo))
    }

    Java

    @Override
    public void onAuthenticationSucceeded(
            @NonNull BiometricPrompt.AuthenticationResult result) {
        // NullPointerException is unhandled; use Objects.requireNonNull().
        byte[] encryptedInfo = result.getCryptoObject().getCipher().doFinal(
            // plaintext-string text is whatever data the developer would like
            // to encrypt. It happens to be plain-text in this example, but it
            // can be anything
                plaintext-string.getBytes(Charset.defaultCharset()));
        Log.d("MY_APP_TAG", "Encrypted information: " +
                Arrays.toString(encryptedInfo));
    }

生体認証情報またはロック画面認証情報を使用して認証する

生体認証情報またはロック画面認証情報(PIN、パターン、またはパスワード)のいずれかを使用した認証を許可する秘密鍵を使用できます。この鍵を構成する場合は、有効期間を指定します。有効期間中、アプリはユーザーの再認証なしで複数の暗号オペレーションを実行できます。

生体認証情報またはロック画面認証情報を使用してユーザーが認証した後で機密情報を暗号化する手順は次のとおりです。

  1. 次の KeyGenParameterSpec 構成を使用する鍵を生成します。

    Kotlin

    generateSecretKey(KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build())

    Java

    generateSecretKey(new KeyGenParameterSpec.Builder(
        KEY_NAME,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
        .setUserAuthenticationRequired(true)
        .setUserAuthenticationParameters(VALIDITY_DURATION_SECONDS,
                ALLOWED_AUTHENTICATORS)
        .build());
  2. ユーザーが認証した後、VALIDITY_DURATION_SECONDS で指定された時間内に機密情報を暗号化します。

    Kotlin

    private fun encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        val cipher = getCipher()
        val secretKey = getSecretKey()
        try {
            cipher.init(Cipher.ENCRYPT_MODE, secretKey)
            val encryptedInfo: ByteArray = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.toByteArray(Charset.defaultCharset()))
            Log.d("MY_APP_TAG", "Encrypted information: " +
                    Arrays.toString(encryptedInfo))
        } catch (e: InvalidKeyException) {
            Log.e("MY_APP_TAG", "Key is invalid.")
        } catch (e: UserNotAuthenticatedException) {
            Log.d("MY_APP_TAG", "The key's validity timed out.")
            biometricPrompt.authenticate(promptInfo)
        }

    Java

    private void encryptSecretInformation() {
        // Exceptions are unhandled for getCipher() and getSecretKey().
        Cipher cipher = getCipher();
        SecretKey secretKey = getSecretKey();
        try {
            // NullPointerException is unhandled; use Objects.requireNonNull().
            ciper.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedInfo = cipher.doFinal(
                // plaintext-string text is whatever data the developer would
                // like to encrypt. It happens to be plain-text in this example,
                // but it can be anything
                    plaintext-string.getBytes(Charset.defaultCharset()));
        } catch (InvalidKeyException e) {
            Log.e("MY_APP_TAG", "Key is invalid.");
        } catch (UserNotAuthenticatedException e) {
            Log.d("MY_APP_TAG", "The key's validity timed out.");
            biometricPrompt.authenticate(promptInfo);
        }
    }

利用ごとの認証キーを使用した認証

BiometricPrompt のインスタンス内で、利用ごとの認証キーのサポートを提供できます。このようなキーでは、そのキーで保護されているデータにアプリがアクセスする必要があるたびに、ユーザーに生体認証情報かデバイスの認証情報を提示するように求める必要があります。利用ごとの認証キーは、高額の支払いや、個人の健康記録の更新など、価値の高いトランザクションの場合に便利です。

利用ごとの認証キーに BiometricPrompt オブジェクトを関連付けるには、次のようなコードを追加します。

Kotlin

val authPerOpKeyGenParameterSpec =
        KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG or
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build()

Java

KeyGenParameterSpec authPerOpKeyGenParameterSpec =
        new KeyGenParameterSpec.Builder("myKeystoreAlias", key-purpose)
    // Accept either a biometric credential or a device credential.
    // To accept only one type of credential, include only that type as the
    // second argument.
    .setUserAuthenticationParameters(0 /* duration */,
            KeyProperties.AUTH_BIOMETRIC_STRONG |
            KeyProperties.AUTH_DEVICE_CREDENTIAL)
    .build();

明示的なユーザー アクションを求めずに認証する

デフォルトでは、生体認証情報が受け入れられた後、ユーザーは「ボタンを押す」といった特定のアクションを実行するよう求められます。この構成は、慎重さを要するアクションまたはリスクの高いアクション(購入など)を確定するダイアログをアプリ内で表示する場合に適しています。

ただし、アプリが低リスクのアクションに対して生体認証ダイアログを表示する場合は、ユーザーが認証を確認する必要がないというヒントをシステムに提供できます。このヒントにより、ユーザーは顔認証や虹彩認証などの受動的な手段で再認証を行った後、アプリのコンテンツをすぐに表示できます。このヒントを提供するには、falsesetConfirmationRequired() メソッドに渡します。

同じダイアログの 2 つのバージョンを図 2 に示します。1 つのバージョンでは明示的なユーザー アクションが必要で、もう 1 つのバージョンでは必要ありません。

ダイアログのスクリーンショット ダイアログのスクリーンショット
図 2. ユーザー確認なしの顔認証(上)とユーザー確認ありの顔認証(下)

認証プロセスを完了するうえで明示的なユーザー アクションを要求しないダイアログを表示する方法を、次のコード スニペットに示します。

Kotlin

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build()

Java

// Lets the user authenticate without performing an action, such as pressing a
// button, after their biometric credential is accepted.
promptInfo = new BiometricPrompt.PromptInfo.Builder()
        .setTitle("Biometric login for my app")
        .setSubtitle("Log in using your biometric credential")
        .setNegativeButtonText("Use account password")
        .setConfirmationRequired(false)
        .build();

非生体認証情報へのフォールバックを許可する

アプリで生体認証情報またはデバイス認証情報を使用した認証を許可する場合は、setAllowedAuthenticators() に渡す値のセットに DEVICE_CREDENTIAL を含めることで、アプリがデバイスの認証情報をサポートすることを宣言できます。

アプリで現在 createConfirmDeviceCredentialIntent() または setDeviceCredentialAllowed() を使用してこの機能を提供している場合は、setAllowedAuthenticators() を使用するように切り替えます。

参考情報

Android での生体認証の詳細については、次のリソースをご覧ください。

ブログ投稿