将 Credential Manager 与您的凭据提供方解决方案集成

Credential Manager 指的是 Android 14 中引入的一组 API,它支持多种登录方法,例如用户名-密码、通行密钥和联合登录解决方案(例如通过 Google 登录)。调用 Credential Manager API 时,Android 系统会聚合设备上安装的所有凭据提供方提供的凭据。本文档介绍了为这些凭据提供方提供集成端点的 API 集。

设置

在凭据提供方中实现功能之前,请完成以下部分中所示的设置步骤。

声明依赖项

在模块的 build.gradle 文件中,使用 Credential Manager 库的最新版本声明依赖项

implementation "androidx.credentials:credentials:1.2.0-{latest}"

在清单文件中声明服务元素

在应用的清单文件 AndroidManifest.xml 中,为扩展 androidx.credentials 库中 CredentialProviderService 类的服务类添加一个 <service> 声明,如以下示例所示。

<service android:name=".MyCredentialProviderService"
         android:enabled="true"
         android:exported="true"
         android:label="My Credential Provider"
         android:icon="<any drawable icon>"
         android:permission="android.permission.BIND_CREDENTIAL_PROVIDER_SERVICE">
    <intent-filter>
        <action android:name="android.service.credentials.CredentialProviderService"/>
    </intent-filter>
    <meta-data
         android:name="android.credentials.provider"
         android:resource="@xml/provider"/>
</service>

上述权限和 intent 过滤器对于 Credential Manager 流程的正常运行至关重要。需要该权限,以便只有 Android 系统才能绑定到此服务。intent 过滤器用于发现此服务作为 Credential Manager 要使用的凭据提供方。

声明支持的凭据类型

在您的 res/xml 目录中,创建一个名为 provider.xml 的新文件。在此文件中,通过库中为每种凭据类型定义的常量来声明您的服务支持的凭据类型。在以下示例中,该服务支持传统密码和通行密钥,其常量分别定义为 TYPE_PASSWORD_CREDENTIALTYPE_PUBLIC_KEY_CREDENTIAL

<?xml version="1.0" encoding="utf-8"?>
<credential-provider xmlns:android="http://schemas.android.com/apk/res/android">
   <capabilities>
       <capability name="android.credentials.TYPE_PASSWORD_CREDENTIAL" />
       <capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
   </capabilities>
</credential-provider>

在以前的 API 级别上,凭据提供方与自动填充等 API 集成,用于密码和其他数据。这些提供方可以使用相同的内部基础架构来存储现有凭据类型,同时扩展它以支持其他类型,包括通行密钥。

提供方交互的两阶段方法

Credential Manager 与凭据提供方的交互分为两个阶段

  1. 第一阶段是开始/查询阶段,系统绑定到凭据提供方服务并调用 onBeginGetCredentialRequest()onBeginCreateCredentialRequest()onClearCredentialStateRequest() 方法,并附带 Begin… 请求。提供方必须处理这些请求,并使用 Begin… 响应进行回复,在其中填充表示要在账号选择器上显示的视觉选项的条目。每个条目都必须设置一个 PendingIntent
  2. 用户选择某个条目后,选择阶段便会开始,与该条目关联的 PendingIntent 会被触发,从而启动相应的提供方 Activity。用户完成与此 Activity 的交互后,凭据提供方必须在结束 Activity 之前将响应设置为 Activity 的结果。然后,此响应将发送到调用 Credential Manager 的客户端应用。

处理通行密钥创建

处理通行密钥创建的查询

当客户端应用希望创建通行密钥并将其存储到凭据提供方时,它们会调用 createCredential API。为了在您的凭据提供方服务中处理此请求,以便通行密钥实际存储在您的存储空间中,请完成以下部分中所示的步骤。

  1. 替换从 CredentialProviderService 扩展的服务中的 onBeginCreateCredentialRequest() 方法。
  2. 通过构建相应的 BeginCreateCredentialResponse 并通过回调传递它来处理 BeginCreateCredentialRequest
  3. 在构建 BeginCreateCredentialResponse 时,添加必需的 CreateEntries。每个 CreateEntry 都应对应一个可以保存凭据的账号,并且必须设置一个 PendingIntent 以及其他必需的元数据。

以下示例说明了如何实现这些步骤。

