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'iconaBitmapche rappresenta il provider o l'account (scalata automaticamente a 32 x 32 PNG dalla libreria).supportedCredentialTypes: un insieme di costanti di stringa diCredentialTypesche 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:
- Verifica il nonce della sessione
credId(difesa principale): quando registri il tuoExportEntry, genera unid(nonce) casuale dal punto di vista crittografico e con entropia elevata e associalo a una sessione di breve durata. Nella tua attività, verifica cherequest.credIdcorrisponda 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. - Verifica
Activity.getCallingPackage()(controllo del chiamante): poiché il selettore delle credenziali avvia la tua attività utilizzandostartActivityForResult, la tua attività può ispezionaregetCallingPackage()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. - Ispeziona
request.callingAppInfo(informazioni sull'app di destinazione):callingAppInfoidentifica 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.
RegisterExportProviderConfigurationExceptionoClearExportProviderConfigurationException: viene generato quando la registrazione o la cancellazione non va a buon fine a causa di problemi di configurazione del provider (ad esempio,supportedCredentialTypesvuoto in unExportEntryo chiamata su un livello del sistema operativo non supportato).RegisterExportUnknownErrorExceptionoClearExportUnknownErrorException: si è verificato un errore di sistema o di archiviazione non classificato durante l'aggiornamento del registry di esportazione.
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.
ImportCredentialsCancellationException: l'utente ha ignorato la UI del selettore o ha annullato l'attività di esportazione (Activity.RESULT_CANCELED).ImportCredentialsNoExportOptionException: nessuna voce di esportazione registrata corrisponde ai tipi di credenziali richiesti o l'esportatore selezionato ha generato un'eccezione di rifiuto.ImportCredentialsProviderConfigurationException: errore di configurazione (ad esempio,credentialTypesvuoto impostato inImportCredentialsRequesto autorizzazioni mancanti).ImportCredentialsInvalidJsonException: l'esportatore ha restituito un payload JSON non valido o vuoto che non ha superato la convalida della richiesta.ImportCredentialsSystemErrorException: si è verificato un errore interno del sistema Android o di trasferimento di Binder durante l'importazione.ImportCredentialsUnknownCallerException: l'app di chiamate non è stata verificata dal framework.ImportCredentialsUnknownErrorException: errore non classificato o imprevisto durante il flusso di importazione.
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)