Android System Key Verifier

Key Verifier para Android proporciona una forma unificada y segura para que los usuarios verifiquen que se están comunicando con la persona correcta en tu app con encriptación de extremo a extremo (E2EE). Protege a los usuarios de los ataques de intermediario, ya que les permite confirmar la autenticidad de las claves de encriptación públicas de un contacto a través de una IU del sistema confiable y coherente.

Esta función la proporciona Key Verifier, un servicio del sistema que forma parte de los Servicios del sistema de Google y se distribuye a través de Play Store. Actúa como un repositorio centralizado en el dispositivo para las claves públicas de E2EE.

Por qué deberías integrar Key Verifier

  • Proporciona una UX unificada: En lugar de crear tu propio flujo de verificación, puedes iniciar la IU estándar del sistema, lo que les brinda a los usuarios una experiencia coherente y confiable en todas sus apps.
  • Aumenta la confianza de los usuarios: Un estado de verificación claro y respaldado por el sistema garantiza a los usuarios que sus conversaciones son seguras y privadas.
  • Reduce la sobrecarga de desarrollo: Descarga la complejidad de la IU de verificación de claves, el almacenamiento y la administración de estados en el servicio del sistema.

Términos clave

  • lookupKey: Es un identificador opaco y persistente para un contacto, almacenado en la columna LOOKUP_KEY del proveedor de contactos. A diferencia de un contact ID, un lookupKey permanece estable incluso si se cambian o se combinan los detalles de contacto subyacentes, lo que lo convierte en la forma recomendada de hacer referencia a un contacto.
  • accountId: Es un identificador específico de la app para la cuenta de un usuario en un dispositivo. Tu app define este ID, que ayuda a distinguir entre las distintas cuentas que puede tener un solo usuario. Se muestra al usuario en la IU. Se recomienda usar algo significativo, como un número de teléfono, una dirección de correo electrónico o un identificador de usuario.
  • deviceId: Es un identificador único para un dispositivo específico asociado con la cuenta de un usuario. Esto permite que un usuario tenga varios dispositivos, cada uno con su propio conjunto de claves criptográficas. No necesariamente representa un dispositivo físico, pero podría ser una forma de distinguir entre varias claves que se usan para la misma cuenta.

Cómo comenzar

Antes de comenzar, configura tu app para que se comunique con el servicio de Key Verifier.

Declara permisos: En tu archivo AndroidManifest.xml, declara los siguientes permisos. También debes solicitarlos al usuario durante el tiempo de ejecución.

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

Obtén la instancia del cliente: Obtén una instancia de ContactKeys, que es tu punto de entrada a la API.

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

val contactKeyClient = ContactKeys.getClient(context)

Orientación para desarrolladores de apps de mensajería

Como desarrollador de una app de mensajería, tu función principal es publicar las claves públicas de tus usuarios y las de sus contactos en el servicio de Key Verifier.

Publica las claves públicas de un usuario

Para permitir que otras personas encuentren y verifiquen tu usuario, publica su clave pública en el repositorio del dispositivo. Para mayor seguridad, considera crear claves en el almacén de claves de 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.
    }
}

Asocia claves públicas con contactos

Cuando tu app recibe una clave pública para uno de los contactos del usuario, debes almacenarla y asociarla con ese contacto en el repositorio central. Esto permite que se verifique la clave y que otras apps muestren el estado de verificación del contacto. Para ello, necesitas el lookupKey del contacto del proveedor de contactos de Android. Por lo general, esto se activa cuando se recupera una clave del servidor de distribución de claves o durante una sincronización periódica de las claves 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.
    }
}

Recupera claves y el estado de verificación

Después de publicar las claves, los usuarios podrán verificarlas escaneando el código QR en persona. La IU de tu app debe reflejar si una conversación usa una clave verificada. Cada clave tiene un estado de verificación que puedes usar para informar tu IU.

Comprende los estados de verificación:

  • UNVERIFIED: Este es el estado predeterminado para cada clave nueva. Esto significa que la clave existe, pero el usuario aún no confirmó su autenticidad. En tu IU, debes tratar esto como un estado neutral y, por lo general, no mostrar ningún indicador especial.

  • VERIFIED: Este estado indica un alto nivel de confianza. Esto significa que el usuario completó correctamente un flujo de verificación (como un escaneo de código QR) y confirmó que la clave pertenece al contacto previsto. En tu IU, debes mostrar un indicador claro y positivo, como una marca de verificación o un escudo verdes.

  • VERIFICATION_FAILED: Este es un estado de advertencia. Significa que la clave asociada con el contacto no coincide con la clave que se verificó anteriormente. Esto puede ocurrir si un contacto obtiene un dispositivo nuevo, pero también podría indicar un posible riesgo de seguridad. En tu IU, alerta al usuario con una advertencia destacada y sugiérele que vuelva a verificar la información antes de enviar información sensible.

Puedes recuperar un estado agregado para todas las claves asociadas a un contacto. Recomendamos usar VerificationState.leastVerifiedFrom() para resolver el estado cuando hay varias claves, ya que priorizará correctamente VERIFICATION_FAILED sobre VERIFIED.

  • Cómo obtener el estado agregado a nivel del contacto
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.
    }
}
  • Cómo obtener el estado agregado a nivel de la cuenta
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.
    }
}

Observa los cambios clave en tiempo real

Para verificar que la IU de tu app siempre muestre el estado de confianza correcto, debes escuchar las actualizaciones. La forma recomendada es usar la API basada en flujos, que emite una nueva lista de claves cada vez que se agrega o quita una clave para una cuenta suscrita, o cuando cambia su estado de verificación. Esto es especialmente útil para mantener actualizada la lista de miembros de una conversación grupal. El estado de verificación de una llave puede cambiar en los siguientes casos:

  • El usuario completa correctamente un flujo de verificación (por ejemplo, el escaneo de un código QR).
  • Se modificó la clave de un contacto, por lo que ya no coincide con el valor verificado anteriormente.
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)
            }
    }
}

Cómo realizar la verificación de claves en persona

La forma más segura para que los usuarios verifiquen una clave es a través de la verificación en persona, a menudo escaneando un código QR o comparando una secuencia de números. La app de Key Verifier proporciona flujos de IU estándar para este proceso, que tu app puede iniciar. Después de un intento de verificación, la API actualiza automáticamente el estado de verificación de la clave, y tu app recibirá una notificación si observas actualizaciones de la clave.

  • Inicia el proceso de verificación de la clave para un contacto seleccionado por el usuario Inicia el PendingIntent proporcionado por getScanQrCodeIntent con el lookupKey del contacto seleccionado. La IU permite que el usuario verifique todas las claves del contacto determinado.
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.
    }
}
  • Cómo iniciar el proceso de verificación de claves para una cuenta seleccionada por el usuario Si el usuario desea verificar una cuenta que no está vinculada directamente a un contacto (o a una cuenta específica de un contacto), puedes iniciar el PendingIntent proporcionado por getScanQrCodeIntentForAccount. Por lo general, se usa para el nombre del paquete y el ID de la cuenta de tu propia app.
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.
    }
}