Les API de transfert d'identifiants du Credential Manager permettent de transférer de manière sécurisée les identifiants utilisateur entre les fournisseurs d'identifiants sur un même appareil. Ce guide explique comment les fournisseurs d'identifiants sur Android peuvent s'intégrer aux API fournies par la bibliothèque androidx.credentials:providerevents. Cette fonctionnalité est compatible avec les mots de passe, les clés d'accès, les informations d'adresse et les champs personnalisés à l'aide du format d'échange d'identifiants FIDO (CXF) standardisé.
Concepts fondamentaux
Le framework de transfert d'identifiants facilite le transfert d'identifiants pair à pair sur le même appareil sans exposer les identifiants bruts à l'OS Android ni aux applications non authentifiées.
Le framework définit deux rôles principaux :
- Exportateur (fournisseur source) : fournisseur d'identifiants qui détient actuellement les identifiants utilisateur. Il préenregistre les métadonnées sur les comptes exportables disponibles (
ExportEntry) auprès du système et répond aux demandes de transfert lorsqu'il est sélectionné par l'utilisateur. - Importateur (fournisseur de client ou assistant de configuration) : fournisseur d'identifiants ou assistant de configuration qui lance une demande d'importation (
ImportCredentialsRequest) en spécifiant les types d'identifiants et d'extensions qu'il peut recevoir.
Compatibilité avec les versions d'Android
L'API Credentials Transfer fonctionne sur les appareils équipés d'Android 8 (niveau d'API 26) ou version ultérieure.
Ajouter des dépendances
Ajoutez la dépendance androidx.credentials:providerevents au fichier build.gradle ou build.gradle.kts de votre module :
dependencies {
implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}
Instancier la classe requise
Créez une instance de l'ProviderEventsManager requis.
val providerEventsManager = ProviderEventsManager.create(context)
Implémenter l'exportateur
Pour permettre aux utilisateurs d'exporter des identifiants de votre application vers d'autres fournisseurs d'identifiants sur l'appareil, implémentez le rôle d'exportateur.
Enregistrer des entrées d'exportation
Lorsque votre fournisseur d'identifiants change (par exemple, un utilisateur se connecte, ajoute des identifiants ou modifie des comptes), enregistrez ou mettez à jour vos éléments ExportEntry à l'aide de ProviderEventsManager.registerExport().
Chaque ExportEntry nécessite :
id: identifiant de chaîne secret généré de manière aléatoire représentant de manière unique cette entrée d'exportation. Vous devez conserver cet ID de manière sécurisée, car vous en aurez besoin ultérieurement pour valider les demandes de transfert entrantes.accountDisplayName: libellé de compte facultatif (par exemple,"Personal Account").userDisplayName: identifiant principal de l'utilisateur (par exemple,"alice@example.com").icon: icôneBitmapreprésentant le fournisseur ou le compte (automatiquement redimensionnée au format PNG 32 x 32 par la bibliothèque).supportedCredentialTypes: ensemble de constantes de chaîne deCredentialTypesreprésentant les types de cette entrée.
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) } }
Déclarer l'activité de l'exportateur dans le fichier manifeste
Lorsque l'utilisateur sélectionne votre ExportEntry dans l'interface utilisateur du sélecteur système, le système lance l'Activity de gestion désigné. Déclarez cette activité avec l'action d'intent obligatoire et le schéma d'URI de contenu.
Pour vous protéger contre les appels non autorisés et les attaques par substitution de l'appelant, déclarez android:requireContentUriPermissionFromCaller="write" sur Android 14 (niveau d'API 34) ou version ultérieure. Cela indique au système d'exploitation Android de vérifier que l'appelant qui lance l'activité dispose des autorisations d'écriture sur l'URI de contenu de destination avant d'autoriser le démarrage de l'activité, comme illustré dans l'exemple suivant :
<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>
Gérer l'intent de transfert dans votre activité
Dans votre CredentialExportActivity, utilisez IntentHandler.retrieveProviderImportCredentialsRequest(intent) pour analyser la requête, vérifier le composant appelant et les métadonnées de la requête, effectuer l'authentification biométrique requise et réécrire la charge utile FIDO CXF dans l'URI de contenu fourni.
Authentifier la requête
Étant donné que votre activité est exportée, protégez le flux d'exportation des identifiants en vérifiant chacune des couches suivantes :
- Vérifiez le nonce de session
credId(défense principale) : lors de l'enregistrement de votreExportEntry, générez unid(nonce) cryptographiquement aléatoire et à entropie élevée, puis associez-le à une session de courte durée. Dans votre activité, vérifiez querequest.credIdcorrespond à un nonce de session actif et non expiré précédemment émis par votre fournisseur. Comme les applications tierces ne peuvent pas deviner ce nonce, cela garantit que l'intent correspond à une sélection authentique de l'utilisateur dans le sélecteur du système. - Vérifier
Activity.getCallingPackage()(vérification de l'appelant) : Comme le sélecteur d'identifiants lance votre activité à l'aide destartActivityForResult, votre activité peut inspectergetCallingPackage()pour confirmer que l'appelant immédiat qui lance l'UI est le framework de sélection d'identifiants système de confiance (sur les appareils Android équipés des services Google Play,"com.google.android.gms"). Rejetez la requête si le package appelant n'est pas reconnu. - Inspecter
request.callingAppInfo(informations sur l'application cible) :callingAppInfoidentifie l'application de destination qui recevra les identifiants importés. Vous pouvez utiliser ces informations comme bon vous semble pour assurer la transparence (par exemple, en affichant le nom et l'icône de l'application de destination dans votre UI de confirmation) ou pour vérifier si l'application de destination est fiable.
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() } }
Exceptions d'exportation de l'enregistrement et de la compensation
Toutes les exceptions de ce module sont des sous-classes de ImportCredentialsException, RegisterExportException ou ClearExportException.
RegisterExportProviderConfigurationExceptionouClearExportProviderConfigurationException: généré lorsque l'enregistrement ou l'effacement échoue en raison de problèmes de configuration du fournisseur (par exemple,supportedCredentialTypesvide dans unExportEntryou appel sur un niveau d'OS non compatible).RegisterExportUnknownErrorExceptionouClearExportUnknownErrorException: une erreur système ou de stockage non classée s'est produite lors de la mise à jour du registre d'exportation.
Implémenter l'importateur
Pour importer des identifiants dans votre application (par exemple, lors de l'intégration ou de l'importation d'un fournisseur), implémentez le rôle d'importateur et lancez le flux en appelant ProviderEventsManager.importCredentials().
Créer la requête d'importation et lancer le flux
Spécifiez les CredentialTypes et KnownExtensions compatibles avec votre importateur :
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) {}
Exceptions d'importation de flux
Toutes les exceptions de ce module sont des sous-classes de ImportCredentialsException, RegisterExportException ou ClearExportException.
ImportCredentialsCancellationException: l'utilisateur a fermé l'UI du sélecteur ou annulé l'activité d'exportation (Activity.RESULT_CANCELED).ImportCredentialsNoExportOptionException: aucune entrée d'exportation enregistrée ne correspond aux types d'identifiants demandés, ou l'exportateur sélectionné a généré une exception de rejet.ImportCredentialsProviderConfigurationException: erreur de configuration (par exemple,credentialTypesvide défini dansImportCredentialsRequestou autorisations manquantes).ImportCredentialsInvalidJsonException: l'exportateur a renvoyé une charge utile JSON incorrecte ou vide qui a échoué à la validation de la requête.ImportCredentialsSystemErrorException: une erreur interne du système Android ou de transfert Binder s'est produite lors de l'importation.ImportCredentialsUnknownCallerException: l'application appelante n'a pas pu être validée par le framework.ImportCredentialsUnknownErrorException: erreur non classée ou inattendue lors du flux d'importation.
Types d'identifiants et extensions compatibles
L'objet androidx.credentials.providerevents.transfer.CredentialTypes définit des constantes de chaîne standardisées correspondant aux types d'éléments FIDO CXF :
| Constante | Valeur (type cxf) |
Description |
|---|---|---|
CREDENTIAL_TYPE_BASIC_AUTH |
"basic-auth" |
Identifiants de connexion (nom d'utilisateur et mot de passe). |
CREDENTIAL_TYPE_PUBLIC_KEY |
"passkey" |
Identifiants de clé publique de clé d'accès FIDO2 ou WebAuthn. |
CREDENTIAL_TYPE_ADDRESS |
"address" |
Informations sur l'adresse postale ou de livraison pour la saisie automatique des formulaires. |
CREDENTIAL_TYPE_API_KEY |
"api-key" |
Clés et jetons d'accès à l'API. |
CREDENTIAL_TYPE_CREDIT_CARD |
"credit-card" |
Informations de paiement par carte de crédit et de débit. |
CREDENTIAL_TYPE_CUSTOM_FIELDS |
"custom-fields" |
Regroupements personnalisés ou champs définis par l'utilisateur. |
CREDENTIAL_TYPE_DRIVERS_LICENSE |
"drivers-license" |
Informations sur le permis de conduire. |
CREDENTIAL_TYPE_FILE |
"file" |
Métadonnées et références d'espace réservé pour les fichiers binaires. |
CREDENTIAL_TYPE_GENERATED_PASSWORD |
"generated-password" |
Mots de passe sécurisés générés par machine. |
CREDENTIAL_TYPE_IDENTITY_DOCUMENT |
"identity-document" |
Cartes nationales d'identité, numéros de sécurité sociale, numéros d'identification fiscale ou références de passeport. |
CREDENTIAL_TYPE_ITEM_REFERENCE |
"item-reference" |
Liens logiques pointant vers un autre élément de la charge utile. |
CREDENTIAL_TYPE_NOTE |
"note" |
Notes sécurisées définies par l'utilisateur (chaîne UTF-8). |
CREDENTIAL_TYPE_PASSPORT |
"passport" |
Informations sur le passeport. |
CREDENTIAL_TYPE_PERSON_NAME |
"person-name" |
Identité de la personne et détails de son nom. |
CREDENTIAL_TYPE_SSH_KEY |
"ssh-key" |
Paires de clés SSH publiques et privées. |
CREDENTIAL_TYPE_TOTP |
"totp" |
Secrets de mot de passe à usage unique basé sur le temps (A2F). |
CREDENTIAL_TYPE_WIFI |
"wifi" |
SSID et phrases secrètes du réseau Wi-Fi. |
Outils de mise en correspondance WASM (WebAssembly) personnalisés (avancé)
Par défaut, l'appel de RegisterExportRequest.create(context, entries) regroupe le credential_transfer_matcher.wasm par défaut du système à partir des éléments de la bibliothèque, qui filtre les entrées uniquement en fonction de l'intersection de supportedCredentialTypes.
Si votre fournisseur d'identifiants nécessite une logique de correspondance complexe (par exemple, des vérifications de fonctionnalités dynamiques ou un filtrage conditionnel basé sur des champs personnalisés), vous pouvez compiler votre propre module WebAssembly correspondant à l'API de transfert d'identifiants WASM et transmettre directement le tableau d'octets bruts :
// 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)