使用 Credential Manager 实现恢复凭据

本页面介绍了如何创建、登录和删除恢复密钥。

版本兼容性

Credential Manager 的 Restore Credentials 功能适用于搭载 Android 9(API 级别 28)及更高版本、Google Play 服务 (GMS) 核心版本 242200000 (v24.22) 或更高版本以及 androidx.credentials 库版本 1.5.0 或更高版本的设备。在没有 Google Play 服务或搭载旧版 GMS 的设备上,捕获 CreateCredentialProviderConfigurationException 和 GetCredentialProviderConfigurationException,以便您的应用顺利回退到标准登录。

前提条件

配置一个类似于通行密钥服务器的 WebAuthn 信赖方服务器。如果您已设置服务器来处理通行密钥身份验证,则可以为恢复密钥重复使用相同的 WebAuthn 注册和断言端点。

依赖项

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

Kotlin

dependencies {
    implementation("androidx.credentials:credentials:1.7.0-alpha02")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha02")
}

Groovy

dependencies {
    implementation "androidx.credentials:credentials:1.7.0-alpha02"
    implementation "androidx.credentials:credentials-play-services-auth:1.7.0-alpha02"
}

“恢复凭证”功能适用于 androidx.credentials 库的 1.5.0 及更高版本。不过,建议您尽可能使用依赖项的最新稳定版本。

android:allowBackup 如何影响“恢复凭证”功能

android:allowBackup 清单属性用于控制应用数据备份,该属性会通过以下三种不同的方式与 Restore Credentials 互动:

  • 恢复密钥传输(与 allowBackup 无关):无论 android:allowBackup 是 true 还是 false,Google Play 服务都会使用端到端加密云备份或本地设备到设备 (D2D) 传输来传输 RestoreCredential 密钥对。对于 D2D 传输,恢复密钥始终会传输;只有在云端备份期间,如果用户在 Android 设置中停用了云端备份,恢复密钥才不会传输。
  • BackupAgent 中的后台恢复(需要 android:allowBackup="true"):只有在清单中配置了 android:allowBackup="true" 和 android:backupAgent 时,系统才会在用户在新设备上打开应用之前执行静默后台登录 (BackupAgent.onRestoreFinished())。如果为 android:allowBackup="false",则在用户首次启动主 Activity 时恢复会话。您也可以同时实现这两种方法,使恢复功能更加强大。
  • Android Studio 云端备份测试(需要 android:allowBackup="true"):在 Android Studio 的备份和恢复工具中测试云端备份类型需要 android:allowBackup="true",而设备到设备测试则无需 allowBackup。

概览

Restore Credentials 在用户的旧设备和新设备上遵循一个三阶段生命周期:

  1. 在旧设备上创建恢复密钥:用户登录后(或者如果用户已登录且本地存储空间中未记录恢复密钥,则在应用启动时),从依赖方服务器提取创建选项,然后调用 credentialManager.createCredential()。
  2. 在新设备上使用恢复密钥登录:使用两层策略检索已转移的 RestoreCredential:首先在 BackupAgent.onRestoreFinished() 中以静默方式检索(当 android:allowBackup="true" 时),以便后台同步和推送通知立即恢复;其次在首次启动应用时,在启动器 Activity.onCreate() 中作为后备方案检索。
  3. 在退出登录时删除恢复密钥:当用户退出登录或其会话被服务器失效时,请使用 ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) 调用 credentialManager.clearCredentialState(),并清除本地同步标志。

创建恢复密钥

您的应用应涵盖用户登录的所有情况,以确保为活跃用户创建恢复密钥。在以下情况下,应创建恢复密钥:

  • 如果用户已登录,并且尚未创建恢复密钥(例如在主 Activity 的 onCreate() 方法中)。
  • 用户登录或完成新账号注册流程时。

为避免在每次应用启动时都调用 createCredential(),请在本地存储空间中存储本地同步标志,例如 has_synced_restore_credential。请勿将此标志存储在默认备份的 SharedPreferences 或 DataStore 中,除非该文件在 data_extraction_rules.xml 中被排除;否则,自动备份会将 has_synced_restore_credential = true 复制到新设备上,并阻止新设备生成或轮替自己的恢复密钥。

实例化 Credential Manager

使用应用的 activity 上下文实例化 CredentialManager 对象。

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

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

在应用服务器中使用符合 FIDO 标准的库,向客户端应用发送创建恢复凭据所需的 PublicKeyCredentialCreationOptions JSON。如需详细了解服务器端实现,请参阅服务器端指南。

创建选项 JSON 遵循与通行密钥注册相同的结构:

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

