Transfer kredensial

Credential Transfer API Pengelola Kredensial memungkinkan transfer kredensial pengguna yang aman dan di perangkat yang sama antar-penyedia kredensial. Panduan ini menjelaskan cara penyedia kredensial di Android dapat berintegrasi dengan API yang disediakan oleh androidx.credentials:providerevents library. Fitur ini mendukung sandi, kunci sandi, informasi alamat, dan kolom kustom menggunakan Format Pertukaran Kredensial FIDO (CXF)standar.

Konsep inti

Framework transfer kredensial memfasilitasi transfer kredensial peer-to-peer di perangkat yang sama tanpa mengekspos kredensial mentah ke OS Android atau aplikasi yang tidak diautentikasi.

Framework ini menentukan dua peran utama:

  • Pengekspor (penyedia sumber): Penyedia kredensial yang saat ini menyimpan kredensial pengguna. Pengekspor ini melakukan pra-pendaftaran metadata tentang akun yang dapat diekspor yang tersedia (ExportEntry) dengan sistem dan merespons permintaan transfer saat dipilih oleh pengguna.
  • Pengimpor (penyedia klien atau wizard penyiapan): Penyedia kredensial atau wizard penyiapan yang memulai permintaan impor (ImportCredentialsRequest) yang menentukan jenis kredensial dan ekstensi yang dapat diterimanya.

Kompatibilitas versi Android

Credential Transfer API berfungsi di perangkat yang menjalankan Android 8 (level API 26) dan yang lebih baru.

Menambahkan dependensi

Tambahkan dependensi androidx.credentials:providerevents ke build.gradle atau build.gradle.kts modul Anda:

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

Membuat instance class yang diperlukan

Buat instance ProviderEventsManager yang diperlukan.

val providerEventsManager = ProviderEventsManager.create(context)

Mengimplementasikan pengekspor

Untuk mengizinkan pengguna mengekspor kredensial dari aplikasi Anda ke penyedia kredensial lain di perangkat, implementasikan peran pengekspor.

Mendaftarkan entri ekspor

Saat penyedia kredensial Anda berubah (misalnya, pengguna login, menambahkan kredensial, atau mengubah akun), daftarkan atau perbarui item ExportEntry Anda menggunakan ProviderEventsManager.registerExport().

Setiap ExportEntry memerlukan:

  • id: ID string rahasia yang dibuat secara acak yang secara unik mewakili entri ekspor ini. Anda harus mempertahankan ID ini dengan aman; Anda akan memerlukannya nanti untuk memverifikasi permintaan transfer yang masuk.
  • accountDisplayName: Label akun opsional (misalnya, "Personal Account").
  • userDisplayName: ID utama pengguna (misalnya, "alice@example.com").
  • icon: Ikon Bitmap yang mewakili penyedia atau akun (otomatis diskalakan ke PNG 32x32 oleh library).
  • supportedCredentialTypes: Kumpulan konstanta string dari CredentialTypes yang mewakili jenis yang dimiliki entri ini.

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

Mendeklarasikan aktivitas pengekspor dalam file manifes

Saat pengguna memilih ExportEntry Anda di UI pemilih sistem, sistem akan meluncurkan Activity penanganan yang ditentukan. Deklarasikan aktivitas ini dengan tindakan intent dan skema URI konten wajib:

<activity
    android:name="com.example.CredentialExportActivity"
    android:exported="true"
    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>

Menangani intent transfer dalam aktivitas Anda

Di CredentialExportActivity, gunakan IntentHandler.retrieveProviderImportCredentialsRequest(intent) untuk mengurai permintaan, memverifikasi aplikasi pemanggil dan credId, melakukan autentikasi biometrik yang diperlukan, dan menulis payload CXF FIDO kembali ke URI konten yang disediakan:

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

Pengecualian pendaftaran dan penghapusan ekspor

Semua pengecualian dalam modul ini adalah subclass dari ImportCredentialsException, RegisterExportException, atau ClearExportException.

Mengimplementasikan pengimpor

Untuk mengimpor kredensial ke aplikasi Anda (misalnya, selama orientasi atau impor penyedia ), implementasikan peran pengimpor dan mulai alur dengan memanggil ProviderEventsManager.importCredentials().

Membuat permintaan impor dan meluncurkan alur

Tentukan CredentialTypes dan KnownExtensions yang didukung pengimpor Anda:

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

Pengecualian alur impor

Semua pengecualian dalam modul ini adalah subclass dari ImportCredentialsException, RegisterExportException, atau ClearExportException.

Jenis dan ekstensi kredensial yang didukung

Objek androidx.credentials.providerevents.transfer.CredentialTypes menentukan konstanta string standar yang sesuai dengan jenis item CXF FIDO:

Konstanta Nilai (jenis cxf) Deskripsi
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" Kredensial login nama pengguna dan sandi.
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" Kredensial kunci publik kunci sandi FIDO2 atau WebAuthn.
CREDENTIAL_TYPE_ADDRESS "address" Info alamat pengiriman atau pos untuk pengisian otomatis formulir.
CREDENTIAL_TYPE_API_KEY "api-key" Kunci dan token akses API.
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" Informasi pembayaran kartu kredit dan debit.
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" Pengelompokan kustom atau kolom yang ditentukan pengguna.
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Detail surat izin mengemudi.
CREDENTIAL_TYPE_FILE "file" Metadata dan referensi placeholder untuk file biner.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Sandi aman yang dibuat oleh mesin.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Referensi KTP, SSN, TIN, atau paspor.
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" Link logis yang mengarah ke item lain dalam payload.
CREDENTIAL_TYPE_NOTE "note" Catatan aman yang ditentukan pengguna (string UTF-8).
CREDENTIAL_TYPE_PASSPORT "passport" Detail dokumen perjalanan paspor.
CREDENTIAL_TYPE_PERSON_NAME "person-name" Detail identitas dan penamaan orang.
CREDENTIAL_TYPE_SSH_KEY "ssh-key" Pasangan kunci publik dan pribadi SSH.
CREDENTIAL_TYPE_TOTP "totp" Secret sandi sekali pakai berbasis waktu (2FA).
CREDENTIAL_TYPE_WIFI "wifi" SSID dan frasa sandi jaringan Wi-Fi.

Pencocok WASM (WebAssembly) kustom (Lanjutan)

Secara default, memanggil RegisterExportRequest.create(context, entries) akan memaketkan credential_transfer_matcher.wasm default sistem dari aset library, yang memfilter entri hanya berdasarkan perpotongan supportedCredentialTypes.

Jika penyedia kredensial Anda memerlukan logika pencocokan yang kompleks (misalnya, pemeriksaan kemampuan dinamis atau pemfilteran bersyarat berdasarkan kolom kustom), Anda dapat mengompilasi modul WebAssembly Anda sendiri yang cocok dengan WASM credential transfer API dan meneruskan array byte mentah secara langsung:

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