override fun onBeginCreateCredentialRequest(
  request: BeginCreateCredentialRequest,
  cancellationSignal: CancellationSignal,
  callback: OutcomeReceiver<BeginCreateCredentialResponse, CreateCredentialException>,
) {
  val response: BeginCreateCredentialResponse? = processCreateCredentialRequest(request)
  if (response != null) {
    callback.onResult(response)
  } else {
    callback.onError(CreateCredentialUnknownException())
  }
}

fun processCreateCredentialRequest(request: BeginCreateCredentialRequest): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }
  }
  // Request not supported
  return null
}

private fun handleCreatePasskeyQuery(
    request: BeginCreatePublicKeyCredentialRequest
    ): BeginCreateCredentialResponse {

    // Adding two create entries - one for storing credentials to the 'Personal'
    // account, and one for storing them to the 'Family' account. These
    // accounts are local to this sample app only.
    val createEntries: MutableList<CreateEntry> = mutableListOf()
    createEntries.add( CreateEntry(
        PERSONAL_ACCOUNT_ID,
        createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    createEntries.add( CreateEntry(
        FAMILY_ACCOUNT_ID,
        createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSKEY_INTENT)
    ))

    return BeginCreateCredentialResponse(createEntries)
}

private fun createNewPendingIntent(accountId: String, action: String): PendingIntent {
    val intent = Intent(action).setPackage(PACKAGE_NAME)

    // Add your local account ID as an extra to the intent, so that when
    // user selects this entry, the credential can be saved to this
    // account
    intent.putExtra(EXTRA_KEY_ACCOUNT_ID, accountId)

    return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQ_CODE,
        intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
        )
    )
}

您的 PendingIntent 构建应遵循以下原则

  • 相应的 Activity 应设置为显示任何必需的生物识别提示、确认或选择。
  • 提供方在调用相应 Activity 时所需的任何数据都应作为附加数据设置在用于创建 PendingIntent 的 intent 上,例如创建流程中的 accountId
  • 您的 PendingIntent 必须使用标志 PendingIntent.FLAG_MUTABLE 构建,以便系统可以将最终请求附加到 intent 附加数据中。
  • 您的 PendingIntent 不得使用标志 PendingIntent.FLAG_ONE_SHOT 构建,因为用户可能会选择一个条目,然后返回并重新选择它,这将导致 PendingIntent 触发两次。
  • 您的 PendingIntent 必须使用唯一的请求代码构建,以便每个条目都可以拥有自己对应的 PendingIntent

处理通行密钥创建请求的条目选择

  1. 当用户选择之前填充的 CreateEntry 时,相应的 PendingIntent 将被调用,并创建关联的提供方 Activity
  2. 调用 Activity 的 onCreate 方法后,访问关联的 intent 并将其传递到 PendingIntentHander 类以获取 ProviderCreateCredentialRequest
  3. 从请求中提取 requestJsoncallingAppInfoclientDataHash
  4. 从 intent 附加数据中提取本地 accountId。这是一个特定于示例应用的实现,并非必需。此账号 ID 可用于将此凭据存储在此特定账号 ID 下。
  5. 验证 requestJson。以下示例使用本地数据类(如 PublicKeyCredentialCreationOptions)将输入 JSON 转换为符合 WebAuthn 规范的结构化类。作为凭据提供方,您可以用自己的解析器替换此项。
  6. 如果调用源自原生 Android 应用,请检查调用应用的资产链接
  7. 显示身份验证提示。以下示例使用 Android 生物识别 API。
  8. 身份验证成功后,生成一个 credentialId 和一个密钥对
  9. 私钥存储在您的本地数据库中,并与 callingAppInfo.packageName 关联。
  10. 构造一个由公钥credentialId 组成的Web Authentication API JSON 响应。以下示例使用本地实用工具类(如 AuthenticatorAttestationResponseFidoPublicKeyCredential),这些类可帮助根据前面提到的规范构造 JSON。作为凭据提供方,您可以用自己的构建器替换这些类。
  11. 使用上面生成的 JSON 构造一个 CreatePublicKeyCredentialResponse
  12. 通过 PendingIntentHander.setCreateCredentialResponse()CreatePublicKeyCredentialResponse 设置为 Intent 的附加数据,并将该 intent 设置为 Activity 的结果。
  13. 完成 Activity。