确保您的创建选项 JSON 满足以下要求:

  • challenge 和 user.id:将这两个值编码为不带填充字符的 Base64URL 字符串(将 + 替换为 -,将 / 替换为 _,并去除尾随的 =)。标准 Base64 字符或原始整数 ID 会导致 CreateRestoreCredentialDomException 或 IllegalArgumentException。
  • rp.id:设置为依赖方的网域(例如 example.com)。
  • pubKeyCredParams:包括 {"type": "public-key", "alg": -7} (ES256) 和 {"type": "public-key", "alg": -257} (RS256)。

创建恢复密钥

从服务器收到公钥创建选项 JSON 后,将 JSON 封装在 CreateRestoreCredentialRequest 对象中,并在 CredentialManager 对象上调用 createCredential()。

// createRestoreRequest contains the details sent by the server 
val response = credentialManager.createCredential(context, createRestoreRequest)

代码要点

  • CreateRestoreCredentialRequest 对象包含以下字段:

    • requestJson:应用服务器以 Web Authentication API 格式发送的凭据创建选项,适用于 PublicKeyCredentialCreationOptionsJSON。
    • isCloudBackupEnabled:Boolean 字段,用于确定是否应将恢复密钥备份到云端。默认情况下,此标志为 true。此字段具有以下值:

      • true:(推荐)如果用户启用了 Google 备份和端到端加密(例如屏幕锁定),此值会将恢复密钥备份到云端。
      • false:此值会将密钥保存在本地,而不是云端。如果用户选择从云端恢复,则新设备上无法使用该密钥,但在本地有线或 Wi-Fi 设备到设备 (D2D) 设置期间,该密钥仍会传输。

处理凭据创建响应

Credential Manager API 会返回 CreateRestoreCredentialResponse 类型的响应。此响应包含采用 JSON 格式的公钥凭证注册响应。

将 response.registrationResponseJson 从您的应用发送到依赖方服务器,以验证公钥并将其与用户账号一起存储,然后在本地存储空间中记录本地同步标记。如需详细了解服务器端验证,请参阅通行密钥指南。

在恢复密钥创建过程中,请处理以下异常:

  • CreateRestoreCredentialDomException:如果 requestJson 无效且不遵循 PublicKeyCredentialCreationOptionsJSON 的 WebAuthn 格式,则会发生此异常。
  • E2eeUnavailableException:如果 isCloudBackupEnabled 为 true,但用户的设备没有数据备份或端到端加密(例如屏幕锁定),则会发生此异常。
    为确保即使在云端 E2EE 不可用时也能为本地设备到设备 (D2D) 传输创建恢复凭据,请捕获 E2eeUnavailableException,并立即使用 CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false) 重试 createCredential()。
  • IllegalArgumentException:如果 requestJson 为空、不是有效的 JSON,或者没有符合 WebAuthn 规范的有效 Base64URL 编码 user.id,则会发生此异常。

使用恢复密钥登录

在设备设置过程中,使用恢复凭证功能以静默方式让用户登录。

从应用服务器获取凭据检索选项

在新设备上调用 getCredential() 之前,请向应用服务器请求 WebAuthn PublicKeyCredentialRequestOptions JSON 质询。由于新设备上的应用尚不知道是哪个用户在登录,因此此端点必须未经身份验证(无用户发现)。如需了解相关的服务器指南,请参阅使用通行密钥登录和服务器端身份验证指南。

检索选项 JSON 具有以下结构:

{
  "challenge": "<your app challenge>",
  "allowCredentials": [],
  "rpId": "<your app server domain>"
}

在服务器上配置检索选项 JSON,如下所示:

  • allowCredentials:省略此字段或传递空数组 ([]),因为在恢复之前无法得知用户的账号。getCredential() 返回后,您的服务器会根据 response.userHandle 字段(与创建期间注册的 user.id 匹配)识别用户,并按 id (credentialId) 查找公钥。
  • userVerification:设置为 "discouraged"(或 "preferred")。由于恢复凭证会在后台静默运行,不显示生物识别或屏幕锁定界面,因此 Android 会将 "required" 替换为 "discouraged",并且返回到您服务器的身份验证器数据会将“用户已验证”(uv) 标志设置为 false。将 WebAuthn 服务器验证器配置为接受 uv = false 以进行恢复密钥断言。

获取恢复密钥

如需在新设备上获取恢复密钥,请将服务器的 requestJson 封装在 GetCredentialRequest 内的 GetRestoreCredentialOption 中,然后对 CredentialManager 对象调用 getCredential()。

