Outil de vérification de clés du système Android

Key Verifier pour Android offre aux utilisateurs un moyen unifié et sécurisé de vérifier qu'ils communiquent avec la bonne personne dans votre application chiffrée de bout en bout. Il protège les utilisateurs contre les attaques de l'homme du milieu en leur permettant de confirmer l'authenticité des clés de chiffrement publiques d'un contact grâce à une UI système fiable et cohérente.

Cette fonctionnalité est fournie par Key Verifier, un service système qui fait partie des services système Google et qui est distribué à l'aide du Play Store. Il fait office de dépôt centralisé sur l'appareil pour les clés publiques E2EE.

Pourquoi intégrer Key Verifier ?

  • Offrez une UX unifiée : au lieu de créer votre propre flux de validation, vous pouvez lancer l'UI standard du système, ce qui offre aux utilisateurs une expérience cohérente et fiable dans toutes leurs applications.
  • Renforcer la confiance des utilisateurs : un état de validation clair et soutenu par le système assure aux utilisateurs que leurs conversations sont sécurisées et privées.
  • Réduisez les frais généraux de développement : déchargez la complexité de l'interface utilisateur de validation des clés, du stockage et de la gestion de l'état sur le service système.

Termes clés

  • lookupKey : identifiant opaque et persistant d'un contact, stocké dans la colonne LOOKUP_KEY du fournisseur de contacts. Contrairement à un contact ID, un lookupKey reste stable même si les coordonnées sous-jacentes sont modifiées ou fusionnées. Il s'agit donc de la méthode recommandée pour référencer un contact.
  • accountId : identifiant spécifique à l'application pour le compte d'un utilisateur sur un appareil. Cet ID est défini par votre application et permet de faire la distinction entre les différents comptes qu'un même utilisateur peut posséder. Cette valeur est affichée dans l'interface utilisateur. Il est recommandé d'utiliser une valeur pertinente, comme un numéro de téléphone, une adresse e-mail ou un nom d'utilisateur.
  • deviceId : identifiant unique d'un appareil spécifique associé au compte d'un utilisateur. Cela permet à un utilisateur de disposer de plusieurs appareils, chacun avec son propre ensemble de clés cryptographiques. Ne représente pas nécessairement un appareil physique, mais peut être un moyen de faire la distinction entre plusieurs clés utilisées pour le même compte.

Premiers pas

Avant de commencer, configurez votre application pour qu'elle communique avec le service Key Verifier.

Déclarez les autorisations : dans votre fichier AndroidManifest.xml, déclarez les autorisations suivantes. Vous devez également les demander à l'utilisateur au moment de l'exécution.

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

Obtenez l'instance du client : obtenez une instance de ContactKeys, qui est votre point d'entrée dans l'API.

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

val contactKeyClient = ContactKeys.getClient(context)

Conseils pour les développeurs d'applications de chat

En tant que développeur d'application de messagerie, votre rôle principal consiste à publier les clés publiques de vos utilisateurs et celles de leurs contacts sur le service Key Verifier.

Publier les clés publiques d'un utilisateur

Pour permettre à d'autres utilisateurs de trouver et de valider votre utilisateur, publiez sa clé publique dans le dépôt sur l'appareil. Pour plus de sécurité, envisagez de créer des clés dans le Keystore 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.
    }
}

Associer des clés publiques à des contacts

Lorsque votre application reçoit une clé publique pour l'un des contacts de l'utilisateur, vous devez la stocker et l'associer à ce contact dans le dépôt central. Cela permet de valider la clé et d'autoriser d'autres applications à afficher l'état de validation du contact. Pour ce faire, vous avez besoin de la lookupKey du contact à partir du fournisseur de contacts Android. Cela se produit généralement lors de la récupération d'une clé à partir de votre serveur de distribution de clés ou lors d'une synchronisation périodique des clés locales.

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.
    }
}

Récupérer les clés et l'état de validation

Une fois les clés publiées, les utilisateurs peuvent les valider en scannant un code QR en personne. L'UI de votre application doit indiquer si une conversation utilise une clé validée. Chaque clé possède un état de validation que vous pouvez utiliser pour informer votre UI.

Comprendre les états de validation :

  • UNVERIFIED : il s'agit de l'état par défaut de chaque nouvelle clé. Cela signifie que la clé existe, mais que l'utilisateur n'a pas encore confirmé son authenticité. Dans votre UI, vous devez le traiter comme un état neutre et ne pas afficher d'indicateur spécial.

  • VERIFIED : cet état indique un niveau de confiance élevé. Cela signifie que l'utilisateur a terminé avec succès un flux de validation (comme le scan d'un code QR) et confirmé que la clé appartient au contact prévu. Dans votre UI, vous devez afficher un indicateur clair et positif, tel qu'une coche ou un bouclier verts.

  • VERIFICATION_FAILED : il s'agit d'un état d'avertissement. Cela signifie que la clé associée au contact ne correspond pas à celle qui a été validée précédemment. Cela peut se produire si un contact fait l'acquisition d'un nouvel appareil, mais cela peut également indiquer un risque de sécurité potentiel. Dans votre UI, alertez l'utilisateur avec un avertissement bien visible et suggérez-lui de valider à nouveau son identité avant d'envoyer des informations sensibles.

Vous pouvez récupérer un état global pour toutes les clés associées à un contact. Nous vous recommandons d'utiliser VerificationState.leastVerifiedFrom() pour résoudre l'état lorsque plusieurs clés sont présentes, car il donnera correctement la priorité à VERIFICATION_FAILED par rapport à VERIFIED.

  • Obtenir l'état agrégé au niveau d'un contact
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.
    }
}
  • Obtenir l'état agrégé au niveau d'un compte
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.
    }
}

Observer les principaux changements en temps réel

Pour vérifier que l'UI de votre application affiche toujours le bon état d'approbation, vous devez écouter les mises à jour. La méthode recommandée consiste à utiliser l'API basée sur les flux, qui émet une nouvelle liste de clés chaque fois qu'une clé pour un compte abonné est ajoutée ou supprimée, ou que son état de validation est modifié. Cela est particulièrement utile pour tenir à jour la liste des membres d'une conversation de groupe. L'état de validation d'une clé peut changer dans les cas suivants :

  • L'utilisateur termine un flux de validation (par exemple, en scannant un code QR).
  • La clé d'un contact est modifiée et ne correspond plus à la valeur précédemment validée.
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)
            }
    }
}

Valider une clé en personne

La méthode la plus sécurisée pour valider une clé consiste à le faire en personne, souvent en scannant un code QR ou en comparant une séquence de chiffres. L'application Key Verifier fournit des flux d'interface utilisateur standards pour ce processus, que votre application peut lancer. Après une tentative de validation, l'API met automatiquement à jour l'état de validation de la clé. Votre application recevra une notification si vous surveillez les mises à jour de la clé.

  • Démarrer le processus de validation de clé pour un contact sélectionné par l'utilisateur Lancez l'PendingIntent fourni par getScanQrCodeIntent à l'aide de l'lookupKey du contact sélectionné. L'UI permet à l'utilisateur de vérifier toutes les clés du contact donné.
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.
    }
}
  • Démarrer le processus de validation de clé pour un compte sélectionné par l'utilisateur Si l'utilisateur souhaite valider un compte qui n'est pas directement associé à un contact (ou un compte spécifique d'un contact), vous pouvez lancer PendingIntent fourni par getScanQrCodeIntentForAccount. Il est généralement utilisé pour le nom de package et l'ID de compte de votre propre application.
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.
    }
}