Las APIs de Credentials Transfer del Administrador de credenciales permiten la transferencia segura, en el mismo dispositivo, de las credenciales de usuario entre los proveedores de credenciales. En esta guía, se detalla
cómo los proveedores de credenciales en Android pueden integrarse 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 par a par en el mismo dispositivo sin exponer las credenciales sin procesar al SO Android ni a las apps no autenticadas.
El framework define dos roles principales:
- Exportador (proveedor de origen): Es un proveedor de credenciales que actualmente contiene credenciales de usuario. Registra previamente los metadatos sobre las cuentas exportables disponibles (
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 archivo 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 cambie 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 de cadena secreto generado de forma aleatoria que representa de forma inequívoca 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: Es la etiqueta de cuenta opcional (por ejemplo,"Personal Account").userDisplayName: Es el identificador principal del usuario (por ejemplo,"alice@example.com").icon: Es un íconoBitmapque representa al proveedor o la cuenta (la biblioteca lo escala automáticamente a un PNG de 32 x 32).supportedCredentialTypes: Es un conjunto de constantes de cadena deCredentialTypesque 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 el Activity de manejo designado. Declara esta actividad con la acción de intent obligatoria y el esquema de URI de contenido.
Para protegerte contra invocaciones no autorizadas y ataques de diputado confundido, declara android:requireContentUriPermissionFromCaller="write" en Android 14 (nivel de API 34) y versiones posteriores. Esto indica al sistema operativo Android que verifique que el emisor que inicia el lanzamiento tenga permisos de escritura en 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 solicitar metadatos, realizar cualquier
autenticación biométrica requerida y volver a escribir la carga útil de FIDO CXF en el
URI de contenido proporcionado.
Autentica la solicitud
Debido a que se exporta tu actividad, protege el flujo de exportación de credenciales verificando cada una de las siguientes capas:
- Verifica el nonce de la sesión de
credId(defensa principal): Cuando registres tuExportEntry, genera unid(nonce) de alta entropía y aleatorio criptográficamente, y asócialo con una sesión de corta duración. En tu actividad, verifica querequest.credIdcoincida con un nonce de sesión activo y no vencido que haya emitido tu proveedor con anterioridad. Como las apps de terceros no pueden adivinar este nonce, se garantiza que el intent corresponda a una selección de usuario auténtica en el selector del sistema. - Verifica
Activity.getCallingPackage()(verificación del emisor): Debido a que el selector de credenciales inicia tu actividad constartActivityForResult, tu actividad puede inspeccionargetCallingPackage()para confirmar que el emisor inmediato que inicia la IU es el framework del selector de credenciales del sistema de confianza (en dispositivos Android con los Servicios de Google Play,"com.google.android.gms"). Rechaza la solicitud si no se reconoce el paquete emisor. - Inspecciona
request.callingAppInfo(información de la app de destino): ElcallingAppInfoidentifica la aplicación de destino que recibirá las credenciales importadas. Puedes usar esta información como consideres adecuado para la transparencia (como mostrar el nombre y el ícono de la app de destino en tu IU de confirmación) o 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() } }
Excepciones de registro y compensación de exportaciones
Todas las excepciones de este módulo son subclases de
ImportCredentialsException, RegisterExportException o
ClearExportException.
RegisterExportProviderConfigurationExceptionoClearExportProviderConfigurationException: Se arroja cuando el registro o la eliminación fallan debido a problemas de configuración del proveedor (por ejemplo,supportedCredentialTypesvacío en unExportEntryo llamada en un nivel de SO no compatible).RegisterExportUnknownErrorExceptionoClearExportUnknownErrorException: Se produjo un error de almacenamiento o del sistema no clasificado mientras se actualizaba el registro de exportación.
Implementa el importador
Para importar credenciales a tu app (por ejemplo, durante la incorporación o la importación del proveedor), implementa el rol de importador y, luego, inicia el flujo llamando a ProviderEventsManager.importCredentials().
Crea la solicitud de importación y el flujo de lanzamiento
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) {}
Excepciones de flujo de importación
Todas las excepciones de este módulo son subclases de
ImportCredentialsException, RegisterExportException o
ClearExportException.
ImportCredentialsCancellationException: El usuario descartó la IU del selector o canceló la actividad de exportación (Activity.RESULT_CANCELED).ImportCredentialsNoExportOptionException: Ninguna entrada de exportación registrada coincidió con los tipos de credenciales solicitados, o el exportador seleccionado arrojó una excepción de rechazo.ImportCredentialsProviderConfigurationException: Es un error de configuración (por ejemplo, uncredentialTypesvacío establecido enImportCredentialsRequesto faltan permisos).ImportCredentialsInvalidJsonException: El exportador devolvió una carga útil de JSON con formato incorrecto o vacía que no pasó la validación de la solicitud.ImportCredentialsSystemErrorException: Se produjo un error interno del sistema Android o de transferencia de Binder durante la importación.ImportCredentialsUnknownCallerException: El framework no pudo verificar la app de llamadas.ImportCredentialsUnknownErrorException: Es un error no clasificado o inesperado durante el flujo de importación.
Tipos de credenciales y extensiones compatibles
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 con nombre de usuario y contraseña |
CREDENTIAL_TYPE_PUBLIC_KEY |
"passkey" |
Credenciales de clave pública de la llave de acceso de FIDO2 o WebAuthn. |
CREDENTIAL_TYPE_ADDRESS |
"address" |
Información de la dirección postal o de envío para autocompletar 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" |
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" |
Documentos nacionales de identidad, SSN, TIN o referencias de pasaportes |
CREDENTIAL_TYPE_ITEM_REFERENCE |
"item-reference" |
Vínculos lógicos que apuntan a otro elemento de la carga útil |
CREDENTIAL_TYPE_NOTE |
"note" |
Notas seguras definidas por el usuario (cadena UTF-8). |
CREDENTIAL_TYPE_PASSPORT |
"passport" |
Detalles del documento de viaje del pasaporte. |
CREDENTIAL_TYPE_PERSON_NAME |
"person-name" |
Detalles de identidad y nombres de personas |
CREDENTIAL_TYPE_SSH_KEY |
"ssh-key" |
Pares de claves públicas y privadas de SSH. |
CREDENTIAL_TYPE_TOTP |
"totp" |
Secretos de contraseñas de un solo uso basadas en el tiempo (2FA) |
CREDENTIAL_TYPE_WIFI |
"wifi" |
SSID y frases de contraseña de la red Wi-Fi |
Comparadores de WASM (WebAssembly) personalizados (avanzado)
De forma predeterminada, cuando se llama a RegisterExportRequest.create(context, entries), se agrupa el credential_transfer_matcher.wasm predeterminado del sistema de los recursos de la biblioteca, que filtra las entradas solo 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)