Transfert d'identifiants

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ône Bitmap repré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 de CredentialTypes repré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 :

  1. Vérifiez le nonce de session credId (défense principale) : lors de l'enregistrement de votre ExportEntry, générez un id (nonce) cryptographiquement aléatoire et à entropie élevée, puis associez-le à une session de courte durée. Dans votre activité, vérifiez que request.credId correspond à 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.
  2. Vérifier Activity.getCallingPackage() (vérification de l'appelant) : Comme le sélecteur d'identifiants lance votre activité à l'aide de startActivityForResult, votre activité peut inspecter getCallingPackage() 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.
  3. Inspecter request.callingAppInfo (informations sur l'application cible) : callingAppInfo identifie 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.

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.

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)