Transferência de credenciais

As APIs de transferência de credenciais do Gerenciador de credenciais permitem a transferência segura e no mesmo dispositivo de credenciais de usuário entre provedores de credenciais. Este guia detalha como os provedores de credenciais no Android podem se integrar às APIs fornecidas pela biblioteca androidx.credentials:providerevents. Esse recurso é compatível com senhas, chaves de acesso, informações de endereço e campos personalizados usando o Formato de troca de credenciais (CXF) padronizado do FIDO.

Principais conceitos

A estrutura de transferência de credenciais facilita a transferência de credenciais ponto a ponto no mesmo dispositivo sem expor credenciais brutas ao SO Android ou apps não autenticados.

A estrutura define duas funções principais:

  • Exportador (provedor de origem): um provedor de credenciais que atualmente detém credenciais de usuário. Ele pré-registra metadados sobre contas exportáveis disponíveis (ExportEntry) com o sistema e responde a solicitações de transferência quando selecionado pelo usuário.
  • Importador (provedor de cliente ou assistente de configuração): um provedor de credenciais ou assistente de configuração que inicia uma solicitação de importação (ImportCredentialsRequest) especificando quais tipos de credenciais e extensões ele pode receber.

Compatibilidade de versões do Android

A API Credentials Transfer funciona em dispositivos com Android 8 (nível 26 da API) e versões mais recentes.

Adicionar dependências

Adicione a dependência androidx.credentials:providerevents ao build.gradle ou build.gradle.kts do módulo:

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

Instanciar a classe necessária

Crie uma instância do ProviderEventsManager necessário.

val providerEventsManager = ProviderEventsManager.create(context)

Implementar o exportador

Para permitir que os usuários exportem credenciais do seu app para outros provedores no dispositivo, implemente a função de exportador.

Registrar entradas de exportação

Quando o provedor de credenciais muda (por exemplo, um usuário faz login, adiciona credenciais ou modifica contas), registre ou atualize os itens ExportEntry usando ProviderEventsManager.registerExport().

Cada ExportEntry requer:

  • id: um identificador secreto de string gerado aleatoriamente que representa de maneira exclusiva esta entrada de exportação. É necessário manter esse ID em segurança, porque você vai precisar dele depois para verificar as solicitações de transferência recebidas.
  • accountDisplayName: rótulo opcional da conta (por exemplo, "Personal Account").
  • userDisplayName: o identificador principal do usuário (por exemplo, "alice@example.com").
  • icon: um ícone Bitmap que representa o provedor ou a conta (dimensionado automaticamente para PNG de 32x32 pela biblioteca).
  • supportedCredentialTypes: um conjunto de constantes de string de CredentialTypes que representam os tipos que esta entrada contém.

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

Declarar a atividade do exportador no arquivo de manifesto

Quando o usuário seleciona seu ExportEntry na interface do seletor do sistema, ele inicia o Activity de processamento designado. Declare essa atividade com a ação da intent e o esquema de URI de conteúdo obrigatórios.

Para se proteger contra invocações não autorizadas e ataques de confusão de representantes, declare android:requireContentUriPermissionFromCaller="write" no Android 14 (nível 34 da API) e versões mais recentes. Isso instrui o sistema operacional Android a verificar se o chamador que inicia o lançamento tem permissões de gravação para o URI do conteúdo de destino antes de permitir que a atividade seja iniciada, conforme mostrado no exemplo a seguir:

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

Processar a intent de transferência na sua atividade

No seu CredentialExportActivity, use IntentHandler.retrieveProviderImportCredentialsRequest(intent) para analisar o pedido, verificar o componente de chamada e os metadados da solicitação, realizar qualquer autenticação biométrica necessária e gravar o payload do FIDO CXF de volta no URI de conteúdo fornecido.

Autenticar a solicitação

