Transferencia de credenciales

Las APIs de transferencia de credenciales del Administrador de credenciales permiten la transferencia segura de credenciales del usuario entre proveedores de credenciales en el mismo dispositivo. En esta guía, se detalla cómo los proveedores de credenciales en Android pueden realizar la integración con las APIs que proporciona la biblioteca de androidx.credentials:providerevents. Esta función admite contraseñas, llaves de acceso, información de direcciones y campos personalizados con el formato de intercambio de credenciales (CXF) de FIDO estandarizado.

Conceptos básicos

El framework de transferencia de credenciales facilita la transferencia de credenciales de punto a punto en el mismo dispositivo sin exponer las credenciales sin procesar al SO Android ni a las apps no autenticadas.

El marco de trabajo define dos roles principales:

  • Exportador (proveedor de origen): Es un proveedor de credenciales que actualmente tiene credenciales de usuario. Pre registra metadatos sobre las cuentas disponibles que se pueden exportar (ExportEntry) con el sistema y responde a las solicitudes de transferencia cuando el usuario las selecciona.
  • Importador (proveedor de clientes o asistente de configuración): Es un proveedor de credenciales o un asistente de configuración que inicia una solicitud de importación (ImportCredentialsRequest) en la que se especifican los tipos de credenciales y extensiones que puede recibir.

Compatibilidad con versiones de Android

La API de Credentials Transfer funciona en dispositivos que ejecutan Android 8 (nivel de API 26) y versiones posteriores.

Cómo agregar dependencias

Agrega la dependencia androidx.credentials:providerevents al build.gradle o build.gradle.kts de tu módulo:

dependencies {
    implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}

Crea una instancia de la clase requerida

Crea una instancia del ProviderEventsManager requerido.

val providerEventsManager = ProviderEventsManager.create(context)

Implementa el exportador

Para permitir que los usuarios exporten credenciales de tu app a otros proveedores de credenciales en el dispositivo, implementa el rol de exportador.

Registra entradas de exportación

Cuando cambia tu proveedor de credenciales (por ejemplo, un usuario accede, agrega credenciales o modifica cuentas), registra o actualiza tus elementos ExportEntry con ProviderEventsManager.registerExport().

Cada ExportEntry requiere lo siguiente:

  • id: Es un identificador secreto generado de forma aleatoria que representa de manera única esta entrada de exportación. Debes conservar este ID de forma segura, ya que lo necesitarás más adelante para verificar las solicitudes de transferencia entrantes.
  • accountDisplayName: Etiqueta de cuenta opcional (por ejemplo, "Personal Account").
  • userDisplayName: Es el identificador principal del usuario (por ejemplo, "alice@example.com").
  • icon: Es un ícono Bitmap que representa el proveedor o la cuenta (la biblioteca lo ajusta automáticamente a un PNG de 32 x 32).
  • supportedCredentialTypes: Es un conjunto de constantes de cadena de CredentialTypes que representan los tipos que contiene esta entrada.

suspend fun registerMyProviderForExport(
    providerEventsManager: ProviderEventsManager,
    providerIcon: Bitmap,
    // Randomly generated and stored in encrypted storage
    secretEntryId: String
) {
    val entry = ExportEntry(
        id = secretEntryId,
        accountDisplayName = "MyProvider Personal",
        userDisplayName = "alice@example.com",
        icon = providerIcon,
        supportedCredentialTypes = setOf(
            CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, // Passwords
            CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, // Passkeys
            CredentialTypes.CREDENTIAL_TYPE_ADDRESS,
            CredentialTypes.CREDENTIAL_TYPE_CREDIT_CARD
        )
    )

    // RegisterExportRequest.create() attaches the default WASM matcher from assets
    val request = RegisterExportRequest.create(context, listOf(entry))

    try {
        val response = providerEventsManager.registerExport(request)
        // Registration successful
    } catch (e: Exception) {
        // Handle registration exceptions (e.g., RegisterExportProviderConfigurationException)
    }
}

Declara la actividad del exportador en el archivo de manifiesto

Cuando el usuario selecciona tu ExportEntry en la IU del selector del sistema, este inicia tu Activity de control designado. Declara esta actividad con la acción de intent y el esquema de URI de contenido obligatorios.

Para protegerse contra las invocaciones no autorizadas y los ataques de confusión de diputados, declara android:requireContentUriPermissionFromCaller="write" en Android 14 (nivel de API 34) y versiones posteriores. Esto indica al sistema operativo Android que verifique que el llamador que inicia el lanzamiento tenga permisos de escritura para el URI de contenido de destino antes de permitir que se inicie la actividad, como se muestra en el siguiente ejemplo:

<activity
    android:name="com.example.CredentialExportActivity"
    android:exported="true"
    android:requireContentUriPermissionFromCaller="write"
    android:label="@string/export_activity_label">
    <intent-filter>
        <!-- This intent action is required for Credential Manager to invoke this activity -->
        <action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="content" />
    </intent-filter>
</activity>

Cómo controlar el intent de transferencia en tu actividad

En tu CredentialExportActivity, usa IntentHandler.retrieveProviderImportCredentialsRequest(intent) para analizar la solicitud, verificar el componente de llamada y los metadatos de la solicitud, realizar cualquier autenticación biométrica requerida y escribir la carga útil de FIDO CXF en el URI de contenido proporcionado.

Autentica la solicitud

Como tu actividad se exporta, protege el flujo de exportación de credenciales verificando cada una de las siguientes capas:

  1. Verifica el nonce de sesión credId (defensa principal): Cuando registres tu ExportEntry, genera un id (nonce) criptográficamente aleatorio y de alta entropía, y asócialo con una sesión de corta duración. En tu actividad, verifica que request.credId coincida con un nonce de sesión activo y no vencido que haya emitido tu proveedor anteriormente. Como las apps de terceros no pueden adivinar este nonce, se garantiza que el intent corresponda a una selección auténtica del usuario en el selector del sistema.
  2. Verificar Activity.getCallingPackage() (verificación de la persona que llama): Dado que el selector de credenciales inicia tu actividad con startActivityForResult, tu actividad puede inspeccionar getCallingPackage() para confirmar que la persona que llama inmediata que inicia la IU es el framework de selector de credenciales del sistema de confianza (en dispositivos Android con Servicios de Google Play, "com.google.android.gms"). Rechaza la solicitud si no se reconoce el paquete que llama.
  3. Inspect request.callingAppInfo (información de la app de destino): El callingAppInfo identifica la aplicación de destino que recibirá las credenciales importadas. Puedes usar esta información como mejor te parezca para la transparencia (por ejemplo, mostrar el nombre y el ícono de la app de destino en la IU de confirmación) o para verificar si la aplicación de destino es de confianza.

class CredentialExportActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 1. Extract the transfer request from the incoming Intent
        val request: ProviderImportCredentialsRequest? =
            IntentHandler.retrieveProviderImportCredentialsRequest(intent)

        if (request == null) {
            finishWithError()
            return
        }

        // 2. Validate CallingAppInfo and secret `credId`
        val callingAppPackage = request.callingAppInfo.packageName
        val receivedCredId = request.credId
        if (!verifySecretEntryId(receivedCredId) || !isTrustedImporter(callingAppPackage)) {
            // Secret ID mismatch or untrusted caller -> abort
            sendExceptionAndFinish(ImportCredentialsNoExportOptionException("Unauthorized request"))
            return
        }

        // 3. Optional: Prompt user for Biometric / PIN authentication before exporting
        authenticateUserThenExport(request)
    }

    private fun authenticateUserThenExport(request: ProviderImportCredentialsRequest) {
        // ... Biometric prompt logic ...
        // Once authenticated, generate the FIDO CXF JSON string matching the requested types
        val cxfJsonPayload = buildFidoCxfJsonPayload(
            requestedTypes = request.request.credentialTypes,
            requestedExtensions = request.request.knownExtensions
        )

        val response = ImportCredentialsResponse(cxfJsonPayload)

        // 4. Write the JSON payload to the Content URI and set Activity result
        IntentHandler.setImportCredentialsResponse(
            context = this,
            uri = request.uri,
            intent = intent,
            response = response
        )
        setResult(Activity.RESULT_OK, intent)
        finish()
    }

    private fun sendExceptionAndFinish(exception: androidx.credentials.providerevents.exception.ImportCredentialsException) {
        IntentHandler.setImportCredentialsException(intent, exception)
        setResult(Activity.RESULT_OK, intent)
        finish()
    }

    private fun verifySecretEntryId(credentialId: String): Boolean {
        // Check if credentialId matches what you stored when calling RegisterExportRequest
        return credentialId == getStoredSecretEntryId()
    }

    private fun isTrustedImporter(packageName: String): Boolean {
        // Implement any specific allowlisting / caller checks if required
        return true
    }

    private fun finishWithError() {
        setResult(Activity.RESULT_CANCELED)
        finish()
    }
}

Exporta excepciones de registro y autorización

Todas las excepciones de este módulo son subclases de ImportCredentialsException, RegisterExportException o ClearExportException.

Implementa el importador

Para importar credenciales a tu app (por ejemplo, durante la incorporación o la importación de proveedores), implementa el rol de importador y llama a ProviderEventsManager.importCredentials() para iniciar el flujo.

