Transfer kredensial

API Transfer Kredensial 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 FIDO (CXF) yang 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 memegang kredensial pengguna. API ini melakukan pra-pendaftaran metadata tentang akun yang tersedia dan dapat diekspor (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

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

Buat 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 Anda menggunakan ProviderEventsManager.registerExport().

Setiap ExportEntry memerlukan:

  • id: ID string rahasia yang dibuat secara acak dan secara unik merepresentasikan entri ekspor ini. Anda harus menyimpan ID ini dengan aman; Anda akan memerlukannya 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 merepresentasikan penyedia atau akun (secara otomatis disesuaikan skalanya menjadi PNG 32x32 oleh library).
  • supportedCredentialTypes: Kumpulan konstanta string dari CredentialTypes yang merepresentasikan 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 dan skema URI konten yang wajib.

Untuk melindungi dari pemanggilan yang tidak sah dan serangan wakil yang bingung, 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 Anda, 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 diberikan.

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) dengan entropi tinggi yang acak secara kriptografis dan kaitkan dengan sesi singkat. Di aktivitas Anda, pastikan request.credId cocok dengan nonce sesi aktif dan belum berakhir 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 pada 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 pemanggil 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 kebutuhan 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 pengecualian pendaftaran dan pembersihan

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

Buat permintaan impor dan alur peluncuran

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 pos atau pengiriman untuk pengisian otomatis formulir.
CREDENTIAL_TYPE_API_KEY "api-key" Token dan kunci 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" Referensi metadata dan placeholder untuk file biner.
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" Sandi aman buatan mesin.
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" Kartu identitas nasional, SSN, TIN, atau referensi 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.

Pencocokan WASM (WebAssembly) kustom (Lanjutan)

Secara default, memanggil RegisterExportRequest.create(context, entries) akan menggabungkan credential_transfer_matcher.wasm default sistem dari aset library, yang memfilter entri hanya 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 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)