Android で認証システムを保護するには、パスワード ベースのモデルから移行することを検討してください。特に、ユーザーの銀行口座やメール アカウントなどの機密性の高いアカウントについては、移行を検討してください。ユーザーがインストールするアプリの中には、悪意のあるものや、ユーザーをフィッシングしようとするものがあることを忘れないでください。
また、承認されたユーザーのみがデバイスを使用するとは限りません。スマートフォンの盗難はよくある問題であり、攻撃者はロック解除されたデバイスを標的として、ユーザーデータや金融アプリから直接利益を得ようとします。機密性の高いアプリはすべて、生体認証による妥当な認証タイムアウト(15 分など)を実装し、送金などの機密性の高い操作を行う前に追加の認証を要求することをおすすめします。
生体認証ダイアログ
Biometrics ライブラリは、顔認識や指紋認証などの生体認証を求めるプロンプトを表示する一連の関数を提供します。ただし、生体認証プロンプトは LSKF にフォールバックするように構成できます。LSKF にはショルダー サーフィンのリスクがあることが知られています。機密性の高いアプリでは、生体認証が PIN にフォールバックしないようにすることをおすすめします。生体認証の再試行がすべて失敗した場合は、ユーザーが待つか、パスワードで再ログインするか、アカウントをリセットできるようにします。アカウントのリセットには、デバイスで簡単にアクセスできない要素が必要です(以下のベスト プラクティスを参照)。
不正行為やスマートフォンの盗難を軽減する仕組み
不正行為を防ぐのに役立つユースケースの 1 つは、トランザクションの前にアプリ内で生体認証をリクエストすることです。ユーザーが金融取引を行う場合、生体認証ダイアログが表示され、取引を行っているのが意図したユーザーであることを確認します。このベスト プラクティスは、攻撃者が LSKF を知っているかどうかにかかわらず、デバイスの所有者であることを証明する必要があるため、デバイスの盗難から保護します。
セキュリティ レベルをさらに高めるため、アプリ デベロッパーはクラス 3 生体認証をリクエストし、銀行取引や金融取引に CryptoObject
を利用することをおすすめします。
実装
- androidx.biometric ライブラリを含めるようにしてください。
- ユーザーに認証してもらいたいロジックを保持するアクティビティまたはフラグメントに、生体認証ログイン ダイアログを含めます。
Kotlin
private var executor: Executor? = null private var biometricPrompt: BiometricPrompt? = null private var promptInfo: BiometricPrompt.PromptInfo? = null fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) executor = ContextCompat.getMainExecutor(this) biometricPrompt = BiometricPrompt(this@MainActivity, executor, object : AuthenticationCallback() { fun onAuthenticationError( errorCode: Int, @NonNull errString: CharSequence ) { super.onAuthenticationError(errorCode, errString) Toast.makeText( getApplicationContext(), "Authentication error: $errString", Toast.LENGTH_SHORT ) .show() } fun onAuthenticationSucceeded( @NonNull result: BiometricPrompt.AuthenticationResult? ) { super.onAuthenticationSucceeded(result) Toast.makeText( getApplicationContext(), "Authentication succeeded!", Toast.LENGTH_SHORT ).show() } fun onAuthenticationFailed() { super.onAuthenticationFailed() Toast.makeText( getApplicationContext(), "Authentication failed", Toast.LENGTH_SHORT ) .show() } }) 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: Button = findViewById(R.id.biometric_login) biometricLoginButton.setOnClickListener { view -> 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 the 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); }); }
おすすめの方法
生体認証について詳しくは、Codelab をご覧になることをおすすめします。
ユースケースに応じて、明示的なユーザー アクションの有無にかかわらずダイアログを実装できます。不正行為を回避するため、すべての取引でユーザーの明示的な操作を伴う生体認証ダイアログを追加することをおすすめします。認証を追加すると UX に摩擦が生じる可能性があることは承知しておりますが、銀行取引で扱われる情報の性質と、生体認証が他の認証方法よりもスムーズであることから、このレベルのナビゲーションを追加する必要があると考えています。
詳しくは、生体認証をご覧ください。
パスキー
パスキーは、パスワードに代わる安全で簡単な認証方法です。パスキーは公開鍵暗号を使用して、ユーザーが指紋認証や顔認証などのデバイスの画面ロック メカニズムを使用してアプリやウェブサイトにログインできるようにします。これにより、ユーザーはパスワードを覚えて管理する必要がなくなり、セキュリティが大幅に向上します。
パスキーは、パスワードと OTP コードの両方を置き換えて、フィッシング攻撃に対する強力な保護を提供し、SMS やアプリベースのワンタイム パスワードによるユーザー エクスペリエンスの低下を回避することで、多要素認証の要件を 1 つのステップで満たすことができます。パスキーは標準化されているため、1 回の実装で、ユーザーのすべてのデバイス、ブラウザ、オペレーティング システムでパスワードなしのエクスペリエンスを実現できます。
Android では、パスキー、パスワード、フェデレーション ログイン(「Google でログイン」など)といった主要な認証方法を統合する 認証情報マネージャー Jetpack ライブラリを使用して、パスキーがサポートされています。
不正行為の軽減にどのように役立つか
パスキーは登録済みのアプリやウェブサイトでのみ機能するため、フィッシング攻撃からユーザーを保護します。
パスキーのコア コンポーネントは暗号秘密鍵です。通常、この秘密鍵はノートパソコンや携帯電話などのデバイスにのみ存在し、Google パスワード マネージャーなどの認証情報プロバイダ(パスワード マネージャーとも呼ばれます)によってデバイス間で同期されます。パスキーが作成されると、対応する公開鍵のみがオンライン サービスによって保存されます。ログイン時に、サービスは秘密鍵を使用して公開鍵からのチャレンジに署名します。これは、ユーザーのデバイスのいずれかからのみ発生する可能性があります。また、この処理を行うには、デバイスまたは認証情報ストアのロックを解除する必要があります。これにより、不正なログイン(盗難されたスマートフォンからのログインなど)を防ぐことができます。
盗難されたロック解除済みのデバイスによる不正アクセスを防ぐため、パスキーは適切な認証タイムアウト ウィンドウと組み合わせて使用する必要があります。デバイスを盗んだ攻撃者が、以前のユーザーがログインしていたという理由だけでアプリを使用できるべきではありません。代わりに、認証情報は定期的な間隔(15 分ごとなど)で期限切れになり、ユーザーは画面ロックの再認証によって身元確認を行う必要があります。
パスキーはデバイス固有のものであるため、パスワードを盗んで他のデバイスで使用することはできません。そのため、スマートフォンが盗まれた場合でも、パスキーによって保護されます。Google パスワード マネージャーを使用している場合、スマートフォンが盗難に遭っても、別のデバイス(パソコンなど)から Google アカウントにログインして、盗まれたスマートフォンからリモートでログアウトできます。これにより、盗まれたスマートフォンの Google パスワード マネージャー(保存済みのパスキーを含む)が使用できなくなります。
最悪の場合、盗まれたデバイスが回収されなかったとしても、パスキーを作成して同期した認証情報プロバイダによって、パスキーが新しいデバイスに同期されます。たとえば、ユーザーが Google パスワード マネージャーでパスキーを作成した場合、新しいデバイスで Google アカウントに再度ログインし、以前のデバイスの画面ロックを入力することで、パスキーにアクセスできます。
詳しくは、Google パスワード マネージャーのパスキーのセキュリティに関する記事をご覧ください。
実装
パスキーは、Android 9(API レベル 28)以降を搭載しているデバイスでサポートされています。パスワードと「Google でログイン」は Android 4.4 以降でサポートされています。パスキーの使用を開始する手順は次のとおりです。
- Credential Manager Codelab に沿って、パスキーの実装方法の概要を把握します。
- パスキーのユーザー エクスペリエンスの設計ガイドラインを確認します。このドキュメントでは、ユースケースに推奨されるフローについて説明します。
- ガイドに沿って、認証情報マネージャーについて学習します。
- アプリの Credential Manager とパスキーの実装を計画します。デジタル アセット リンクのサポートを追加する計画を立てます。
パスキーの作成、登録、認証の方法について詳しくは、デベロッパー向けドキュメントをご覧ください。
安全なアカウントの再設定
ロック解除されたデバイスにアクセスできる不正な攻撃者は(スマートフォンが奪われた場合など)、機密性の高いアプリ、特に銀行アプリや現金アプリにアクセスしようとします。アプリが生体認証を実装している場合、攻撃者はアカウントをリセットして侵入しようとします。アカウントのリセット フローは、メールや SMS の OTP リセットリンクなど、デバイスで簡単にアクセスできる情報のみに依存しないことが重要です。
アプリのリセット フローに組み込むことができる一般的なベスト プラクティスを以下に示します。
- 顔認識(OTP に加えて)
- セキュリティ保護用の質問
- 知識要素(母親の旧姓、出生地、好きな曲など)
- 身分証明書による認証
SMS Retriever API
SMS Retriever API を使用すると、Android アプリで SMS ベースのユーザー認証を自動的に実行できます。これにより、ユーザーは確認コードを手動で入力する必要がなくなります。また、この API では、RECEIVE_SMS
や READ_SMS
などの危険なアプリ権限をユーザーに求めることはありません。ただし、デバイスへの不正なローカル アクセスを防ぐための唯一のユーザー確認として SMS を使用すべきではありません。
不正行為の軽減にどのように役立つか
一部のユーザーは、SMS コードを唯一の認証要素として使用しており、不正行為の侵入を容易にしています。
SMS Retriever API を使用すると、アプリはユーザー操作なしで SMS コードを直接取得でき、不正行為に対する保護レベルを高めることができます。
実装
SMS Retriever API の実装は、Android とサーバーの 2 つの部分に分かれています。
Android: (ガイド)
- ユーザーの電話番号を取得します。
- SMS 取得ツール クライアントを起動します。
- 電話番号をサーバーに送信します。
- 確認メッセージを受信する。
- OTP をサーバーに送信します。
サーバー:(ガイド)
- 確認メッセージを作成します。
- 確認メッセージを SMS で送信します。
- OTP が返されたら、検証します。
おすすめの方法
アプリが統合され、SMS Retriever API でユーザーの電話番号が検証されると、OTP の取得が試行されます。成功した場合、SMS がデバイスで自動的に受信されたことを示す強いシグナルとなります。成功せず、ユーザーが OTP を手動で入力する必要がある場合は、ユーザーが不正行為に遭っている可能性があることを示す警告サインである可能性があります。
SMS は、ロック解除されたデバイスを奪う攻撃者や SIM クローニング攻撃などのローカル攻撃の余地を残すため、唯一のユーザー確認メカニズムとして使用すべきではありません。可能な限り、生体認証を使用することをおすすめします。生体認証センサーが利用できないデバイスでは、ユーザー認証は、現在のデバイスから容易に取得できない要素を少なくとも 1 つ使用する必要があります。
詳細
ベスト プラクティスの詳細については、以下のリソースをご覧ください。
- セキュリティに関する Android のドキュメント
- Play Integrity API のドキュメント
- Android 15 の変更点
- Monzo Bank による詐欺電話防止のベスト プラクティス