以下代码示例说明了这些步骤。一旦调用 onCreate(),此代码需要在您的 Activity 类中进行处理。

val request =
  PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)

val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)
if (request != null && request.callingRequest is CreatePublicKeyCredentialRequest) {
  val publicKeyRequest: CreatePublicKeyCredentialRequest =
    request.callingRequest as CreatePublicKeyCredentialRequest
  createPasskey(
    publicKeyRequest.requestJson,
    request.callingAppInfo,
    publicKeyRequest.clientDataHash,
    accountId
  )
}

fun createPasskey(
  requestJson: String,
  callingAppInfo: CallingAppInfo?,
  clientDataHash: ByteArray?,
  accountId: String?
) {
  val request = PublicKeyCredentialCreationOptions(requestJson)

  val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
      override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
      ) {
        super.onAuthenticationError(errorCode, errString)
        finish()
      }

      override fun onAuthenticationFailed() {
        super.onAuthenticationFailed()
        finish()
      }

      override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
      ) {
        super.onAuthenticationSucceeded(result)

        // Generate a credentialId
        val credentialId = ByteArray(32)
        SecureRandom().nextBytes(credentialId)

        // Generate a credential key pair
        val spec = ECGenParameterSpec("secp256r1")
        val keyPairGen = KeyPairGenerator.getInstance("EC");
        keyPairGen.initialize(spec)
        val keyPair = keyPairGen.genKeyPair()

        // Save passkey in your database as per your own implementation

        // Create AuthenticatorAttestationResponse object to pass to
        // FidoPublicKeyCredential

        val response = AuthenticatorAttestationResponse(
          requestOptions = request,
          credentialId = credentialId,
          credentialPublicKey = getPublicKeyFromKeyPair(keyPair),
          origin = appInfoToOrigin(callingAppInfo),
          up = true,
          uv = true,
          be = true,
          bs = true,
          packageName = callingAppInfo.packageName
        )

        val credential = FidoPublicKeyCredential(
          rawId = credentialId, response = response
        )
        val result = Intent()

        val createPublicKeyCredResponse =
          CreatePublicKeyCredentialResponse(credential.json())

        // Set the CreateCredentialResponse as the result of the Activity
        PendingIntentHandler.setCreateCredentialResponse(
          result, createPublicKeyCredResponse
        )
        setResult(Activity.RESULT_OK, result)
        finish()
      }
    }
  )

  val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Create passkey for ${request.rp.name}")
    .setAllowedAuthenticators(
        BiometricManager.Authenticators.BIOMETRIC_STRONG
        /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
      )
    .build()
  biometricPrompt.authenticate(promptInfo)
}

fun appInfoToOrigin(info: CallingAppInfo): String {
  val cert = info.signingInfo.apkContentsSigners[0].toByteArray()
  val md = MessageDigest.getInstance("SHA-256");
  val certHash = md.digest(cert)
  // This is the format for origin
  return "android:apk-key-hash:${b64Encode(certHash)}"
}

处理密码创建请求的查询

要处理密码创建请求的查询,请执行以下操作

  • 在上一节中提到的 processCreateCredentialRequest() 方法内,在 switch 块中添加另一个用于处理密码请求的 case。
  • 在构建 BeginCreateCredentialResponse 时,添加必需的 CreateEntries
  • 每个 CreateEntry 都应对应一个可以保存凭据的账号,并且必须设置一个 PendingIntent 以及其他元数据。

以下示例说明了如何实现这些步骤

fun processCreateCredentialRequest(
    request: BeginCreateCredentialRequest
  ): BeginCreateCredentialResponse? {
  when (request) {
    is BeginCreatePublicKeyCredentialRequest -> {
      // Request is passkey type
      return handleCreatePasskeyQuery(request)
    }

    is BeginCreatePasswordCredentialRequest -> {
    // Request is password type
      return handleCreatePasswordQuery(request)
    }
  }
  return null
}