Como sua atividade é exportada, proteja o fluxo de exportação de credenciais verificando cada uma das seguintes camadas:

  1. Verifique o nonce da sessão credId (defesa principal): ao registrar seu ExportEntry, gere um id (nonce) criptograficamente aleatório e de alta entropia e associe-o a uma sessão de curta duração. Na sua atividade, verifique se request.credId corresponde a um nonce de sessão ativo e não expirado emitido anteriormente pelo seu provedor. Como os apps de terceiros não conseguem adivinhar esse nonce, isso garante que a intent corresponda a uma seleção autêntica do usuário no seletor do sistema.
  2. Verificar Activity.getCallingPackage() (verificação do caller): como o seletor de credenciais inicia sua atividade usando startActivityForResult, ela pode inspecionar getCallingPackage() para confirmar que o caller imediato que inicia a interface é o framework confiável do sistema seletor de credenciais (em dispositivos Android com o Google Play Services, "com.google.android.gms"). Rejeite a solicitação se o pacote de chamada for desconhecido.
  3. Inspecione request.callingAppInfo (informações do app de destino): o callingAppInfo identifica o aplicativo de destino que vai receber as credenciais importadas. Você pode usar essas informações como achar melhor para transparência (por exemplo, mostrando o nome e o ícone do app de destino na interface de confirmação) ou para verificar se o aplicativo de destino é confiável.

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()
    }
}

Exportar exceções de registro e compensação

Todas as exceções neste módulo são subclasses de ImportCredentialsException, RegisterExportException ou ClearExportException.

Implementar o importador

Para importar credenciais para seu app (por exemplo, durante a integração ou a importação de provedores), implemente a função de importador e inicie o fluxo chamando ProviderEventsManager.importCredentials().

Criar a solicitação de importação e iniciar o fluxo

Especifique quais CredentialTypes e KnownExtensions seu importador aceita:

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) {}

Importar exceções de fluxo

Todas as exceções neste módulo são subclasses de ImportCredentialsException, RegisterExportException ou ClearExportException.

Tipos e extensões de credenciais aceitos

O objeto androidx.credentials.providerevents.transfer.CredentialTypes define constantes de string padronizadas correspondentes aos tipos de item FIDO CXF:

Constante Valor (tipo cxf) Descrição
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Credenciais de login de nome de usuário e senha.
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" Credenciais de chave pública de chave de acesso FIDO2 ou WebAuthn.
CREDENTIAL_TYPE_ADDRESS "address" Informações de endereço postal ou de entrega para preenchimento automático de formulários.
CREDENTIAL_TYPE_API_KEY "api-key" Chaves e tokens de acesso à API.
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" Informações de pagamento com cartão de crédito e débito.
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" Agrupamentos personalizados ou campos definidos pelo usuário.
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Detalhes da carteira de habilitação.
CREDENTIAL_TYPE_FILE "file" Metadados e referências de marcador de posição para arquivos binários.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Senhas seguras geradas por máquina.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Documentos de identidade nacionais, CPF, CNPJ ou referências de passaporte.
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Links lógicos que apontam para outro item na carga útil.
CREDENTIAL_TYPE_NOTE "note" Observações seguras definidas pelo usuário (string UTF-8).
CREDENTIAL_TYPE_PASSPORT "passport" Detalhes do documento de viagem do passaporte.
CREDENTIAL_TYPE_PERSON_NAME "person-name" Identidade da pessoa e detalhes de nomeação.
CREDENTIAL_TYPE_SSH_KEY "ssh-key" Pares de chaves públicas e privadas SSH.
CREDENTIAL_TYPE_TOTP "totp" Secrets de senha única baseada em tempo (2FA).
CREDENTIAL_TYPE_WIFI "wifi" SSID e senhas de redes Wi-Fi.

Matchers WASM (WebAssembly) personalizados (avançado)

Por padrão, chamar RegisterExportRequest.create(context, entries) agrupa o credential_transfer_matcher.wasm padrão do sistema dos recursos da biblioteca, que filtra entradas com base apenas na interseção de supportedCredentialTypes.

Se o provedor de credenciais exigir uma lógica de correspondência complexa (por exemplo, verificações de capacidade dinâmica ou filtragem condicional com base em campos personalizados), você poderá compilar seu próprio módulo WebAssembly correspondente à API de transferência de credenciais WASM e transmitir a matriz de bytes bruta diretamente:

// 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)