Android 適用的金鑰驗證工具提供統一且安全的方式,讓使用者在端對端加密 (E2EE) 應用程式中,確認與他們通訊的對象是否正確。這項工具可讓使用者透過值得信賴且一致的系統 UI,確認聯絡人公開加密金鑰的真實性,防範中間人攻擊。
這項功能是由 Key Verifier 提供,這項系統服務是 Google 系統服務的一部分,並透過 Play 商店發布。這個服務可做為裝置上的中央存放區,集中管理 E2EE 公開金鑰。
與 Key Verifier 整合的好處
- 提供一致的使用者體驗:您可以啟動系統的標準 UI,不必自行建構驗證流程,讓使用者在所有應用程式中都能享有值得信賴的一致體驗。
- 提升使用者信心:明確的系統支援驗證狀態可確保對話安全無虞,讓使用者放心。
- 減少開發負擔:將金鑰驗證 UI、儲存空間和狀態管理作業的複雜性,轉移至系統服務。
重要詞彙
- lookupKey:聯絡人的不透明永久 ID,儲存在聯絡人供應商的 LOOKUP_KEY 欄中。與
contact ID
不同,即使基礎聯絡人詳細資料變更或合併,lookupKey
仍會保持穩定,因此建議您使用這項功能參照聯絡人。 - accountId:裝置上使用者帳戶的應用程式專屬 ID。 這個 ID 由應用程式定義,可協助區分單一使用者可能擁有的多個帳戶。這項資訊會顯示在 UI 中,建議使用有意義的資訊,例如電話號碼、電子郵件地址或使用者代碼
- deviceId:與使用者帳戶相關聯的特定裝置專屬 ID。這樣一來,使用者就能擁有多部裝置,每部裝置都有一組專屬的加密金鑰。不一定代表實體裝置,但可用於區分同一帳戶使用的多個金鑰
開始使用
開始之前,請先設定應用程式,以便與 Key Verifier 服務通訊。
宣告權限:在 AndroidManifest.xml 中,宣告下列權限。您也必須在執行階段向使用者要求這些權限。
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
取得用戶端例項:取得 ContactKeys
的例項,這是 API 的進入點。
import com.google.android.gms.contactkeys.ContactKeys
val contactKeyClient = ContactKeys.getClient(context)
訊息應用程式開發人員指南
身為訊息應用程式開發人員,您的主要職責是將使用者的公開金鑰和聯絡人的金鑰發布至金鑰驗證服務。
發布使用者的公開金鑰
如要允許他人尋找及驗證您的使用者,請將公開金鑰發布至裝置端存放區。為提升安全性,建議在 Android Keystore 中建立金鑰。
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.
}
}
將公開金鑰與聯絡人建立關聯
應用程式收到使用者某位聯絡人的公開金鑰時,必須儲存該金鑰,並在中央存放區中將其與該聯絡人建立關聯。這樣一來,金鑰就能通過驗證,其他應用程式也能顯示聯絡人的驗證狀態。如要這麼做,您需要 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 code 驗證金鑰。應用程式的 UI 應顯示對話是否使用已驗證的金鑰。每個金鑰都有驗證狀態,可供您在 UI 中使用。
瞭解驗證狀態:
UNVERIFIED
:這是每個新金鑰的預設狀態。這表示金鑰存在,但使用者尚未確認其真實性。在 UI 中,您應將此視為中性狀態,通常不會顯示任何特殊指標。VERIFIED
:這個狀態表示信任度高。這表示使用者已順利完成驗證流程 (例如掃描 QR code),並確認金鑰屬於預期聯絡人。在 UI 中,您應顯示清楚的正面指標,例如綠色勾號或盾牌。VERIFICATION_FAILED
:這是警告狀態。這表示與聯絡人相關聯的金鑰與先前驗證的金鑰不符。如果聯絡人換了新裝置,就可能發生這種情況,但這也可能代表有潛在安全風險。在 UI 中,向使用者發出醒目警告,建議他們先重新驗證,再傳送私密資訊。
您可以擷取與聯絡人相關聯的所有金鑰的匯總狀態。如果有多個金鑰,建議使用 VerificationState.leastVerifiedFrom()
解決狀態問題,因為系統會正確地將 VERIFICATION_FAILED
優先於 VERIFIED
。
- 取得聯絡人層級的匯總狀態
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 一律會顯示正確的信任狀態,請監聽更新。建議使用以流程為基礎的 API,每當訂閱帳戶新增或移除金鑰,或是金鑰的驗證狀態變更時,系統就會發出新的金鑰清單。這項功能特別適合用來更新群組對話的成員名單。金鑰的驗證狀態可能會在下列情況下變更:
- 使用者順利完成驗證流程 (例如掃描 QR code)。
- 聯絡人的金鑰已修改,因此不再符合先前驗證的值。
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 code 或比較一連串數字。「金鑰驗證器」應用程式會提供這個程序的標準 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.
}
}
- 為使用者選取的帳戶啟動金鑰驗證程序
如果使用者想驗證的帳戶並未直接連結至聯絡人
(或聯絡人的特定帳戶),您可以啟動
PendingIntent
提供的getScanQrCodeIntentForAccount
。這通常用於您自家應用程式的套件名稱和帳戶 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.
}
}