Trasferimento delle credenziali

Le API Credentials Transfer di Gestore delle credenziali consentono il trasferimento sicuro delle credenziali utente tra i provider di credenziali sullo stesso dispositivo. Questa guida descrive in dettaglio come i provider di credenziali su Android possono integrarsi con le API fornite dalla libreria androidx.credentials:providerevents. Questa funzionalità supporta password, passkey, informazioni sull'indirizzo e campi personalizzati utilizzando il formato standardizzato FIDO Credential Exchange Format (CXF).

Concetti principali

Il framework di trasferimento delle credenziali facilita il trasferimento peer-to-peer delle credenziali sullo stesso dispositivo senza esporre le credenziali non elaborate al sistema operativo Android o alle app non autenticate.

Il framework definisce due ruoli principali:

  • Esportatore (fornitore di origine): un fornitore di credenziali che attualmente conserva le credenziali utente. Pre-registra i metadati relativi agli account esportabili disponibili (ExportEntry) nel sistema e risponde alle richieste di trasferimento quando selezionate dall'utente.
  • Strumento di importazione (provider client o configurazione guidata): un provider di credenziali o una configurazione guidata che avvia una richiesta di importazione (ImportCredentialsRequest) specificando i tipi di credenziali ed estensioni che può ricevere.

Compatibilità con le versioni di Android

L'API Credentials Transfer funziona sui dispositivi con Android 8 (livello API 26) e versioni successive.

Aggiungi dipendenze

Aggiungi la dipendenza androidx.credentials:providerevents al file build.gradle o build.gradle.kts del tuo modulo:

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

Crea un'istanza della classe richiesta

Crea un'istanza del ProviderEventsManager richiesto.

val providerEventsManager = ProviderEventsManager.create(context)

Implementa l'esportatore

Per consentire agli utenti di esportare le credenziali dalla tua app ad altri provider di credenziali sul dispositivo, implementa il ruolo di esportatore.

Registra le voci di esportazione

Quando il provider di credenziali cambia (ad esempio, un utente accede, aggiunge credenziali o modifica gli account), registra o aggiorna gli elementi ExportEntry utilizzando ProviderEventsManager.registerExport().

Ogni ExportEntry richiede:

  • id: un identificatore di stringa segreto generato in modo casuale che rappresenta in modo univoco questa voce di esportazione. Devi conservare questo ID in modo sicuro; ti servirà in un secondo momento per verificare le richieste di trasferimento in entrata.
  • accountDisplayName: Etichetta dell'account facoltativa (ad esempio, "Personal Account").
  • userDisplayName: l'identificatore principale dell'utente (ad esempio, "alice@example.com").
  • icon: un'icona Bitmap che rappresenta il provider o l'account (scalata automaticamente a 32 x 32 PNG dalla libreria).
  • supportedCredentialTypes: un insieme di costanti di stringa di CredentialTypes che rappresentano i tipi contenuti in questa voce.

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

Dichiara l'attività di esportazione nel file manifest

Quando l'utente seleziona la tua ExportEntry nell'UI del selettore di sistema, il sistema avvia l'Activity di gestione designata. Dichiara questa attività con l'azione di intent obbligatoria e lo schema URI contenuto.

Per proteggerti da chiamate non autorizzate e attacchi confused deputy, dichiara android:requireContentUriPermissionFromCaller="write" su Android 14 (livello API 34) e versioni successive. In questo modo, il sistema operativo Android verifica che il chiamante che avvia il lancio disponga delle autorizzazioni in scrittura per l'URI dei contenuti di destinazione prima di consentire l'avvio dell'attività, come mostrato nell'esempio seguente:

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

Gestisci l'intent di trasferimento nella tua attività

Nella tua CredentialExportActivity, utilizza IntentHandler.retrieveProviderImportCredentialsRequest(intent) per analizzare la richiesta, verificare il componente chiamante e i metadati della richiesta, eseguire l'autenticazione biometrica richiesta e riscrivere il payload FIDO CXF nell'URI contenuto fornito.

Autentica la richiesta

