Android System Key Verifier

Android 用 Key Verifier は、エンドツーエンドの暗号化(E2EE)に対応するアプリで、ユーザーが正しい相手と通信していることを確認するための統一された安全な方法を提供します。信頼できる一貫性のあるシステム UI を通じて連絡先の公開暗号鍵の信頼性を確認できるようにすることで、中間者攻撃からユーザーを保護します。

この機能は、Google システム サービスの一部であり、Play ストアを使用して配信されるシステム サービスである Key Verifier によって提供されます。E2EE 公開鍵の一元化されたオンデバイス リポジトリとして機能します。

Key Verifier と統合する理由

  • 統一された UX を提供する: 独自の確認フローを構築する代わりに、システムの標準 UI を起動して、すべてのアプリで一貫性のある信頼できるエクスペリエンスをユーザーに提供できます。
  • ユーザーの信頼を高める: システムに裏付けられた明確な確認ステータスにより、ユーザーは会話が安全かつプライベートであることを確信できます。
  • 開発オーバーヘッドの削減: 鍵の検証 UI、ストレージ、ステータス管理の複雑さをシステム サービスにオフロードします。

主な用語

  • lookupKey: 連絡先の不透明で永続的な ID。連絡先プロバイダの LOOKUP_KEY 列に保存されます。contact ID とは異なり、lookupKey は基盤となる連絡先の詳細が変更または統合されても安定した状態を維持します。そのため、連絡先を参照する際は lookupKey を使用することをおすすめします。
  • accountId: デバイス上のユーザー アカウントのアプリ固有の識別子。この ID はアプリで定義され、1 人のユーザーが持つ複数のアカウントを区別するのに役立ちます。これは UI でユーザーに表示されるため、電話番号、メールアドレス、ユーザー ハンドルなどの意味のあるものを使用することをおすすめします。
  • deviceId: ユーザーのアカウントに関連付けられている特定のデバイスの一意の識別子。これにより、ユーザーは複数のデバイスを持ち、それぞれに独自の暗号鍵セットを設定できます。必ずしも物理デバイスを表すものではありませんが、同じアカウントで使用される複数の鍵を区別する方法として使用できます。

スタートガイド

始める前に、Key Verifier サービスと通信するようにアプリを設定します。

権限を宣言する: AndroidManifest.xml で、次の権限を宣言します。また、実行時にユーザーにリクエストする必要があります。

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

クライアント インスタンスを取得する: API のエントリ ポイントである ContactKeys のインスタンスを取得します。

import com.google.android.gms.contactkeys.ContactKeys

val contactKeyClient = ContactKeys.getClient(context)

メッセージ アプリ デベロッパー向けのガイダンス

メッセージ アプリのデベロッパーの主な役割は、ユーザーの公開鍵と連絡先の鍵を Key Verifier サービスに公開することです。

ユーザーの公開鍵を公開する

他のユーザーがユーザーを見つけて確認できるようにするには、ユーザーの公開鍵をオンデバイス リポジトリに公開します。セキュリティを強化するため、Android キーストアで鍵を作成することを検討してください。

import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun publishSelfKey(
    contactKeyClient: ContactKeyClient,
    accountId: String,
    deviceId: String,
    publicKey: ByteArray
) {
    try {
        Tasks.await(
          contactKeyClient.updateOrInsertE2eeSelfKey(
            deviceId,
            accountId,
            publicKey
          )
        )
        // Self key published successfully.
    } catch (e: Exception) {
        // Handle error.
    }
}

公開鍵を連絡先に関連付ける

アプリがユーザーの連絡先の 1 つの公開鍵を受け取った場合は、その公開鍵を保存し、中央リポジトリでその連絡先に関連付ける必要があります。これにより、キーを検証し、他のアプリで連絡先の検証ステータスを表示できるようになります。これを行うには、Android 連絡先プロバイダから連絡先の lookupKey が必要です。これは通常、鍵配布サーバーから鍵を取得するとき、またはローカル鍵の定期的な同期中にトリガーされます。

import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun storeContactKey(
    contactKeyClient: ContactKeyClient,
    contactLookupKey: String,
    contactAccountId: String,
    contactDeviceId: String,
    contactPublicKey: ByteArray
) {
    try {
        Tasks.await(
            contactKeyClient.updateOrInsertE2eeContactKey(
                contactLookupKey,
                contactDeviceId,
                contactAccountId,
                contactPublicKey
            )
        )
        // Contact's key stored successfully.
    } catch (e: Exception) {
        // Handle error.
    }
}

鍵と確認ステータスを取得する

鍵を公開すると、ユーザーは対面で QR コードをスキャンして鍵を検証できます。アプリの UI には、会話で確認済みの鍵が使用されているかどうかが反映されるようにする必要があります。各キーには、UI に通知するために使用できる検証ステータスがあります。

確認ステータスについて:

  • UNVERIFIED: これは、すべての新しい鍵のデフォルトの状態です。これは、鍵は存在するが、ユーザーがまだその真正性を確認していないことを意味します。UI では、これをニュートラルな状態として扱い、通常は特別なインジケーターを表示しません。

  • VERIFIED: このステータスは、信頼レベルが高いことを示します。これは、ユーザーが確認フロー(QR コードのスキャンなど)を正常に完了し、キーが意図した連絡先に属していることを確認したことを意味します。UI には、緑色のチェックマークや盾など、明確でポジティブなインジケーターを表示する必要があります。

  • VERIFICATION_FAILED: これは警告状態です。これは、連絡先に関連付けられているキーが、以前に確認されたキーと一致しないことを意味します。これは、連絡先のユーザーが新しいデバイスを入手した場合に発生することがありますが、セキュリティ上のリスクを示す可能性もあります。UI で、目立つ警告を表示してユーザーに通知し、機密情報を送信する前に再確認することを提案します。

