הטמעה של שחזור פרטי כניסה באמצעות Credential Manager

בדף הזה מוסבר איך ליצור מפתח שחזור, להיכנס באמצעותו ולמחוק אותו.

תאימות בין גרסאות

התכונה "שחזור פרטי הכניסה" של Credential Manager פועלת במכשירים עם Android מגרסה 9 (רמת API‏ 28) ומעלה, גרסת ליבה 242200000 (גרסה 24.22) ומעלה של Google Play Services ‏(GMS), וגרסה 1.5.0 ומעלה של ספריית androidx.credentials. במכשירים שאין בהם את Google Play Services או שיש בהם גרסה ישנה יותר של GMS, צריך להוסיף את CreateCredentialProviderConfigurationException ו-GetCredentialProviderConfigurationException כדי שהאפליקציה תעבור בצורה חלקה לכניסה רגילה.

דרישות מוקדמות

מגדירים שרת של גורם מסתמך ב-WebAuthn, בדומה לשרת של מפתחות הגישה. אם כבר הגדרתם שרת לטיפול באימות באמצעות מפתחות גישה, אתם יכולים להשתמש באותן נקודות קצה (endpoint) של רישום וטענת נכוֹנוּת (assertion) ב-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"
}

התכונה "שחזור פרטי הכניסה" זמינה בגרסה 1.5.0 ואילך של הספרייה androidx.credentials. עם זאת, מומלץ להשתמש בגרסאות היציבות העדכניות ביותר של יחסי התלות, ככל האפשר.

איך android:allowBackup משפיע על שחזור פרטי הכניסה

המאפיין android:allowBackup במניפסט שולט בגיבוי של נתוני האפליקציה, שפועל עם "שחזור פרטי הכניסה" בשלוש דרכים שונות:

  • העברת מפתח השחזור (לא תלוי ב-allowBackup): Google Play Services מעבירים את זוג המפתחות של RestoreCredential באמצעות גיבוי בענן שמוצפן מקצה לקצה או העברה מקומית ממכשיר למכשיר (D2D), בלי קשר לשאלה אם android:allowBackup הוא true או false. בהעברות D2D, מפתח השחזור תמיד מועבר. המצב היחיד שבו הוא לא מועבר הוא במהלך גיבוי בענן, כשהמשתמש ביטל את ההסכמה לגיבוי בענן בהגדרות של Android.
  • שחזור ברקע ב-BackupAgent (נדרש android:allowBackup="true"): כניסה שקטה ברקע לפני שהמשתמש פותח את האפליקציה במכשיר החדש (BackupAgent.onRestoreFinished()) פועלת רק כש-android:allowBackup="true" ו-android:backupAgent מוגדרים במניפסט. אם 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. מחיקת מפתח השחזור ביציאה מהחשבון: כשהמשתמש יוצא מהחשבון או כשהשרת מבטל את תוקף הסשן, צריך להפעיל את credentialManager.clearCredentialState() עם ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) ולנקות את דגל הסנכרון המקומי.

יצירת מפתח שחזור

האפליקציה צריכה לכסות את כל המקרים שבהם משתמש נכנס לחשבון, כדי לוודא שנוצר מפתח שחזור למשתמשים פעילים. צריך ליצור את מפתח השחזור בתרחישים הבאים:

  • אם המשתמש מחובר לחשבון ועדיין לא נוצר מפתח שחזור (למשל ב-onCreate() ל-Activity הראשי).
  • כשהמשתמש נכנס לחשבון או משלים תהליך הרשמה לחשבון חדש.

כדי להימנע מקריאה ל-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 בשרת האפליקציה כדי לשלוח לאפליקציית הלקוח את קובץ ה-JSON של PublicKeyCredentialCreationOptions שנדרש ליצירת פרטי הכניסה לשחזור. מידע נוסף על ההטמעה בצד השרת זמין במאמר הנחיות לצד השרת.

ה-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 או מזהים של מספרים שלמים גולמיים גורמים לשגיאות CreateRestoreCredentialDomException או IllegalArgumentException.
  • ‫rp.id: מגדירים את הדומיין של הצד המסתמך (לדוגמה, example.com).
  • ‫pubKeyCredParams: כולל את {"type": "public-key", "alg": -7} (ES256) ואת {"type": "public-key", "alg": -257} (RS256).

יצירת מפתח השחזור