private fun handleCreatePasswordQuery(
    request: BeginCreatePasswordCredentialRequest
  ): BeginCreateCredentialResponse {
  val createEntries: MutableList<CreateEntry> = mutableListOf()

  // Adding two create entries - one for storing credentials to the 'Personal'
  // account, and one for storing them to the 'Family' account. These
  // accounts are local to this sample app only.
  createEntries.add(
    CreateEntry(
      PERSONAL_ACCOUNT_ID,
      createNewPendingIntent(PERSONAL_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )
  createEntries.add(
    CreateEntry(
      FAMILY_ACCOUNT_ID,
      createNewPendingIntent(FAMILY_ACCOUNT_ID, CREATE_PASSWORD_INTENT)
    )
  )

  return BeginCreateCredentialResponse(createEntries)
}

处理密码创建请求的条目选择

当用户选择一个已填充的 CreateEntry 时,相应的 PendingIntent 会执行并启动关联的 Activity。访问传递到 onCreate 中的关联 intent,并将其传递到 PendingIntentHander 类以获取 ProviderCreateCredentialRequest 方法。

以下示例说明了如何实现此过程。此代码需要在您 Activity 的 onCreate() 方法中进行处理。

val createRequest = PendingIntentHandler.retrieveProviderCreateCredentialRequest(intent)
val accountId = intent.getStringExtra(CredentialsRepo.EXTRA_KEY_ACCOUNT_ID)

val request: CreatePasswordRequest = createRequest.callingRequest as CreatePasswordRequest

// Fetch the ID and password from the request and save it in your database
<your_database>.addNewPassword(
    PasswordInfo(
        request.id,
        request.password,
        createRequest.callingAppInfo.packageName
    )
)

//Set the final response back
val result = Intent()
val response = CreatePasswordResponse()
PendingIntentHandler.setCreateCredentialResponse(result, response)
setResult(Activity.RESULT_OK, result)
this@<activity>.finish()

处理用户登录

用户登录通过以下步骤处理

  • 当客户端应用尝试登录用户时,它会准备一个 GetCredentialRequest 实例。
  • Android 框架通过绑定到这些服务,将此请求传播到所有适用的凭据提供方。
  • 然后,提供方服务会收到一个 BeginGetCredentialRequest,其中包含一个 BeginGetCredentialOption 列表,每个选项都包含可用于检索匹配凭据的参数。

为了在您的凭据提供方服务中处理此请求,请完成以下步骤

  1. 替换 onBeginGetCredentialRequest() 方法来处理请求。请注意,如果您的凭据被锁定,您可以立即在响应上设置 AuthenticationAction 并调用回调。

    private val unlockEntryTitle = "Authenticate to continue"
    
    override fun onBeginGetCredentialRequest(
        request: BeginGetCredentialRequest,
        cancellationSignal: CancellationSignal,
        callback: OutcomeReceiver<BeginGetCredentialResponse, GetCredentialException>,
    ) {
        if (isAppLocked()) {
            callback.onResult(BeginGetCredentialResponse(
                authenticationActions = mutableListOf(AuthenticationAction(
                    unlockEntryTitle, createUnlockPendingIntent())
                    )
                )
            )
            return
        }
        try {
            response = processGetCredentialRequest(request)
            callback.onResult(response)
        } catch (e: GetCredentialException) {
            callback.onError(GetCredentialUnknownException())
        }
    }
    

    需要解锁凭据才能返回任何 credentialEntries 的提供方,必须设置一个 PendingIntent,将用户导航到应用的解锁流程

    private fun createUnlockPendingIntent(): PendingIntent {
        val intent = Intent(UNLOCK_INTENT).setPackage(PACKAGE_NAME)
        return PendingIntent.getActivity(
        applicationContext, UNIQUE_REQUEST_CODE, intent, (
            PendingIntent.FLAG_MUTABLE
            or PendingIntent.FLAG_UPDATE_CURRENT
            )
        )
    }
    
  2. 从本地数据库检索凭据,并使用 CredentialEntries 设置它们以显示在选择器上。对于通行密钥,您可以在 intent 上将 credentialId 设置为附加数据,以便在用户选择此条目时知道它映射到哪个凭据。

    companion object {
        // These intent actions are specified for corresponding activities
        // that are to be invoked through the PendingIntent(s)
        private const val GET_PASSKEY_INTENT_ACTION = "PACKAGE_NAME.GET_PASSKEY"
        private const val GET_PASSWORD_INTENT_ACTION = "PACKAGE_NAME.GET_PASSWORD"
    
    }
    
    fun processGetCredentialsRequest(
    request: BeginGetCredentialRequest
    ): BeginGetCredentialResponse {
        val callingPackage = request.callingAppInfo?.packageName
        val credentialEntries: MutableList<CredentialEntry> = mutableListOf()
    
        for (option in request.beginGetCredentialOptions) {
            when (option) {
                is BeginGetPasswordOption -> {
                    credentialEntries.addAll(
                            populatePasswordData(
                                callingPackage,
                                option
                            )
                        )
                    }
                    is BeginGetPublicKeyCredentialOption -> {
                        credentialEntries.addAll(
                            populatePasskeyData(
                                callingPackage,
                                option
                            )
                        )
                    )
                } else -> {
                    Log.i(TAG, "Request not supported")
                }
            }
        }
        return BeginGetCredentialResponse(credentialEntries)
    }
    
  3. 从数据库查询凭据,创建通行密钥和密码条目以填充。

    private fun populatePasskeyData(
        callingAppInfo: CallingAppInfo,
        option: BeginGetPublicKeyCredentialOption
    ): List<CredentialEntry> {
      val passkeyEntries: MutableList<CredentialEntry> = mutableListOf()
      val request = PublicKeyCredentialRequestOptions(option.requestJson)
      // Get your credentials from database where you saved during creation flow
      val creds = <getCredentialsFromInternalDb(request.rpId)>
      val passkeys = creds.passkeys
      for (passkey in passkeys) {
          val data = Bundle()
          data.putString("credId", passkey.credId)
          passkeyEntries.add(
              PublicKeyCredentialEntry(
                  context = applicationContext,
                  username = passkey.username,
                  pendingIntent = createNewPendingIntent(
                      GET_PASSKEY_INTENT_ACTION,
                      data
                  ),
                  beginPublicKeyCredentialOption = option,
                  displayName = passkey.displayName,
                  icon = passkey.icon
              )
          )
      }
      return passkeyEntries
    }
    
    // Fetch password credentials and create password entries to populate to
    // the user
    private fun populatePasswordData(
    callingPackage: String,
    option: BeginGetPasswordOption
    ): List<CredentialEntry> {
        val passwordEntries: MutableList<CredentialEntry> = mutableListOf()
    
        // Get your password credentials from database where you saved during
        // creation flow
        val creds = <getCredentialsFromInternalDb(callingPackage)>
        val passwords = creds.passwords
        for (password in passwords) {
            passwordEntries.add(
                PasswordCredentialEntry(
                    context = applicationContext,
                    username = password.username,
                    pendingIntent = createNewPendingIntent(
                    GET_PASSWORD_INTENT
                    ),
                    beginGetPasswordOption = option
                        displayName = password.username,
                    icon = password.icon
                )
            )
        }
        return passwordEntries
    }
    
    private fun createNewPendingIntent(
        action: String,
        extra: Bundle? = null
    ): PendingIntent {
        val intent = Intent(action).setPackage(PACKAGE_NAME)
        if (extra != null) {
            intent.putExtra("CREDENTIAL_DATA", extra)
        }
    
        return PendingIntent.getActivity(
            applicationContext, UNIQUE_REQUEST_CODE, intent,
            (PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
        )
    }
    
  4. 一旦您查询并填充了凭据,现在您需要处理用户选择凭据的选择阶段,无论是通行密钥还是密码。

处理用户选择通行密钥

  1. 在相应 Activity 的 onCreate 方法中,检索关联的 intent,并将其传递给 PendingIntentHandler.retrieveProviderGetCredentialRequest()
  2. 从上面检索到的请求中提取 GetPublicKeyCredentialOption。随后,从该选项中提取 requestJsonclientDataHash
  3. 从 intent 附加数据中提取 credentialId,该 ID 是凭据提供方在设置相应的 PendingIntent 时填充的。
  4. 使用上面访问的请求参数从本地数据库中提取通行密钥。
  5. 断言通行密钥通过提取的元数据和用户验证是有效的。

    val getRequest =
        PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    val publicKeyRequest =
    getRequest.credentialOption as GetPublicKeyCredentialOption
    
    val requestInfo = intent.getBundleExtra("CREDENTIAL_DATA")
    val credIdEnc = requestInfo.getString("credId")
    
    // Get the saved passkey from your database based on the credential ID
    // from the publickeyRequest
    val passkey = <your database>.getPasskey(credIdEnc)
    
    // Decode the credential ID, private key and user ID
    val credId = b64Decode(credIdEnc)
    val privateKey = b64Decode(passkey.credPrivateKey)
    val uid = b64Decode(passkey.uid)
    
    val origin = appInfoToOrigin(getRequest.callingAppInfo)
    val packageName = getRequest.callingAppInfo.packageName
    
    validatePasskey(
        publicKeyRequest.requestJson,
        origin,
        packageName,
        uid,
        passkey.username,
        credId,
        privateKey
    )
    
  6. 要验证用户,请显示生物识别提示(或其他断言方法)。以下代码片段使用 Android 生物识别 API。

  7. 身份验证成功后,根据 W3 Web Authentication 断言规范构建 JSON 响应。在以下代码片段中,辅助数据类(如 AuthenticatorAssertionResponse)用于接收结构化参数并将其转换为所需的 JSON 格式。响应包含来自 WebAuthn 凭据私钥的数字签名。依赖方服务器可以验证此签名,以便在用户登录前对其进行身份验证。

  8. 使用上面生成的 JSON 构造一个 PublicKeyCredential,并将其设置到最终的 GetCredentialResponse 上。将此最终响应设置为此 Activity 的结果。

以下示例说明了如何实现这些步骤

val request = PublicKeyCredentialRequestOptions(requestJson)
val privateKey: ECPrivateKey = convertPrivateKey(privateKeyBytes)

val biometricPrompt = BiometricPrompt(
    this,
    <executor>,
    object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationError(
        errorCode: Int, errString: CharSequence
        ) {
            super.onAuthenticationError(errorCode, errString)
            finish()
        }

        override fun onAuthenticationFailed() {
            super.onAuthenticationFailed()
            finish()
        }

        override fun onAuthenticationSucceeded(
        result: BiometricPrompt.AuthenticationResult
        ) {
        super.onAuthenticationSucceeded(result)
        val response = AuthenticatorAssertionResponse(
            requestOptions = request,
            credentialId = credId,
            origin = origin,
            up = true,
            uv = true,
            be = true,
            bs = true,
            userHandle = uid,
            packageName = packageName
        )

        val sig = Signature.getInstance("SHA256withECDSA");
        sig.initSign(privateKey)
        sig.update(response.dataToSign())
        response.signature = sig.sign()

        val credential = FidoPublicKeyCredential(
            rawId = credId, response = response
        )
        val result = Intent()
        val passkeyCredential = PublicKeyCredential(credential.json)
        PendingIntentHandler.setGetCredentialResponse(
            result, GetCredentialResponse(passkeyCredential)
        )
        setResult(RESULT_OK, result)
        finish()
        }
    }
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
    .setTitle("Use your screen lock")
    .setSubtitle("Use passkey for ${request.rpId}")
    .setAllowedAuthenticators(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
            /* or BiometricManager.Authenticators.DEVICE_CREDENTIAL */
        )
    .build()
biometricPrompt.authenticate(promptInfo)

处理密码身份验证的用户选择

  1. 在您的相应 Activity 中,访问传递到 onCreate 中的 intent,并使用 PendingIntentHandler 提取 ProviderGetCredentialRequest
  2. 在请求中使用 GetPasswordOption 来检索传入包名的密码凭据。

    val getRequest =
    PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
    
    val passwordOption = getRequest.credentialOption as GetPasswordCredentialOption
    
    val username = passwordOption.username
    // Fetch the credentials for the calling app package name
    val creds = <your_database>.getCredentials(callingAppInfo.packageName)
    val passwords = creds.passwords
    val it = passwords.iterator()
    var password = ""
    while (it.hasNext() == true) {
        val passwordItemCurrent = it.next()
        if (passwordItemCurrent.username == username) {
           password = passwordItemCurrent.password
           break
        }
    }
    
  3. 检索后,为选定的密码凭据设置响应。

    // Set the response back
    val result = Intent()
    val passwordCredential = PasswordCredential(username, password)
    PendingIntentHandler.setGetCredentialResponse(
    result, GetCredentialResponse(passwordCredential)
    )
    setResult(Activity.RESULT_OK, result)
    finish()
    

处理身份验证操作条目的选择

前文所述,如果凭据被锁定,凭据提供方可以设置一个 AuthenticationAction。如果用户选择此条目,则会调用与 PendingIntent 中设置的 intent 操作对应的 Activity。然后,凭据提供方可以显示生物识别身份验证流程或类似机制来解锁凭据。成功后,凭据提供方必须构造一个 BeginGetCredentialResponse类似于上面描述的用户登录处理方式,因为凭据现在已解锁。然后,在将准备好的 intent 设置为结果并完成 Activity 之前,必须通过 PendingIntentHandler.setBeginGetCredentialResponse() 方法设置此响应。

清除凭据请求

客户端应用可能会请求清除为凭据选择维护的任何状态,例如凭据提供方可能会记住先前选择的凭据并在下次仅返回该凭据。客户端应用调用此 API 并期望清除粘性选择。您的凭据提供方服务可以通过重写 onClearCredentialStateRequest() 方法来处理此请求

override fun onClearCredentialStateRequest(
    request: android.service.credentials.ClearCredentialStateRequest,
    cancellationSignal: CancellationSignal,
    callback: OutcomeReceiver<Void?, ClearCredentialException>,
  ) {
    // Delete any maintained state as appropriate.
}

为了允许用户从密码、通行密钥和自动填充屏幕打开您的提供方设置,凭据提供方应用应在 res/xml/provider.xml 中实现 credential-providersettingsActivity 清单属性。如果用户点击密码、通行密钥和自动填充服务列表中的提供方名称,此属性允许您使用 intent 打开您应用自己的设置屏幕。将此属性的值设置为要从设置屏幕启动的 Activity 的名称。

<credential-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsSubtitle="Example settings provider name"
    android:settingsActivity="com.example.SettingsActivity">
    <capabilities>
        <capability name="android.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
    </capabilities>
</credential-provider>
Diagram showing the change and open button functions
图 1:更改按钮会打开现有选择对话框,允许用户选择其首选凭据提供方。打开按钮会启动清单更改中定义的设置 Activity,并专门为该提供方打开一个设置页面。

设置 intent

打开设置android.settings.CREDENTIAL_PROVIDER intent 会显示一个设置屏幕,用户可以在其中选择他们首选的凭据提供方以及其他凭据提供方。

The Passwords, passkeys, and autofill settings screen
图 2:密码、通行密钥和自动填充设置屏幕。

首选凭据服务ACTION_REQUEST_SET_AUTOFILL_SERVICE intent 会将用户重定向到首选提供方选择屏幕。此屏幕上选择的提供方将成为首选凭据和自动填充提供方。

Diagram showing the change and open button functions
图 3:密码、通行密钥和自动填充的首选服务设置屏幕。

获取特权应用的允许列表

网页浏览器等特权应用通过在 Credential Manager 的 GetCredentialRequest()CreatePublicKeyCredentialRequest() 方法中设置 origin 参数,代表其他依赖方进行 Credential Manager 调用。为了处理这些请求,凭据提供方使用 getOrigin() API 检索 origin

为了检索 origin,凭据提供方应用需要将特权和受信任调用方的列表传递给 androidx.credentials.provider.CallingAppInfo's getOrigin() API。此允许列表必须是有效的 JSON 对象。如果从 signingInfo 获取的 packageName 和证书指纹与传递给 getOrigin() API 的 privilegedAllowlist 中找到的应用的那些相匹配,则会返回 origin。获取 origin 值后,提供方应用应将其视为特权调用,并在 AuthenticatorResponse 中的客户端数据上设置此 origin,而不是使用调用应用的签名计算 origin

如果您检索到 origin,请直接使用 CreatePublicKeyCredentialRequest()GetPublicKeyCredentialOption() 中提供的 clientDataHash,而不是在签名请求期间组装和哈希 clientDataJSON。为了避免 JSON 解析问题,请在 attestation 和 assertion 响应中为 clientDataJSON 设置一个占位符值。Google 密码管理器对 getOrigin() 的调用使用公开可用的允许列表。作为凭据提供方,您可以使用此列表,也可以按照 API 描述的 JSON 格式提供自己的列表。由提供方选择使用哪个列表。要获得与第三方凭据提供方的特权访问权限,请参阅第三方提供的文档。

在设备上启用提供方

用户必须通过 设备设置 > 密码和账号 > 您的提供方 > 启用或停用 来启用提供方。

fun createSettingsPendingIntent(): PendingIntent