创建通行密钥

在用户可以使用通行密钥进行身份验证之前,您的应用必须先注册或创建其账号的通行密钥。

如需创建通行密钥,请从应用服务器获取创建通行密钥所需的详细信息,然后调用 Credential Manager API,该 API 会返回公钥和私钥对。返回的私钥会作为通行密钥存储在凭据提供程序(例如 Google 密码管理工具)中。公钥存储在您的应用服务器上。

通行密钥存储在凭据提供程序中,公钥存储在应用服务器上
图 1: 通行密钥的创建

前提条件

请确保您已设置数字资产链接,并且以搭载 Android 9(API 级别 28)或更高版本的设备为目标平台。

概览

本指南重点介绍了在信赖方客户端应用中创建通行密钥所需的更改,并简要概述了信赖方应用服务器的实现。如需详细了解服务器端集成,请参阅服务器端通行密钥注册

  1. 为应用添加依赖项:添加所需的 Credential Manager 库。
  2. 实例化 Credential Manager:创建 Credential Manager 实例。
  3. 从应用服务器获取凭据创建选项:从应用服务器向客户端应用发送创建通行密钥所需的详细信息,例如有关应用、用户的信息,以及 challenge 和其他字段。
  4. 请求通行密钥:在应用中,使用从应用服务器收到的详细信息创建 GetPublicKeyCredentialOption 对象,并使用此对象调用 credentialManager.getCredential() 方法来创建通行密钥。
  5. 处理通行密钥创建响应:当您在客户端应用上收到凭据时,必须对公钥进行编码、序列化,然后将其发送到应用服务器。您还必须处理在创建通行密钥时可能发生的每种异常。
  6. 在服务器上验证并保存公钥:完成服务器端步骤,以验证凭据的来源,然后保存公钥。
  7. 通知用户:通知用户其通行密钥已创建。

1. 为应用添加依赖项

将以下依赖项添加到应用模块的 build.gradle 文件中:

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.6.0-beta03")
    implementation("androidx.credentials:credentials-play-services-auth:1.6.0-beta03")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.6.0-beta03"
    implementation "androidx.credentials:credentials-play-services-auth:1.6.0-beta03"
}

2. 实例化 Credential Manager

使用应用或 activity 上下文创建 CredentialManager 对象。

// Use your app or activity context to instantiate a client instance of
// CredentialManager.
private val credentialManager = CredentialManager.create(context)

3. 从应用服务器获取凭据创建选项

当用户点击“创建通行密钥”按钮或新用户注册时,从您的应用向应用服务器发出请求,以获取启动通行密钥注册流程所需的信息。

在应用服务器中使用符合 FIDO 标准的库,向客户端应用发送创建通行密钥所需的信息,例如有关用户、应用和其他配置属性的信息。如需了解详情,请参阅服务器端通行密钥注册

在客户端应用中,对应用服务器发送的公钥创建选项进行解码。这些数据通常以 JSON 格式表示。如需详细了解如何针对 Web 客户端进行此解码,请参阅编码和解码。对于 Android 客户端应用,您必须单独处理解码。

以下代码段展示了应用服务器发送的公钥创建选项的结构:

{
  "challenge": "<base64url-encoded challenge>",
  "rp": {
    "name": "<relying party name>",
    "id": "<relying party host name>"
  },
  "user": {
    "id": "<base64url-encoded user ID>",
    "name": "<user name>",
    "displayName": "<user display name>"
  },
  "pubKeyCredParams": [
    {
      "type": "public-key",
      "alg": -7
    }
  ],
  "attestation": "none",
  "excludeCredentials": [
    {
        "id": "<base64url-encoded credential ID to exclude>", 
        "type": "public-key"
    }
  ],
  "authenticatorSelection": {
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  }
}

