版本相容性

Credential Manager 的還原憑證功能適用於搭載 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 影響。

總覽

還原憑證在使用者的新舊裝置上,會經歷三個階段的生命週期:

  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

使用應用程式的活動背景資訊,將 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()。

// requestJson contains the credential creation options sent by the server
val createRestoreRequest = CreateRestoreCredentialRequest(requestJson)
val response = try {
    credentialManager.createCredential(context, createRestoreRequest)
} catch (e: E2eeUnavailableException) {
    // Fall back to local device-to-device (D2D) restore if cloud E2EE is unavailable
    credentialManager.createCredential(
        context,
        CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false),
    )
}

程式碼重點

  • 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")。由於還原憑證會在背景執行,不會顯示生物辨識或螢幕鎖定 UI,因此 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() 執行時裝置離線的情況下復原。

如果將新的 BackupAgent 新增至已使用 android:allowBackup="true" 自動備份的應用程式,請一併在 AndroidManifest.xml 的 <application> 元素上設定 android:fullBackupOnly="true",這樣 Android 就會繼續使用自動備份,而不是切換至舊版鍵/值備份。

    suspend fun getRestoreKey(
        fetchAuthenticationJson: () -> String,
    ) {
        // Fetch the options required to get the restore key
        val authenticationJson = fetchAuthenticationJson()

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

        try {
            val response = credentialManager.getCredential(context, getRequest)

            // Type-check and extract the restore credential
            val credential = response.credential
            if (credential is RestoreCredential) {
                val responseJson = credential.authenticationResponseJson
            }
        } catch (e: NoCredentialException) {
            // No restore key is present on this device
        }
    }

class MyBackupAgent : BackupAgent() {

    override fun onRestoreFinished() {
        super.onRestoreFinished()
        val credentialManager = CredentialManager.create(this)
        val restoreFunctions = RestoreCredentialsFunctions(this, credentialManager)

        runBlocking(Dispatchers.IO) {
            restoreFunctions.getRestoreKey(
                fetchAuthenticationJson = {
                    /* Fetch challenge JSON from server */ ""
                }
            )
        }
    }
}

Credential Manager API 會傳回 GetCredentialResponse,其 credential 屬性是包含 authenticationResponseJson 的 RestoreCredential (而非 PublicKeyCredential)。

處理登入回應

將 restoreCredential.authenticationResponseJson 從應用程式傳送至依賴方伺服器,驗證密碼編譯斷言,並為使用者核發新的工作階段符記。如需伺服器端驗證的詳細資料,請參閱「使用密碼金鑰登入」。

設計還原金鑰的後端儲存空間和生命週期政策時,請遵循下列規範:

  • 區分還原金鑰和使用者密碼金鑰:在資料庫中標記還原金鑰,這樣就不會顯示在面向使用者的密碼金鑰管理畫面,也不需要進行使用者驗證 (uv = true) 檢查。
  • 支援每位使用者擁有多部使用中的裝置:使用者可以擁有多部裝置 (例如手機和平板電腦)。允許每個使用者帳戶有多個有效的還原金鑰 (例如依裝置或安裝 ID 鍵入),而不是覆寫單一資料列。
  • 設定適當的存留時間並清除孤立金鑰:解除安裝應用程式或清除應用程式資料會刪除本機還原金鑰,但不會通知伺服器。還原金鑰的保留時間應足以涵蓋裝置升級期間 (即使使用者在轉移後登出舊裝置),並根據閒置存留時間或每位使用者的金鑰數量上限,刪除過時的金鑰。

刪除還原金鑰

Credential Manager 是無狀態,且不會偵測使用者活動,因此不會在使用後自動刪除還原金鑰。如要刪除還原金鑰,請呼叫 clearCredentialState() 方法。基於安全考量,請在使用者登出時刪除金鑰,以免下次啟動應用程式時,系統在使用者不知情的情況下自動登入。

只有在下列情況下,系統才會移除還原金鑰:

  • 系統層級動作:使用者解除安裝應用程式或清除資料。
  • 應用程式層級呼叫:在應用程式程式碼中處理使用者登出時,呼叫 clearCredentialState(),以程式輔助方式刪除金鑰。

由於預設 ClearCredentialStateRequest() 只會清除標準工作階段狀態,並保留還原金鑰,因此請在登出時執行雙重清除:

  1. 呼叫 credentialManager.clearCredentialState(ClearCredentialStateRequest()),清除有效的憑證提供者工作階段狀態。
  2. 使用 ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) 呼叫 credentialManager.clearCredentialState(),刪除儲存的 RestoreCredential。
  3. 從本機儲存空間刪除本機 has_synced_restore_credential 標記。
// Clear active credential provider session state
credentialManager.clearCredentialState(ClearCredentialStateRequest())

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

後續步驟

  • 測試還原憑證:使用 Android Studio 或 adb shell bmgr 測試雲端和裝置對裝置 (D2D) 還原流程。
  • 自動備份和還原測試:使用 BackupRestoreController 編寫 JUnit 5 主機測試,在 CI 中驗證 CLOUD_ENCRYPTED 和 DEVICE_TO_DEVICE 還原。