Android の認証システムを保護するには、特にユーザーの銀行口座やメールアカウントのような機密性の高いアカウントについては、パスワード ベースのモデルから移行することを検討してください。ユーザーがインストールするアプリの中には、意図して意図したものではなく、ユーザーをフィッシングしようとする可能性のあるものもあることに留意してください。
また、許可されたユーザーだけがデバイスを使用することを想定しないでください。スマートフォンの盗難はよくある問題であり、攻撃者はロック解除されたデバイスを狙って、ユーザーデータや金融アプリから直接利益を得ます。機密性の高いすべてのアプリは、生体認証による適切な認証タイムアウト(15 分程度)を実装し、送金などの機密性の高い操作の前に追加の認証を要求することをおすすめします。
生体認証ダイアログ
生体認証ライブラリには、顔認証や指紋認証などの生体認証をリクエストするプロンプトを表示する一連の関数が用意されています。ただし、生体認証プロンプトは 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 に負担がかかる可能性があることは承知していますが、銀行取引で処理される情報の性質上、生体認証は他の認証方法よりもスムーズなものであるため、このレベルのナビゲーションを追加する必要があると考えています。
詳しくは、生体認証についての説明をご覧ください。
パスキー
パスキーは、パスワードに代わるより安全で簡単な方法です。パスキーでは公開鍵暗号を使用して、ユーザーが指紋認証や顔認証などのデバイスの画面ロック メカニズムを使用してアプリやウェブサイトにログインできるようにします。これにより、ユーザーはパスワードを覚えて管理する必要がなくなり、セキュリティが大幅に向上します。
パスキーは 1 ステップで多要素認証要件を満たすことができます。パスワードと OTP コードの両方を置換することで、フィッシング攻撃から堅牢な保護を提供し、SMS やアプリベースのワンタイム パスワードによるユーザー エクスペリエンスの課題を回避します。パスキーは標準化されているため、1 回の実装で、ユーザーのすべてのデバイス、ブラウザ、オペレーティング システムでパスワードレスのエクスペリエンスを実現できます。
Android では、パスキー、パスワード、フェデレーション ログイン(「Google でログイン」など)などの主要な認証方法を統合する認証情報マネージャー Jetpack ライブラリでパスキーがサポートされています。
不正行為の軽減にどのように役立つか
パスキーは登録したアプリとウェブサイトでのみ機能するため、フィッシング攻撃から保護されます。
パスキーのコア コンポーネントは暗号秘密鍵です。通常、この秘密鍵はノートパソコンやスマートフォンなどのデバイス上にのみ保存され、Google パスワード マネージャーなどの認証情報プロバイダ(パスワード マネージャーとも呼ばれます)によってデバイス間で同期されます。パスキーの作成時にオンライン サービスによって、対応する公開鍵のみが保存されます。ログイン中、サービスは秘密鍵を使用して公開鍵からチャレンジに署名します。これは、デバイスのいずれか 1 つからのみ発生します。さらに、これを行うには、デバイスまたは認証情報ストアのロックを解除する必要があります。これにより、盗難されたスマートフォンなどによる不正なログインを防ぐことができます。
ロック解除されたデバイスが盗まれた場合の不正アクセスを防ぐために、パスキーに適切な認証タイムアウト ウィンドウを設定する必要があります。デバイスを盗む攻撃者が、前のユーザーがログインしたからといって、アプリを使用できないようにする必要があります。認証情報は一定の間隔(15 分ごとなど)で期限切れにする必要があります。また、ユーザーは画面ロックの再認証によって本人確認を行う必要があります。
パスキーはデバイス固有です。スマートフォンが盗まれた場合でも、パスキーがあれば、盗難者が他のデバイスで使用するパスワードを盗むことはできないため、ユーザーを保護します。Google パスワード マネージャーを使用していてスマートフォンが盗まれた場合は、別のデバイス(パソコンなど)から Google アカウントにログインし、盗まれたスマートフォンからリモートでログアウトできます。これにより、盗まれたスマートフォンの Google パスワード マネージャー(保存されているパスキーを含む)が使用できなくなります。
最悪のシナリオでは、盗まれたデバイスが復元されない場合、パスキーを作成して同期した認証情報プロバイダによって、パスキーが新しいデバイスに同期されます。たとえば、ユーザーが Google パスワード マネージャーを選択してパスキーを作成した場合、Google アカウントに再度ログインし、以前のデバイスで画面ロックを指定することで、新しいデバイスでパスキーにアクセスできます。
詳細については、Google パスワード マネージャーでのパスキーのセキュリティの記事をご覧ください。
実装
パスキーは、Android 9(API レベル 28)以降を搭載しているデバイスでサポートされています。パスワードと「Google でログイン」は、Android 4.4 以降でサポートされています。パスキーを使用する手順は次のとおりです。
- 認証情報マネージャーの Codelab に沿って、パスキーの実装方法に関する基礎知識を学びます。
- パスキーのユーザー エクスペリエンス設計ガイドラインを確認します。このドキュメントでは、このユースケースに推奨されるフローについて説明します。
- ガイドに沿って認証情報マネージャーについて学習する。
- アプリの認証情報マネージャーとパスキーの実装を計画します。デジタル アセット リンクのサポートを追加することを計画します。
パスキーの作成、登録、認証を行う方法について詳しくは、デベロッパー向けドキュメントをご覧ください。
アカウントの安全なリセット
ロック解除されたデバイスにアクセスできる不正な攻撃者が(スマートフォンを盗まれた場合など)、機密性の高いアプリ(特にバンキング アプリや現金アプリ)にアクセスしようとします。アプリに生体認証が実装されている場合、攻撃者はアカウントをリセットしてアクセスしようとします。アカウント リセットのフローでは、メールや SMS OTP リセットリンクなど、デバイスから簡単にアクセスできる情報のみに依存しないことが不可欠です。
アプリのリセットフローに組み込むことができる一般的なベスト プラクティスは次のとおりです。
- 顔認識(OTP 以外)
- セキュリティ保護用の質問
- 知識要素(母親の旧姓、出生地、お気に入りの曲など)
- ID 確認
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 Retriever クライアントを起動します。
- 電話番号をサーバーに送信します。
- 確認メッセージを受信します。
- OTP をサーバーに送信します。
サーバー:(ガイド)
- 確認メッセージを作成します。
- 確認メッセージを SMS で送信します。
- OTP が返却されたら、その OTP を確認します。
おすすめの方法
アプリが統合され、SMS Retriever API でユーザーの電話番号が検証されると、OTP の取得が試みられます。成功した場合、それはデバイスで SMS が自動的に受信したことを示す強力なシグナルです。成功せず、ユーザーが OTP を手動で入力する必要がある場合は、ユーザーに不正行為が発生している可能性があるという警告が表示されることがあります。
SMS を唯一のユーザー確認メカニズムとして使用しないでください。ロック解除されたデバイスを盗む攻撃者や SIM クローニング攻撃などのローカル攻撃を受ける余地が残るためです。可能な限り、生体認証システムを使用することをおすすめします。生体認証センサーを利用できないデバイスでは、ユーザー認証は、現在のデバイスから簡単に取得できない要素を少なくとも 1 つ利用する必要があります。
さらに詳しく
ベスト プラクティスについて詳しくは、次のリソースをご覧ください。