אחרי שמקבלים מהשרת את קובץ ה-JSON של האפשרויות ליצירת מפתח ציבורי, עוטפים את קובץ ה-JSON באובייקט CreateRestoreCredentialRequest וקוראים ל-createCredential() באובייקט CredentialManager.

// 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: הערך הזה שומר את המפתח באופן מקומי ולא בענן. המפתח לא זמין במכשיר החדש אם המשתמש בוחר לשחזר מהענן, אבל הוא עדיין מועבר במהלך הגדרה מקומית של מכשיר למכשיר (D2D) באמצעות כבל או Wi-Fi.

טיפול בתגובה ליצירת פרטי הכניסה

‫Credential Manager API מחזיר תשובה מסוג CreateRestoreCredentialResponse. התגובה הזו מכילה את התגובה לרישום פרטי הכניסה של המפתח הציבורי בפורמט JSON.

שולחים את response.registrationResponseJson מהאפליקציה לשרת של הצד המסתמך כדי לאמת את המפתח הציבורי ולשמור אותו לצד חשבון המשתמש, ואז מתעדים את סמן הסנכרון המקומי באחסון המקומי. מידע נוסף על אימות בצד השרת זמין בהנחיות למפתחות גישה.

במהלך תהליך היצירה של מפתח השחזור, צריך לטפל בחריגים הבאים:

  • ‫CreateRestoreCredentialDomException: החריגה הזו מתרחשת אם הערך של requestJson לא תקין ולא תואם לפורמט WebAuthn של PublicKeyCredentialCreationOptionsJSON.
  • ‫E2eeUnavailableException: החריגה הזו מתרחשת אם הערך של isCloudBackupEnabled הוא true, אבל במכשיר של המשתמש אין גיבוי נתונים או הצפנה מקצה לקצה, כמו נעילת מסך.
    כדי לוודא שפרטי הכניסה לשחזור נוצרים להעברות מקומיות ממכשיר למכשיר (D2D) גם כש-E2EE בענן לא זמין, צריך לתפוס את E2eeUnavailableException ולנסות שוב מיד את createCredential() עם CreateRestoreCredentialRequest(requestJson, isCloudBackupEnabled = false).
  • IllegalArgumentException: החריגה הזו מתרחשת אם requestJson ריק, לא בפורמט JSON תקין או לא מכיל user.id תקין בקידוד Base64URL שתואם למפרטים של WebAuthn.

כניסה באמצעות מפתח שחזור

משתמשים בתכונה "שחזור פרטי הכניסה" כדי להכניס את המשתמש לחשבון באופן אוטומטי במהלך תהליך הגדרת המכשיר.

קבלת אפשרויות לאחזור פרטי כניסה משרת האפליקציה

לפני שקוראים ל-getCredential() במכשיר החדש, צריך לבקש אתגר JSON של PublicKeyCredentialRequestOptions ב-WebAuthn משרת האפליקציה. מכיוון שהאפליקציה במכשיר החדש עדיין לא יודעת איזה משתמש נכנס, נקודת הקצה הזו חייבת להיות לא מאומתת (גילוי ללא משתמש). למידע נוסף על השרת, קראו את המאמר כניסה באמצעות מפתח גישה ואת המדריך לאימות בצד השרת.

ל-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 עבור טענות נכוֹנוּת (assertions) של מפתח השחזור.

קבלת מפתח השחזור

כדי לקבל את מפתח השחזור במכשיר החדש, צריך לעטוף את ה-requestJson של השרת ב-GetRestoreCredentialOption בתוך GetCredentialRequest ולקרוא ל-getCredential() באובייקט CredentialManager.

מטמיעים אסטרטגיית שחזור דו-שלבית כדי שהמשתמשים ייכנסו לחשבון גם אם שחזור נתוני האפליקציה ברקע פועל וגם אם לא:

  • רמה 1 (שחזור ברקע ב-BackupAgent.onRestoreFinished()): אם הגיבוי של נתוני האפליקציה מופעל (android:allowBackup="true"), צריך ליצור מחלקת משנה של BackupAgent ולבטל את onRestoreFinished() (לא להשתמש ב-onRestore(), שנקרא רק לגיבויים מדור קודם של ערכי מפתח). מכיוון ש-onRestoreFinished() היא פונקציה סינכרונית ו-getCredential() היא פונקציית suspend של Kotlin, צריך לגשר על הקריאה באמצעות runBlocking(Dispatchers.IO). תפיסה שקטה של NoCredentialException כשאין מפתח שחזור.
  • רמה 2 (חלופה בחזית במרכז האפליקציות Activity.onCreate()): בהפעלה הראשונה של האפליקציה כשאין סשן מקומי, צריך להפעיל את getCredential() מ-lifecycleScope לפני שמוצג מסך ההתחברות. הפקודה הזו מטפלת באפליקציות עם android:allowBackup="false" ומשחזרת את המצב אם המכשיר היה אופליין בזמן ש-onRestoreFinished() פעל.

