Android Protected の確認

ユーザーが支払いなどの機密性の高いトランザクションを開始したときにユーザーの意図を確認できるように、Android 9(API レベル 28)以降を搭載しているサポート対象デバイスでは、Android Protected の確認を使用できます。このワークフローを使用すると、機密性の高いトランザクションを完了する意向を再確認し、承認を求める短いステートメントをアプリで表示できます。

ユーザーがこのステートメントを承認すると、アプリは Android キーストアから取得した鍵を使用して、ダイアログに表示されたメッセージに署名できます。署名により、ユーザーが確実にステートメントを見て、それに同意したことになります。

注意: Android Protected の確認は、ユーザーに対して安全な情報チャンネルを提供しません。アプリでは、Android プラットフォームが提供する機密保持の保証以外は保証されません。特に、このワークフローを使用して、ユーザーのデバイスに通常は表示しない機密情報を表示しないようにしてください。

ユーザーがメッセージに合意すると、その完全性は保証されますが、アプリでは引き続き転送中のデータを暗号化して、署名されたメッセージの機密性を保護する必要があります。

アプリで確実にユーザーによる確認ができるようにするには、次の手順を実行します。

  1. KeyGenParameterSpec.Builder クラスを使用して、非対称署名鍵を生成します。この鍵を生成するときには、setUserConfirmationRequired()true を渡します。また、setAttestationChallenge() を呼び出して、リライング パーティから提供された適切なチャレンジ値を渡します。

  2. 新しく生成された鍵に加えて、鍵の構成証明書と適切なリライング パーティを登録します。

  3. トランザクションの詳細をサーバーに送信し、サーバーで追加データのバイナリ ラージ オブジェクト(BLOB)を生成して返します。追加データには、確認対象のデータや解析のヒント(プロンプト文字列の言語 / 地域など)を含められます。

    実装の安全性を高めるには、BLOB に暗号ノンスを含めて、リプレイ攻撃から保護し、トランザクションのあいまいさを排除する必要があります。

  4. 確認ダイアログに表示されたプロンプトをユーザーが承認したことをアプリに通知する ConfirmationCallback オブジェクトを設定します。

    Kotlin

    class MyConfirmationCallback : ConfirmationCallback() {
    
          override fun onConfirmed(dataThatWasConfirmed: ByteArray?) {
              super.onConfirmed(dataThatWasConfirmed)
              // Sign dataThatWasConfirmed using your generated signing key.
              // By completing this process, you generate a signed statement.
          }
    
          override fun onDismissed() {
              super.onDismissed()
              // Handle case where user declined the prompt in the
              // confirmation dialog.
          }
    
          override fun onCanceled() {
              super.onCanceled()
              // Handle case where your app closed the dialog before the user
              // responded to the prompt.
          }
    
          override fun onError(e: Exception?) {
              super.onError(e)
              // Handle the exception that the callback captured.
          }
      }

    Java

    public class MyConfirmationCallback extends ConfirmationCallback {
    
      @Override
      public void onConfirmed(@NonNull byte[] dataThatWasConfirmed) {
          super.onConfirmed(dataThatWasConfirmed);
          // Sign dataThatWasConfirmed using your generated signing key.
          // By completing this process, you generate a signed statement.
      }
    
      @Override
      public void onDismissed() {
          super.onDismissed();
          // Handle case where user declined the prompt in the
          // confirmation dialog.
      }
    
      @Override
      public void onCanceled() {
          super.onCanceled();
          // Handle case where your app closed the dialog before the user
          // responded to the prompt.
      }
    
      @Override
      public void onError(Throwable e) {
          super.onError(e);
          // Handle the exception that the callback captured.
      }
    }

    ユーザーがダイアログを承認すると、onConfirmed() コールバックが呼び出されます。dataThatWasConfirmed BLOB は CBOR データ構造です。このデータ構造には、ユーザーに表示されたプロンプト テキストや、ConfirmationPrompt ビルダーに渡した追加データなどの詳細情報も含まれます。以前に作成した鍵を使用して dataThatWasConfirmed BLOB に署名し、この BLOB を署名およびトランザクションの詳細とともにリライング パーティに渡します。

    Android Protected の確認によるセキュリティ保証を最大限に活用するために、リライング パーティは署名付きメッセージを受け取り次第、以下の手順を実行する必要があります。

    1. 署名をメッセージと署名鍵の構成証明書チェーンで確認します。
    2. 構成証明書に TRUSTED_CONFIRMATION_REQUIRED フラグが設定されていることを確認します。このフラグは、署名鍵の確認を信頼できるユーザーが行う必要があることを示します。署名鍵が RSA 鍵の場合は、PURPOSE_ENCRYPT または PURPOSE_DECRYPT プロパティが設定されていないことを確認します。
    3. extraData をチェックして、この確認メッセージが新しいリクエストに属していて、まだ処理されていないことを確認します。この手順を行うことで、リプレイ攻撃から保護できます。
    4. promptText を解析して、確認したアクションやリクエストに関する情報を取得します。promptText はユーザーが実際に確認したメッセージの一部にすぎません。リライング パーティは extraData に含まれている確認対象のデータが promptText に対応していると仮定することはできません。
  5. 次のコード スニペットのようなロジックを追加して、ダイアログを表示します。

    Kotlin

    // This data structure varies by app type. This is an example.
      data class ConfirmationPromptData(val sender: String,
              val receiver: String, val amount: String)
    
      val myExtraData: ByteArray = byteArrayOf()
      val myDialogData = ConfirmationPromptData("Ashlyn", "Jordan", "$500")
      val threadReceivingCallback = Executor { runnable -> runnable.run() }
      val callback = MyConfirmationCallback()
    
      val dialog = ConfirmationPrompt.Builder(context)
              .setPromptText("${myDialogData.sender}, send
                              ${myDialogData.amount} to
                              ${myDialogData.receiver}?")
              .setExtraData(myExtraData)
              .build()
      dialog.presentPrompt(threadReceivingCallback, callback)

    Java

      // This data structure varies by app type. This is an example.
      class ConfirmationPromptData {
          String sender, receiver, amount;
          ConfirmationPromptData(String sender, String receiver, String amount) {
              this.sender = sender;
              this.receiver = receiver;
              this.amount = amount;
          }
      };
      final int MY_EXTRA_DATA_LENGTH = 100;
      byte[] myExtraData = new byte[MY_EXTRA_DATA_LENGTH];
      ConfirmationPromptData myDialogData = new ConfirmationPromptData("Ashlyn", "Jordan", "$500");
      Executor threadReceivingCallback = Runnable::run;
      MyConfirmationCallback callback = new MyConfirmationCallback();
      ConfirmationPrompt dialog = (new ConfirmationPrompt.Builder(getApplicationContext()))
              .setPromptText("${myDialogData.sender}, send ${myDialogData.amount} to ${myDialogData.receiver}?")
              .setExtraData(myExtraData)
              .build();
      dialog.presentPrompt(threadReceivingCallback, callback);

参考情報

Android Protected の確認について詳しくは、以下のリソースをご覧ください。

ブログ