Poiché la tua attività viene esportata, proteggi il flusso di esportazione delle credenziali verificando ciascuno dei seguenti livelli:

  1. Verifica il nonce della sessione credId (difesa principale): quando registri il tuo ExportEntry, genera un id (nonce) casuale dal punto di vista crittografico e con entropia elevata e associalo a una sessione di breve durata. Nella tua attività, verifica che request.credId corrisponda a un nonce di sessione attivo e non scaduto emesso in precedenza dal tuo provider. Poiché le app di terze parti non possono indovinare questo nonce, ciò garantisce che l'intent corrisponda a una selezione utente autentica nel selettore di sistema.
  2. Verifica Activity.getCallingPackage() (controllo del chiamante): poiché il selettore delle credenziali avvia la tua attività utilizzando startActivityForResult, la tua attività può ispezionare getCallingPackage() per confermare che il chiamante immediato che avvia l'interfaccia utente sia il framework del selettore delle credenziali di sistema attendibile (sui dispositivi Android con Google Play Services, "com.google.android.gms"). Rifiuta la richiesta se il pacchetto chiamante non viene riconosciuto.
  3. Ispeziona request.callingAppInfo (informazioni sull'app di destinazione): callingAppInfo identifica l'applicazione di destinazione che riceverà le credenziali importate. Puoi utilizzare queste informazioni come ritieni opportuno per la trasparenza (ad esempio, visualizzando il nome e l'icona dell'app di destinazione nella UI di conferma) o per verificare se l'applicazione di destinazione è attendibile.

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

Esportazione delle eccezioni di registrazione e compensazione

Tutte le eccezioni in questo modulo sono sottoclassi di ImportCredentialsException, RegisterExportException o ClearExportException.

Implementa l'importatore

Per importare le credenziali nella tua app (ad esempio durante l'onboarding o l'importazione del provider), implementa il ruolo di importatore e avvia il flusso chiamando ProviderEventsManager.importCredentials().

Crea la richiesta di importazione e avvia il flusso

Specifica quali CredentialTypes e KnownExtensions sono supportati dal tuo importatore:

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

Eccezioni del flusso di importazione

Tutte le eccezioni in questo modulo sono sottoclassi di ImportCredentialsException, RegisterExportException o ClearExportException.

Tipi di credenziali ed estensioni supportati

L'oggetto androidx.credentials.providerevents.transfer.CredentialTypes definisce costanti di stringa standardizzate corrispondenti ai tipi di articolo FIDO CXF:

Costante Valore (tipo cxf) Descrizione
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Credenziali di accesso con nome utente e password.
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" Credenziali della chiave pubblica della passkey FIDO2 o WebAuthn.
CREDENTIAL_TYPE_ADDRESS "address" Informazioni sull'indirizzo postale o di spedizione per la compilazione automatica dei moduli.
CREDENTIAL_TYPE_API_KEY "api-key" Chiavi e token di accesso alle API.
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" Dati di pagamento con carta di credito e di debito.
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" Raggruppamenti personalizzati o campi definiti dall'utente.
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Dettagli della patente di guida.
CREDENTIAL_TYPE_FILE "file" Riferimenti a metadati e segnaposto per i file binari.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Password sicure generate automaticamente.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Carte di identità nazionali, SSN, TIN o riferimenti al passaporto.
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Link logici che rimandano a un altro elemento nel payload.
CREDENTIAL_TYPE_NOTE "note" Note sicure definite dall'utente (stringa UTF-8).
CREDENTIAL_TYPE_PASSPORT "passport" Dettagli del documento di viaggio del passaporto.
CREDENTIAL_TYPE_PERSON_NAME "person-name" Dettagli sull'identità e sul nome della persona.
CREDENTIAL_TYPE_SSH_KEY "ssh-key" Coppie di chiavi SSH pubbliche e private.
CREDENTIAL_TYPE_TOTP "totp" Secret per password monouso a tempo (2FA).
CREDENTIAL_TYPE_WIFI "wifi" SSID e passphrase della rete Wi-Fi.

Matcher WASM (WebAssembly) personalizzati (avanzati)

Per impostazione predefinita, la chiamata a RegisterExportRequest.create(context, entries) raggruppa l'credential_transfer_matcher.wasm predefinito di sistema dagli asset della libreria, che filtra le voci esclusivamente in base all'intersezione di supportedCredentialTypes.

Se il tuo fornitore di credenziali richiede una logica di corrispondenza complessa (ad esempio, controlli delle funzionalità dinamiche o filtri condizionali basati su campi personalizzati), puoi compilare il tuo modulo WebAssembly corrispondente all'API di trasferimento delle credenziali WASM e passare direttamente l'array di byte non elaborati:

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