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

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

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

BiometricPrompt を呼び出す前に、BiometricManager クラスの canAuthenticate() メソッドを使用して、デバイスが生体認証をサポートしているかどうかをチェックできます。

次のコード スニペットは、このメソッドを呼び出す方法を示しています。

Kotlin

    val biometricManager = BiometricManager.from(this)
    when (biometricManager.canAuthenticate()) {
        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 ->
            Log.e("MY_APP_TAG", "The user hasn't associated " +
            "any biometric credentials with their account.")
    }
    

Java

    BiometricManager biometricManager = BiometricManager.from(this);
    switch (biometricManager.canAuthenticate()) {
        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:
            Log.e("MY_APP_TAG", "The user hasn't associated " +
                "any biometric credentials with their account.");
            break;
    }
    

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

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

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

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

  1. アプリの app/build.gradle ファイルに、生体認証ライブラリの依存関係を追加します。

        dependencies {
            implementation 'androidx.biometric:biometric:1.0.1'
        }
        
  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.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.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)
            .setUserAuthenticationValidityDurationSeconds(VALIDITY_DURATION)
            .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)
            .setUserAuthenticationValidityDurationSeconds(VALIDITY_DURATION)
            .build());
        
  2. ユーザーが認証した後、VALIDITY_DURATION 秒以内に機密情報を暗号化します。

    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.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.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);
            }
        }
        

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

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

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

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

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

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

Kotlin

    // Allows user to 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

    // Allows user to 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();
    

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

アプリで生体認証情報またはデバイス認証情報を使用した認証を許可する場合は、setDeviceCredentialAllowed() メソッドに true を渡すことで、ロック画面の PIN、パターン、またはパスワードを使用した認証を許可できます。

次のコード スニペットは、生体認証情報またはデバイス認証情報を使用した認証をサポートする方法を示しています。

Kotlin

    // Allows user to authenticate using their lock screen
    // PIN, pattern, or password.
    promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Biometric login for my app")
            .setSubtitle("Log in using your biometric credential")
            // Cannot call setNegativeButtonText() and
            // setDeviceCredentialAllowed() at the same time.
            // .setNegativeButtonText("Use account password")
            .setDeviceCredentialAllowed(true)
            .build()
    

Java

    // Allows user to authenticate using their lock screen
    // PIN, pattern, or password.
    promptInfo = new BiometricPrompt.PromptInfo.Builder()
            .setTitle("Biometric login for my app")
            .setSubtitle("Log in using your biometric credential")
            // Cannot call setNegativeButtonText() and
            // setDeviceCredentialAllowed() at the same time.
            // .setNegativeButtonText("Use account password")
            .setDeviceCredentialAllowed(true)
            .build();
    

参考情報

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

ブログ投稿