公钥创建选项中的关键字段包括:

  • challenge:服务器生成的随机字符串,用于防止重放攻击。
  • rp:有关应用的详细信息。
    • rp.name:应用的名称。
    • rp.id:应用的网域或子网域。
  • user:有关用户的详细信息。
    • id:用户的唯一 ID。此值不得包含个人身份信息,例如电子邮件地址或用户名。您可以使用 16 字节的随机值。
    • name:用户可识别的账号唯一标识符,例如电子邮件地址或用户名。此 ID 将显示在账号选择器中。如果使用用户名,请使用与密码身份验证相同的值。
    • displayName:账号的可选易记名称,用于在账号选择器中显示。
  • authenticatorSelection:将用于身份验证的设备的详细信息。

    • authenticatorAttachment:表示首选的身份验证器。可能的值如下: - platform:此值用于用户设备中内置的身份验证器,例如指纹传感器。 - cross-platform:此值用于漫游设备,例如安全密钥。它通常不会在通行密钥上下文中使用。 - 未指定(推荐):将此值留空可让用户灵活地在其首选设备上创建通行密钥。在大多数情况下,最好不指定参数。
      • requireResidentKey:如需创建通行密钥,请将此 Boolean 字段的值设置为 true
      • residentKey:如需创建通行密钥,请将值设置为 required
      • userVerification:用于指定在通行密钥注册期间进行用户验证的要求。可能的值如下: - preferred:如果您优先考虑用户体验而非保护,请使用此值,例如在用户验证造成的摩擦比保护更严重的环境中。 - required:如果需要调用设备上可用的用户验证方法,请使用此值。 - discouraged:如果建议不要使用用户验证方法,请使用此值。
        如需详细了解 userVerification,请参阅 userVerification 深入分析
  • excludeCredentials:在数组中列出凭据 ID,以防止在已存在具有相同凭据提供程序的通行密钥时创建重复的通行密钥。

4. 申请通行密钥

解析服务器端公钥创建选项后,通过将这些选项封装在 CreatePublicKeyCredentialRequest 对象中并调用 createCredential() 来创建通行密钥。

createPublicKeyCredentialRequest 包含以下内容:

  • requestJson:应用服务器发送的凭据创建选项。
  • preferImmediatelyAvailableCredentials:这是一个可选的布尔值字段,用于定义是否要仅使用本地可用的凭据或凭据提供程序同步的凭据来完成请求,而不使用安全密钥或混合密钥流中的凭据。可能的用途如下:
    • false(默认):如果对 Credential Manager 的调用是由显式用户操作触发的,请使用此值。
    • true:如果 Credential Manager 是机会性调用(例如在首次打开应用时),请使用此值。
      如果您将该值设置为 true,并且没有立即可用的凭据,Credential Manager 将不会显示任何界面,而请求将立即失败,针对获取请求返回 NoCredentialException,针对创建请求返回 CreateCredentialNoCreateOptionException
  • origin:此字段会自动为 Android 应用设置。对于需要设置 origin 的浏览器和类似特权应用,请参阅让特权应用代表其他方发出 Credential Manager 调用
  • isConditional:这是一个可选字段,默认值为 false。如果您将此设置设为 true,并且用户没有通行密钥,那么在用户下次使用已保存的密码登录时,系统会自动为其创建通行密钥。通行密钥存储在用户的凭据提供方中。条件创建功能需要使用 androidx.credentials最新版本

调用 createCredential() 函数会启动 Credential Manager 的内置底部操作表界面,该界面会提示用户使用通行密钥,并选择凭据提供方和用于存储的账号。不过,如果 isConditional 设置为 true,则系统不会显示底部工作表界面,而是自动创建通行密钥。

5. 处理响应

使用设备的屏锁验证用户身份后,系统会创建通行密钥并将其存储在用户选择的凭据提供方中。

成功调用 createCredential() 后的响应是一个 PublicKeyCredential 对象。

PublicKeyCredential 如下所示:

{
  "id": "<identifier>",
  "type": "public-key",
  "rawId": "<identifier>",
  "response": {
    "clientDataJSON": "<ArrayBuffer encoded object with the origin and signed challenge>",
    "attestationObject": "<ArrayBuffer encoded object with the public key and other information.>"
  },
  "authenticatorAttachment": "platform"
}