Construye la solicitud de importación y el flujo de inicio

Especifica qué CredentialTypes y KnownExtensions admite tu importador:

suspend fun startCredentialImport(
    activityContext: Context,
    providerEventsManager: ProviderEventsManager
) {
    val importRequest = ImportCredentialsRequest(
        credentialTypes = setOf(
            CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH,
            CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY,
            CredentialTypes.CREDENTIAL_TYPE_ADDRESS,
            CredentialTypes.CREDENTIAL_TYPE_NOTE
        ),
        knownExtensions = setOf(
            KnownExtensions.KNOWN_EXTENSION_SHARED
        )
    )

    try {
        // Launches the system Selector UI; suspends until user selects a provider and completes transfer
        val response = providerEventsManager.importCredentials(activityContext, importRequest)

        // 1. Inspect the source exporter's package info
        val exporterPackageName = response.callingAppInfo.packageName

        // 2. Parse the FIDO CXF JSON string
        val cxfJsonString = response.response.responseJson
        parseAndSaveImportedCredentials(cxfJsonString)
    } catch (e: ImportCredentialsException) {
        // Handle specific import exceptions (e.g., ImportCredentialsCancellationException)
        handleImportFailure(e)
    }
}

private fun parseAndSaveImportedCredentials(cxfJsonString: String) {
    val rootJson = JSONObject(cxfJsonString)
    // Parse according to FIDO Credential Exchange Format (CXF v1.0) specification:
    // https://fidoalliance.org/specs/cx/cxf-v1.0-ps-20250814.html
}

// Helper function to make it compile
private fun handleImportFailure(e: ImportCredentialsException) {}

Importa excepciones de flujo

Todas las excepciones de este módulo son subclases de ImportCredentialsException, RegisterExportException o ClearExportException.

Tipos de credenciales y extensiones admitidos

El objeto androidx.credentials.providerevents.transfer.CredentialTypes define constantes de cadena estandarizadas que corresponden a los tipos de elementos de CXF de FIDO:

Constante Valor (tipo cxf) Descripción
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Credenciales de acceso (nombre de usuario y contraseña)
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" Son las credenciales de clave pública de la llave de acceso de FIDO2 o WebAuthn.
CREDENTIAL_TYPE_ADDRESS "address" Es la información de la dirección postal o de envío para el autocompletado de formularios.
CREDENTIAL_TYPE_API_KEY "api-key" Claves y tokens de acceso a la API
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" Información de pago con tarjeta de crédito y débito
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" Agrupaciones personalizadas o campos definidos por el usuario
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Detalles de la licencia de conducir
CREDENTIAL_TYPE_FILE "file" Son referencias de metadatos y marcadores de posición para archivos binarios.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Contraseñas seguras generadas por máquinas
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Referencias a documentos nacionales de identidad, números de la Seguridad Social, números de identificación fiscal o pasaportes
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Son vínculos lógicos que apuntan a otro elemento en la carga útil.
CREDENTIAL_TYPE_NOTE "note" Notas seguras definidas por el usuario (cadena UTF-8).
CREDENTIAL_TYPE_PASSPORT "passport" Son los detalles del documento de viaje del pasaporte.
CREDENTIAL_TYPE_PERSON_NAME "person-name" Detalles de la identidad y el nombre de la persona
CREDENTIAL_TYPE_SSH_KEY "ssh-key" Pares de claves SSH públicas y privadas
CREDENTIAL_TYPE_TOTP "totp" Son los secretos de contraseñas de un solo uso basadas en el tiempo (2FA).
CREDENTIAL_TYPE_WIFI "wifi" SSID y contraseñas de redes Wi-Fi

Matchers personalizados de WASM (WebAssembly) (avanzado)

De forma predeterminada, cuando se llama a RegisterExportRequest.create(context, entries), se incluye el credential_transfer_matcher.wasm predeterminado del sistema desde los recursos de la biblioteca, lo que filtra las entradas únicamente en función de la intersección de supportedCredentialTypes.

Si tu proveedor de credenciales requiere una lógica de coincidencia compleja (por ejemplo, verificaciones de capacidades dinámicas o filtrado condicional basado en campos personalizados), puedes compilar tu propio módulo de WebAssembly que coincida con la API de transferencia de credenciales de WASM y pasar el array de bytes sin procesar directamente:

// Loading a custom WASM matcher
val customMatcherBytes = context.assets.open("my_custom_matcher.wasm").use { it.readBytes() }
val customRequest = RegisterExportRequest(
    entries = myEntries,
    exportMatcher = customMatcherBytes
)
providerEventsManager.registerExport(customRequest)