认证转移

Credential Manager 的凭据传输 API 支持在凭据提供方之间安全地在同一设备上转移用户凭据。本指南详细介绍了 Android 上的凭据提供方如何与 androidx.credentials:providerevents 库提供的 API 集成。此功能使用 标准化的 FIDO 凭据交换格式 (CXF)支持 密码、通行密钥、地址信息和自定义字段。

核心概念

凭据传输框架有助于在同一设备上进行对等凭据传输,而不会向 Android 操作系统或未经身份验证的应用公开原始凭据。

该框架定义了两个主要角色:

  • 导出器(来源提供方): 当前持有用户凭据的凭据提供方。它会向系统预注册有关可用可导出 账号 (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;您稍后需要使用它 来验证传入的传输请求。
  • accountDisplayName:可选账号标签(例如 "Personal Account")。
  • userDisplayName:用户的主要标识符(例如 "alice@example.com")。
  • icon:表示提供方或账号的 Bitmap 图标(由库自动缩放为 32x32 PNG)。
  • supportedCredentialTypes:一组来自 CredentialTypes的字符串常量,表示此条目包含的类型。

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

在清单文件中声明导出器 activity

当用户在系统选择器界面上选择您的 ExportEntry 时,系统会启动您指定的处理 Activity。使用强制性 intent 操作和内容 URI scheme 声明此 Activity:

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

处理您的 activity 中的传输 intent

CredentialExportActivity 中,使用 IntentHandler.retrieveProviderImportCredentialsRequest(intent) 解析请求,验证调用应用和 credId,执行任何所需的生物识别身份验证,并将 FIDO CXF 载荷写回提供的 content 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" SSH 公钥和私钥对。
CREDENTIAL_TYPE_TOTP "totp" 基于时间的一次性密码 (2FA) 密钥。
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)