在客户端应用中,序列化对象并将其发送到应用服务器。

添加代码来处理失败情况,如以下代码段所示:

fun handleFailure(e: CreateCredentialException) {
    when (e) {
        is CreatePublicKeyCredentialDomException -> {
            // Handle the passkey DOM errors thrown according to the
            // WebAuthn spec.
        }
        is CreateCredentialCancellationException -> {
            // The user intentionally canceled the operation and chose not
            // to register the credential.
        }
        is CreateCredentialInterruptedException -> {
            // Retry-able error. Consider retrying the call.
        }
        is CreateCredentialProviderConfigurationException -> {
            // Your app is missing the provider configuration dependency.
            // Most likely, you're missing the
            // "credentials-play-services-auth" module.
        }
        is CreateCredentialCustomException -> {
            // You have encountered an error from a 3rd-party SDK. If you
            // make the API call with a request object that's a subclass of
            // CreateCustomCredentialRequest using a 3rd-party SDK, then you
            // should check for any custom exception type constants within
            // that SDK to match with e.type. Otherwise, drop or log the
            // exception.
        }
        else -> Log.w(TAG, "Unexpected exception type ${e::class.java.name}")
    }
}

6. 验证并保存应用服务器上的公钥

在应用服务器上,您必须验证公钥凭据,然后保存公钥

如需验证公钥凭据的来源,请将其与已批准的应用许可名单进行比较。如果密钥的来源无法识别,则拒绝该密钥。

如需获取应用的 SHA 256 指纹,请执行以下操作:

  1. 在终端中运行以下命令,以打印发布版应用的签名证书:

    keytool -list -keystore <path-to-apk-signing-keystore>
    

    在响应中,找到签名证书的 SHA 256 指纹,即 Certificate fingerprints block : SHA256

  2. 使用 base64url 编码对 SHA256 指纹进行编码。此 Python 示例演示了如何正确编码指纹:

    import binascii
    import base64
    fingerprint = '<SHA256 finerprint>' # your app's SHA256 fingerprint
    print(base64.urlsafe_b64encode(binascii.a2b_hex(fingerprint.replace(':', ''))).decode('utf8').replace('=', ''))
    
  3. 在上一步的输出开头添加 android:apk-key-hash:,使输出类似于以下内容:

    android:apk-key-hash:<encoded SHA 256 fingerprint>
    

    结果应与应用服务器上允许的来源相匹配。如果您有多个签名证书(例如用于调试和发布的证书)或多个应用,请重复以上过程并在应用服务器上接受所有这些来源有效。

7. 通知用户

成功创建通行密钥后,请通知用户有关通行密钥的信息,并告知他们可以从凭据提供方应用或应用设置中管理通行密钥。使用自定义对话框、通知或信息条通知用户。由于恶意实体意外创建通行密钥需要立即发出安全提醒,因此请考虑通过电子邮件等外部通信方式来补充这些应用内方法。

提升用户体验

为了在实现使用 Credential Manager 进行注册的同时提升用户体验,请考虑添加用于恢复凭据和禁止显示自动填充对话框的功能。

添加了在新设备上恢复凭据的功能

如需让用户在新设备上顺畅登录其账号,请实现恢复凭据功能。添加使用 BackupAgent 恢复凭据的功能后,当用户在新设备上打开已恢复的应用时,系统会自动登录用户,让用户可以立即使用您的应用。

禁止在凭据字段中自动填充(可选)

对于用户需要使用 Credential Manager 的底部操作表界面进行身份验证的应用界面,请向用户名和密码字段添加 isCredential 属性。这样可防止自动填充对话框(FillDialogSaveDialog)与 Credential Manager 的底部动作条界面重叠。

Android 14 及更高版本支持 isCredential 属性。

以下示例演示了如何将 isCredential 属性添加到应用的相关视图中的相关用户名和密码字段:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:isCredential="true" />

后续步骤