אם מוסיפים BackupAgent חדש לאפליקציה שכבר משתמשת בגיבוי אוטומטי עם android:allowBackup="true", צריך גם להגדיר את android:fullBackupOnly="true" ברכיב <application> ב-AndroidManifest.xml כדי שמערכת 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 שלו הוא RestoreCredential (ולא PublicKeyCredential) שמכיל את authenticationResponseJson.

טיפול בתגובה לכניסה לחשבון

שולחים את restoreCredential.authenticationResponseJson מהאפליקציה לשרת של הצד המסתמך כדי לאמת את טענת הנכוֹנוּת (assertion) הקריפטוגרפית ולהנפיק טוקן סשן חדש למשתמש. לפרטים על אימות בצד השרת, ראו כניסה באמצעות מפתח גישה.

כשאתם מתכננים את האחסון בקצה העורפי ואת כללי מדיניות מחזור החיים של מפתחות השחזור, חשוב לפעול לפי ההנחיות הבאות:

  • הבחנה בין מפתחות שחזור לבין מפתחות גישה של משתמשים: תייגו את מפתחות השחזור במסד הנתונים שלכם כדי שהם יוסתרו ממסכי הניהול של מפתחות הגישה שגלויים למשתמשים, ויהיו פטורים מבדיקות אימות המשתמש (uv = true).
  • תמיכה בכמה מכשירים פעילים לכל משתמש: למשתמש יכולים להיות כמה מכשירים (כמו טלפון וטאבלט). כדאי לאפשר כמה מפתחות שחזור פעילים לכל חשבון משתמש (לדוגמה, מפתחות לפי מזהה מכשיר או מזהה התקנה) במקום להחליף שורה אחת.
  • הגדרת TTL מתאים וניקוי מפתחות מיותרים: הסרת אפליקציה או ניקוי נתוני האפליקציה מוחקים את מפתח השחזור המקומי בלי להודיע לשרת. שמרו את מפתחות השחזור למשך זמן ארוך מספיק כדי שישרדו את חלונות השדרוג של המכשיר (גם אם המשתמש יוצא מהחשבון במכשיר הישן אחרי ההעברה), וסלקו מפתחות לא פעילים על סמך זמן חיים (TTL) של חוסר פעילות או מגבלות מפתחות לכל משתמש.

מחיקת מפתח השחזור

‫Credential Manager הוא חסר מצב (stateless) ולא מודע לפעילות המשתמש, ולכן הוא לא מוחק באופן אוטומטי את מפתחות השחזור אחרי השימוש. כדי למחוק מפתח שחזור, צריך להפעיל את ה-method‏ clearCredentialState(). מטעמי אבטחה, מחקו את המפתח בכל פעם שמשתמש יוצא מהחשבון, כדי שהוא לא ייכנס שוב לחשבון באופן אוטומטי בפעם הבאה שהוא יפעיל את האפליקציה.

מפתחות שחזור מוסרים רק במצבים הבאים:

  • פעולות ברמת המערכת: המשתמשים מסירים את האפליקציה או מוחקים את הנתונים שלה.
  • קריאות ברמת האפליקציה: מוחקים את המפתח באופן פרוגרמטי על ידי קריאה ל-clearCredentialState() כשמטפלים ביציאה של המשתמש מהחשבון בקוד של האפליקציה.

מכיוון שברירת המחדל של ClearCredentialStateRequest() מנקה רק את מצב הסשן הרגיל ומשאירה את מפתח השחזור ללא שינוי, צריך לבצע ניקוי כפול ביציאה:

  1. צריך להפעיל את credentialManager.clearCredentialState(ClearCredentialStateRequest()) כדי לנקות את מצב הסשן הפעיל של ספק פרטי הכניסה.
  2. קוראים ל-credentialManager.clearCredentialState() עם ClearCredentialStateRequest(ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL) כדי למחוק את ה-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)

השלבים הבאים