Transferência de credenciais

As APIs Credentials Transfer do Gerenciador de credenciais permitem a transferência segura, 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, na sigla em inglês) da FIDO padronizado.

Principais conceitos

O framework 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 a apps não autenticados.

O framework define dois papéis principais:

  • Exportador (provedor de origem): um provedor de credenciais que atualmente contém credenciais do usuário. Ele faz o pré-registro de metadados sobre contas exportáveis disponíveis (ExportEntry) no 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 com versões do Android

A API Credentials Transfer funciona em dispositivos com o 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 de credenciais no dispositivo, implemente o papel de exportador.

Registrar entradas de exportação

Quando o provedor de credenciais mudar (por exemplo, um usuário fizer login, adicionar credenciais ou modificar contas), registre ou atualize os itens ExportEntry usando ProviderEventsManager.registerExport().

Cada ExportEntry requer:

  • id: um identificador de string secreto gerado aleatoriamente que representa de forma exclusiva essa entrada de exportação. É preciso persistir esse ID em segurança. Você vai precisar dele mais tarde 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 32 x 32 pela biblioteca).
  • supportedCredentialTypes: um conjunto de constantes de string de CredentialTypes que representam os tipos que essa 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, o sistema inicia o Activity de processamento designado. Declare essa atividade com a ação de intent obrigatória e o esquema de URI de conteúdo.

Para se proteger contra invocações não autorizadas e ataques de deputado confuso, 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 autor da chamada que inicia o lançamento tem permissões de gravação no URI de 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 CredentialExportActivity, use IntentHandler.retrieveProviderImportCredentialsRequest(intent) para analisar a solicitação, 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 valor de uso único da sessão credId (defesa principal): ao registrar seu ExportEntry, gere um id (valor de uso único) 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 valor de uso único de sessão ativo e não expirado emitido anteriormente pelo seu provedor. Como os apps de terceiros não podem adivinhar esse valor de uso único, isso garante que a intent corresponda a uma seleção de usuário autêntica no seletor do sistema.
  2. Verificar Activity.getCallingPackage() (verificação do autor da chamada): como o seletor de credenciais inicia sua atividade usando startActivityForResult, ela pode inspecionar getCallingPackage() para confirmar se o autor da chamada imediato que inicia a IU é o framework confiável do seletor de credenciais do sistema (em dispositivos Android com o Google Play Services, "com.google.android.gms"). Rejeite a solicitação se o pacote de chamada não for reconhecido.
  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 fins de transparência (como exibir 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 registro e limpar exceções

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

Implementar o importador

Para importar credenciais para o app (por exemplo, durante a integração ou a importação do provedor), 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 são compatíveis com o 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) {}

Importar exceções de fluxo

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

Tipos de credenciais e extensões compatíveis

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

Constante Valor (tipo cxf) Descrição
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Credenciais de login com 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 marcadores 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, SSN, TIN ou referências de passaporte.
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Links lógicos que apontam para outro item no payload.
CREDENTIAL_TYPE_NOTE "note" Notas 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" Detalhes de identidade e nome da pessoa.
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 longas da rede Wi-Fi.

Correspondências personalizadas do WASM (WebAssembly) (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 as 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 WASM de transferência de credenciais e transmitir a matriz de bytes brutos 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)