認證轉移

Credential Manager 的憑證轉移 API 可讓憑證提供者在同一部裝置上安全地轉移使用者憑證。本指南詳細說明 Android 上的憑證提供者如何與 androidx.credentials:providerevents 程式庫提供的 API 整合。這項功能支援密碼、密碼金鑰、地址資訊和自訂欄位,並使用標準化的 FIDO 憑證交換格式 (CXF)

核心概念

憑證轉移架構可讓您在同一部裝置上進行點對點憑證轉移,且不會向 Android OS 或未經驗證的應用程式公開原始憑證。

這個架構定義了兩個主要角色:

  • 匯出者 (來源提供者):目前持有使用者憑證的憑證提供者。預先向系統註冊可匯出帳戶的相關中繼資料 (ExportEntry),並在使用者選取時回應轉移要求。
  • 匯入工具 (用戶端供應商或設定精靈):啟動匯入要求 (ImportCredentialsRequest) 的憑證供應商或設定精靈,可指定要接收的憑證和擴充功能類型。

Android 版本相容性

憑證轉移 API 適用於搭載 Android 8 (API 級別 26) 以上版本的裝置。

新增依附元件

androidx.credentials:providerevents 依附元件新增至模組的 build.gradlebuild.gradle.kts

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

例項化必要類別

建立所需 ProviderEventsManager 的執行個體。

val providerEventsManager = ProviderEventsManager.create(context)

實作匯出工具

如要允許使用者將憑證從應用程式匯出至裝置上的其他憑證提供者,請實作匯出者角色。

註冊匯出項目

當憑證供應商變更時 (例如使用者登入、新增憑證或修改帳戶),請使用 ProviderEventsManager.registerExport() 註冊或更新 ExportEntry 項目。

每個 ExportEntry 都需要:

  • id:隨機產生的私密字串 ID,可做為這個匯出項目的專屬代表。請務必妥善保存這個 ID,稍後您需要這個 ID 來驗證轉移要求。
  • accountDisplayName:選用帳戶標籤 (例如 "Personal Account")。
  • userDisplayName:使用者的主要 ID (例如 "alice@example.com")。
  • icon:代表提供者或帳戶的 Bitmap 圖示 (程式庫會自動縮放為 32x32 PNG)。
  • supportedCredentialTypesCredentialTypes 中的一組字串常數,代表這個項目所保留的類型。

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

在資訊清單檔案中宣告匯出器活動

使用者在系統選取器 UI 中選取 ExportEntry 時,系統會啟動指定的處理 Activity。使用必要意圖動作和內容 URI 配置宣告這項活動:

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

在活動中處理轉移意圖

CredentialExportActivity 中,使用 IntentHandler.retrieveProviderImportCredentialsRequest(intent) 剖析要求、驗證呼叫應用程式和 credId、執行任何必要的生物特徵辨識驗證,然後將 FIDO CXF 酬載寫回提供的內容 URI:

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

匯出註冊和結算例外狀況

這個模組中的所有例外狀況都是 ImportCredentialsExceptionRegisterExportExceptionClearExportException 的子類別。

實作匯入工具

如要將憑證匯入應用程式 (例如在新手上路或供應商匯入期間),請實作匯入者角色,並呼叫 ProviderEventsManager.importCredentials() 啟動流程。

建構匯入要求並啟動流程

指定匯入工具支援的 CredentialTypesKnownExtensions

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

匯入流程例外狀況

這個模組中的所有例外狀況都是 ImportCredentialsExceptionRegisterExportExceptionClearExportException 的子類別。

支援的憑證類型和擴充功能

androidx.credentials.providerevents.transfer.CredentialTypes 物件 定義對應至 FIDO CXF 項目類型的標準字串常數:

常數 價值 (cxf 類型) 說明
CREDENTIAL_TYPE_BASIC_AUTH "basic-auth" 使用者名稱和密碼登入憑證。
CREDENTIAL_TYPE_PUBLIC_KEY "passkey" FIDO2 或 WebAuthn 密碼金鑰公開金鑰憑證。
CREDENTIAL_TYPE_ADDRESS "address" 郵寄或運送地址資訊,用於自動填入表單。
CREDENTIAL_TYPE_API_KEY "api-key" API 存取金鑰和權杖。
CREDENTIAL_TYPE_CREDIT_CARD "credit-card" 信用卡和簽帳金融卡付款資訊。
CREDENTIAL_TYPE_CUSTOM_FIELDS "custom-fields" 自訂分組或使用者定義的欄位。
CREDENTIAL_TYPE_DRIVERS_LICENSE "drivers-license" 駕照詳細資料。
CREDENTIAL_TYPE_FILE "file" 二進位檔案的中繼資料和預留位置參照。
CREDENTIAL_TYPE_GENERATED_PASSWORD "generated-password" 機器產生的安全密碼。
CREDENTIAL_TYPE_IDENTITY_DOCUMENT "identity-document" 國民身分證、社會安全號碼 (SSN)、納稅人識別號碼:TIN 或護照參考資料。
CREDENTIAL_TYPE_ITEM_REFERENCE "item-reference" 指向酬載中另一個項目的邏輯連結。
CREDENTIAL_TYPE_NOTE "note" 使用者定義的安全性記事 (UTF-8 字串)。
CREDENTIAL_TYPE_PASSPORT "passport" 護照旅行證件詳細資料。
CREDENTIAL_TYPE_PERSON_NAME "person-name" 人員身分和命名詳細資料。
CREDENTIAL_TYPE_SSH_KEY "ssh-key" 安全殼層公開和私密金鑰組。
CREDENTIAL_TYPE_TOTP "totp" 限時動態密碼 (雙因素驗證) 密鑰。
CREDENTIAL_TYPE_WIFI "wifi" Wi-Fi 網路 SSID 和密碼。

自訂 WASM (WebAssembly) 比對器 (進階)

根據預設,呼叫 RegisterExportRequest.create(context, entries) 會從程式庫資產組合系統預設的 credential_transfer_matcher.wasm,並根據 supportedCredentialTypes 的交集篩選項目。

如果憑證供應商需要複雜的比對邏輯 (例如動態功能檢查或根據自訂欄位進行條件式篩選),您可以編譯自己的 WebAssembly 模組,比對 WASM 憑證轉移 API,並直接傳遞原始位元組陣列:

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