Transfer kredensial

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

Konsep inti

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

Framework ini menentukan dua peran utama:

  • Pengekspor (penyedia sumber): Penyedia kredensial yang saat ini menyimpan kredensial pengguna. Aplikasi ini mendaftarkan metadata tentang akun yang dapat diekspor yang tersedia (ExportEntry) ke 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

Credentials 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, terapkan peran pengekspor.

Mendaftarkan entri ekspor

Saat penyedia kredensial Anda berubah (misalnya, pengguna login, menambahkan kredensial, atau mengubah akun), daftarkan atau perbarui item ExportEntry 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 membutuhkannya nanti untuk memverifikasi permintaan transfer 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 Anda tetapkan. Deklarasikan aktivitas ini dengan tindakan intent wajib dan skema URI konten.

Untuk melindungi dari pemanggilan yang tidak sah dan serangan confused deputy, deklarasikan android:requireContentUriPermissionFromCaller="write" di Android 14 (level API 34) dan yang lebih tinggi. Hal ini menginstruksikan sistem operasi Android untuk memverifikasi bahwa pemanggil yang memulai peluncuran memiliki izin tulis ke URI konten tujuan sebelum mengizinkan aktivitas dimulai, seperti yang ditunjukkan dalam contoh berikut:

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

Menangani intent transfer dalam aktivitas Anda

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

Mengautentikasi permintaan

Karena aktivitas Anda diekspor, lindungi alur ekspor kredensial dengan memverifikasi setiap lapisan berikut:

  1. Verifikasi nonce sesi credId (pertahanan utama): Saat mendaftarkan ExportEntry, buat id (nonce) acak kriptografis dengan entropi tinggi dan kaitkan dengan sesi berumur pendek. Dalam aktivitas Anda, pastikan request.credId cocok dengan nonce sesi aktif yang belum habis masa berlakunya yang sebelumnya dikeluarkan oleh penyedia Anda. Karena aplikasi pihak ketiga tidak dapat menebak nonce ini, hal ini memastikan bahwa intent sesuai dengan pilihan pengguna yang autentik di pemilih sistem.
  2. Verifikasi Activity.getCallingPackage() (pemeriksaan pemanggil): Karena pemilih kredensial meluncurkan aktivitas Anda menggunakan startActivityForResult, aktivitas Anda dapat memeriksa getCallingPackage() untuk mengonfirmasi bahwa pemanggil langsung yang meluncurkan UI adalah framework pemilih kredensial sistem tepercaya (di perangkat Android dengan layanan Google Play, "com.google.android.gms"). Tolak permintaan jika paket panggilan tidak dikenali.
  3. Periksa request.callingAppInfo (informasi aplikasi target): callingAppInfo mengidentifikasi aplikasi tujuan yang akan menerima kredensial yang diimpor. Anda dapat menggunakan informasi ini sesuai keinginan untuk transparansi (seperti menampilkan nama dan ikon aplikasi tujuan di UI konfirmasi) atau memverifikasi apakah aplikasi tujuan tepercaya.

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

Mengekspor pendaftaran dan menghapus pengecualian

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), terapkan peran pengimpor dan mulai alur dengan memanggil ProviderEventsManager.importCredentials().

Membuat permintaan impor dan meluncurkan alur

Tentukan CredentialTypes dan KnownExtensions yang didukung importir 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 FIDO CXF:

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 pos atau pengiriman untuk isi 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 buatan pengguna.
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" Detail surat izin mengemudi.
CREDENTIAL_TYPE_FILE "file" Referensi metadata dan placeholder untuk file biner.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Sandi aman yang dibuat oleh mesin.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Referensi kartu identitas nasional, 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" Rahasia 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 murni berdasarkan persimpangan 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 API transfer kredensial WASM 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)