連絡先に関連付けられているすべてのキーの集計ステータスを取得できます。複数のキーが存在する場合は、VerificationState.leastVerifiedFrom() を使用してステータスを解決することをおすすめします。VERIFICATION_FAILEDVERIFIED よりも優先されるためです。

  • 連絡先レベルで集計ステータスを取得する
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.contactkeys.constants.VerificationState
import com.google.android.gms.tasks.Tasks

suspend fun displayContactVerificationStatus(
    contactKeyClient: ContactKeyClient,
    contactLookupKey: String
) {
    try {
        val keysResult = Tasks.await(contactKeyClient.getAllE2eeContactKeys(contactLookupKey))
        val states =
          keysResult.keys.map { VerificationState.fromState(it.localVerificationState) }
        val contactStatus = VerificationState.leastVerifiedFrom(states)
        updateUi(contactLookupKey, contactStatus)
    } catch (e: Exception) {
        // Handle error.
    }
}
  • アカウント レベルで集計ステータスを取得する
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.contactkeys.constants.VerificationState
import com.google.android.gms.tasks.Tasks

suspend fun displayAccountVerificationStatus(
    contactKeyClient: ContactKeyClient,
    accountId: String
) {
    try {
        val keys = Tasks.await(contactKeyClient.getE2eeAccountKeysForAccount(accountId))
        val states = keys.map { VerificationState.fromState(it.localVerificationState) }
        val accountStatus = VerificationState.leastVerifiedFrom(states)
        updateUi(accountId, accountStatus)
    } catch (e: Exception) {
        // Handle error.
    }
}

鍵の変更をリアルタイムで確認する

アプリの UI に常に正しい信頼ステータスが表示されるようにするには、更新をリッスンする必要があります。推奨される方法は、Flow ベースの API を使用することです。この API は、登録済みアカウントのキーが追加、削除されたとき、または認証ステータスが変更されたときに、新しいキーのリストを出力します。これは、グループ会話のメンバー リストを最新の状態に保つ場合に特に便利です。鍵の検証ステータスは、次の場合に変更されることがあります。

  • ユーザーが確認フロー(QR コードのスキャンなど)を完了した。
  • 連絡先の鍵が変更され、以前に確認された値と一致しなくなった。
fun observeKeyUpdates(contactKeyClient: ContactKeyClient, accountIds: List<String>) {
    lifecycleScope.launch {
        contactKeyClient.getAccountContactKeysFlow(accountIds)
            .collect { updatedKeys ->
                // A key was added, removed, or updated.
                // Refresh your app's UI and internal state.
                refreshUi(updatedKeys)
            }
    }
}

対面でのキー検証を行う

ユーザーが鍵を検証する最も安全な方法は、対面での検証です。多くの場合、QR コードをスキャンするか、一連の数字を比較します。Key Verifier アプリは、このプロセス用の標準 UI フローを提供します。アプリはこのフローを起動できます。検証を試行すると、API は鍵の検証ステータスを自動的に更新します。鍵の更新をモニタリングしている場合、アプリに通知が届きます。

  • ユーザーが選択した連絡先のキー検証プロセスを開始する 選択した連絡先の lookupKey を使用して、getScanQrCodeIntent が提供する PendingIntent を起動します。UI を使用すると、ユーザーは指定した連絡先のすべての鍵を確認できます。
import android.app.ActivityOptions
import android.app.PendingIntent
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun initiateVerification(contactKeyClient: ContactKeyClient, lookupKey: String) {
    try {
        val pendingIntent = Tasks.await(contactKeyClient.getScanQrCodeIntent(lookupKey))
        val options =
          ActivityOptions.makeBasic()
            .setPendingIntentBackgroundActivityStartMode(
              ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
            )
            .toBundle()
        pendingIntent.send(options)
    } catch (e: Exception) {
        // Handle error.
    }
}
  • ユーザーが選択したアカウントのキー検証プロセスを開始する ユーザーが連絡先(または連絡先の特定のアカウント)に直接リンクされていないアカウントを検証する場合は、getScanQrCodeIntentForAccount が提供する PendingIntent を起動できます。通常は、独自のアプリのパッケージ名とアカウント ID に使用されます。
import android.app.ActivityOptions
import android.app.PendingIntent
import com.google.android.gms.contactkeys.ContactKeyClient
import com.google.android.gms.tasks.Tasks

suspend fun initiateVerification(contactKeyClient: ContactKeyClient, packageName: String, accountId: String) {
    try {
        val pendingIntent = Tasks.await(contactKeyClient.getScanQrCodeIntentForAccount(packageName, accountId))
        // Allow activity start from background on Android SDK34+
        val options =
          ActivityOptions.makeBasic()
            .setPendingIntentBackgroundActivityStartMode(
              ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED
            )
            .toBundle()
        pendingIntent.send(options)
    } catch (e: Exception) {
        // Handle error.
    }
}