实施两层恢复策略,以便无论是否运行后台应用数据恢复,用户都能登录:

  • 第 1 层级(在 BackupAgent.onRestoreFinished() 中进行后台恢复):如果启用了应用数据备份 (android:allowBackup="true"),请将 BackupAgent 子类化并替换 onRestoreFinished()(请勿使用 onRestore(),该方法仅针对旧版键值对备份调用)。由于 onRestoreFinished() 是同步的,而 getCredential() 是 Kotlin suspend 函数,因此请使用 runBlocking(Dispatchers.IO) 桥接调用。在没有恢复密钥时,静默捕获 NoCredentialException。
  • 第 2 层(启动器 Activity.onCreate() 中的前台回退):在首次启动应用且不存在本地会话时,请先从 lifecycleScope 调用 getCredential(),然后再显示登录界面。这会处理具有 android:allowBackup="false" 的应用,并在 onRestoreFinished() 运行时设备处于离线状态的情况下进行恢复。

如果您向已使用 android:allowBackup="true" 自动备份的应用添加新的 BackupAgent,请同时在 AndroidManifest.xml 中的 <application> 元素上设置 android:fullBackupOnly="true",以便 Android 继续使用自动备份,而不是切换到旧版键值对备份。

// Fetch the options required to get the restore key
val authenticationJson = fetchAuthenticationJson()

// Create the GetRestoreCredentialRequest object
val options = GetRestoreCredentialOption(authenticationJson)
val getRequest = GetCredentialRequest(listOf(options))

val response = credentialManager.getCredential(context, getRequest)

// Type-check and extract the restore credential
val credential = response.credential as RestoreCredential

Credential Manager API 会返回一个 GetCredentialResponse,其 credential 属性是一个包含 authenticationResponseJson 的 RestoreCredential(而非 PublicKeyCredential)。

处理登录响应

将 restoreCredential.authenticationResponseJson 从应用发送到您的依赖方服务器,以验证加密断言并为用户颁发新的会话令牌。如需了解服务器端验证详情,请参阅使用通行密钥登录。

在为恢复密钥设计后端存储和生命周期政策时,请遵循以下准则:

  • 区分恢复密钥与用户通行密钥:在数据库中标记恢复密钥,以便在面向用户的通行密钥管理界面中隐藏这些密钥,并免除用户验证 (uv = true) 检查。
  • 支持每个用户拥有多个活跃设备:一个用户可以拥有多个设备(例如手机和平板电脑)。允许每个用户账号有多个活跃的恢复密钥(例如,按设备或安装 ID 键控),而不是覆盖单行。
  • 设置适当的 TTL 并清理孤立密钥:卸载应用或清除应用数据会删除本地恢复密钥,而不会通知您的服务器。保留恢复密钥足够长的时间,以便在设备升级期间(即使在转移后用户在旧设备上退出登录)仍能使用,并根据闲置 TTL 或每个用户的密钥限制来清理过时的密钥。

删除恢复密钥

Credential Manager 是无状态的,不知道用户活动,因此不会自动删除已使用的恢复密钥。如需删除恢复密钥,请调用 clearCredentialState() 方法。出于安全考虑,请在用户每次退出登录时删除密钥,以免用户在下次启动应用时在不知情的情况下重新登录。

只有在以下情况下,恢复密钥才会被移除:

  • 系统级操作:用户卸载应用或清除其数据。
  • 应用级调用:在应用代码中处理用户退出操作时,通过调用 clearCredentialState() 以编程方式删除密钥。

由于默认 ClearCredentialStateRequest() 仅会清除标准会话状态,而不会清除恢复密钥,因此请在退出账号时执行双重清除:

  1. 调用 credentialManager.clearCredentialState(ClearCredentialStateRequest()) 以清除活跃的凭据提供程序会话状态。
  2. 使用 ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) 调用 credentialManager.clearCredentialState(),以删除存储的 RestoreCredential。
  3. 从本地存储空间中删除本地 has_synced_restore_credential 标记。
// Create a ClearCredentialStateRequest object
val clearRequest = ClearCredentialStateRequest(TYPE_CLEAR_RESTORE_CREDENTIAL)

// When the user logs out, delete the restore key
val response = credentialManager.clearCredentialState(clearRequest)

后续步骤

  • 测试恢复凭证:使用 Android Studio 或 adb shell bmgr 测试云端和设备到设备 (D2D) 恢复流程。
  • 自动化备份和恢复测试:使用 BackupRestoreController 编写 JUnit 5 主机测试,以验证 CI 中的 CLOUD_ENCRYPTED 和 DEVICE_TO_DEVICE 恢复。