הטמעה של אפליקציית המחזיק ב-Credential Manager

ה-API של Credential Manager Holder מאפשר לאפליקציית המחזיק (נקראת גם "ארנק") ב-Android לנהל ולהציג מסמכים דיגיטליים לאימות בפני בודקים.

תמונה שבה מוצג ממשק המשתמש של פרטי הכניסה הדיגיטליים בכלי לניהול פרטי כניסה
איור 1. ממשק המשתמש של בורר פרטי הכניסה הדיגיטליים.

מושגי ליבה

חשוב להכיר את המושגים הבאים לפני שמשתמשים ב-Holder API.

פורמטים של פרטי כניסה

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

  • סוג: הקטגוריה, למשל תואר או רישיון נהיגה בנייד.
  • מאפיינים: מאפיינים כמו שם פרטי ושם משפחה.
  • קידוד: הדרך שבה פרטי הכניסה בנויים, למשל SD-JWT או mdoc
  • תוקף: שיטה לאימות קריפטוגרפי של האותנטיות של פרטי הכניסה.

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

המאגר תומך בשני פורמטים:

יכול להיות שגורם מאמת ישלח בקשת OpenID4VP ל-SD-JWT ול-mDocs כשהוא משתמש ב-Credential Manager. הבחירה משתנה בהתאם לתרחיש השימוש ולתחום.

רישום מטא-נתונים של פרטי כניסה

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

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

תשתמשו במחלקה OpenId4VpRegistry כדי לרשום את פרטי הכניסה הדיגיטליים, כי היא תומכת בפורמטים של פרטי כניסה mdoc ו-SD-JWT. הגורמים המאמתים ישלחו בקשות OpenID4VP כדי לבקש את פרטי האישורים האלה.

רישום פרטי הכניסה של האפליקציה

כדי להשתמש ב-Credential Manager Holder API, מוסיפים את יחסי התלות הבאים לסקריפט ה-build של מודול האפליקציה:

Groovy

dependencies {
    // Use to implement credentials registrys

    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha04"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04"

}

Kotlin

dependencies {
    // Use to implement credentials registrys

    implementation("androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-digitalcredentials-preview:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider:1.0.0-alpha04")
    implementation("androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha04")

}

יצירת RegistryManager

יוצרים מופע RegistryManager ורושמים בו בקשת OpenId4VpRegistry.

// Create the registry manager
val registryManager = RegistryManager.create(context)

// The guide covers how to build this out later
val registryRequest = OpenId4VpRegistry(credentialEntries, id)

try {
    registryManager.registerCredentials(registryRequest)
} catch (e: Exception) {
    // Handle exceptions
}

יצירת בקשה של OpenId4VpRegistry

כמו שציינו קודם, תצטרכו לרשום OpenId4VpRegistry כדי לטפל בבקשת OpenID4VP ממאמת. אנחנו נניח שיש לכם כמה סוגים של נתונים מקומיים שנטענו עם פרטי הכניסה שלכם לארנק (לדוגמה, sdJwtsFromStorage). עכשיו תמירו אותם למקבילות של Jetpack DigitalCredentialEntry על סמך הפורמט שלהם – SdJwtEntry או MdocEntry ל-SD-JWT או ל-mdoc, בהתאמה.

הוספת Sd-JWT למאגר

ממפים כל פרטי כניסה מקומיים מסוג SD-JWT לSdJwtEntry עבור המרשם:

fun mapToSdJwtEntries(sdJwtsFromStorage: List<StoredSdJwtEntry>): List<SdJwtEntry> {
    val list = mutableListOf<SdJwtEntry>()

    for (sdJwt in sdJwtsFromStorage) {
        list.add(
            SdJwtEntry(
                verifiableCredentialType = sdJwt.getVCT(),
                claims = sdJwt.getClaimsList(),
                entryDisplayPropertySet = sdJwt.toDisplayProperties(),
                id = sdJwt.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

הוספת קובצי mdocs למרשם

ממפים את פרטי הכניסה המקומיים של mdoc לסוג Jetpack‏ MdocEntry:

fun mapToMdocEntries(mdocsFromStorage: List<StoredMdocEntry>): List<MdocEntry> {
    val list = mutableListOf<MdocEntry>()

    for (mdoc in mdocsFromStorage) {
        list.add(
            MdocEntry(
                docType = mdoc.retrieveDocType(),
                fields = mdoc.getFields(),
                entryDisplayPropertySet = mdoc.toDisplayProperties(),
                id = mdoc.getId() // Make sure this cannot be readily guessed
            )
        )
    }
    return list
}

מידע חשוב על הקוד

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

רישום פרטי הכניסה

משלבים את הערכים שהומרו ורושמים את הבקשה ב-RegistryManager:

val credentialEntries = mapToSdJwtEntries(sdJwtsFromStorage) + mapToMdocEntries(mdocsFromStorage)

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    id = "my-wallet-openid-registry-v1" // A stable, unique ID to identify your registry record.
)

עכשיו אפשר לרשום את פרטי הכניסה ב-CredentialManager.

try {
    val response = registryManager.registerCredentials(openidRegistryRequest)
} catch (e: Exception) {
    // Handle failure
}

פרטי הכניסה שלכם נרשמו עכשיו במנהל פרטי הכניסה.

ניהול מטא-נתונים של אפליקציות

למטא-נתונים שאפליקציית המחזיק רושמת ב-CredentialManager יש את המאפיינים הבאים:

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

אופציונלי: יצירת כלי להתאמת נתונים

מנגנון ההתאמה הוא קובץ בינארי של Wasm שמופעל על ידי Credential Manager בארגז חול כדי לסנן את פרטי הכניסה הרשומים בהתאם לבקשה נכנסת של מאמת.

  • התאמה לערך ברירת המחדל: כשיוצרים מופע של המחלקה OpenId4VpRegistry, היא כוללת אוטומטית את ההתאמה לערך ברירת המחדל OpenId4VP (OpenId4VpDefaults.DEFAULT_MATCHER). בכל תרחישי השימוש הרגילים של OpenID4VP, הספרייה מטפלת בהתאמה בשבילכם.
  • כלי התאמה מותאם אישית: כדאי להטמיע כלי התאמה מותאם אישית רק אם אתם תומכים בפרוטוקול לא סטנדרטי שנדרשת לו לוגיקת התאמה משלו.

טיפול בפרטי כניסה שנבחרו

כשמשתמש בוחר פרטי כניסה, אפליקציית המחזיק צריכה לטפל בבקשה. תצטרכו להגדיר פעילות שתאזין למסנן הכוונות androidx.credentials.registry.provider.action.GET_CREDENTIAL. בדוגמה של הארנק שלנו אפשר לראות איך התהליך הזה מתבצע.

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

  • אפליקציית השיחות: האפליקציה ששלחה את הבקשה, שאפשר לאחזר באמצעות getCallingAppInfo.
  • פרטי אמצעי הזיהוי שנבחר: מידע על המועמד שאותו המשתמש בחר, שאוחזר באמצעות selectedCredentialSet extension method. המידע הזה יהיה זהה למזהה אמצעי הזיהוי שרשמתם.
  • בקשות ספציפיות: הבקשה הספציפית שהגיש המאמת, שאוחזרה מהשיטה getCredentialOptions. בתהליך בקשה של אישורים דיגיטליים, אמור להופיע GetDigitalCredentialOption אחד ברשימה הזו.

בדרך כלל, הגורם המאמת שולח בקשה להצגת אישורים דיגיטליים, שאפשר לעבד באמצעות קוד לדוגמה הבא:

request.credentialOptions.forEach { option ->
    if (option is GetDigitalCredentialOption) {
        Log.i(TAG, "Got DC request: ${option.requestJson}")
        processRequest(option.requestJson)
    }
}

דוגמה לכך אפשר לראות בארנק לדוגמה.

בדיקת הזהות של המאמת

  1. תמצה את ProviderGetCredentialRequest מהכוונה:
val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)
  1. בדיקה של מקור עם הרשאות: אפליקציות עם הרשאות (כמו דפדפני אינטרנט) יכולות לבצע שיחות בשם מאמתים אחרים על ידי הגדרת פרמטר המקור. כדי לאחזר את המקור הזה, צריך להעביר רשימה של מתקשרים מהימנים עם הרשאות (רשימת היתרים בפורמט JSON) אל API של CallingAppInfo getOrigin().
val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

אם המקור לא ריק: המקור מוחזר אם packageName וטביעות האצבע של האישור שהתקבלו מ-signingInfo תואמות לאלה של אפליקציה שנמצאה ברשימת ההיתרים שהועברה אל getOrigin() API. אחרי שמקבלים את ערך המקור, אפליקציית הספק צריכה להתייחס לזה כאל קריאה עם הרשאות מיוחדות ולהגדיר את המקור הזה בתגובת OpenID4VP, במקום לחשב את המקור באמצעות החתימה של האפליקציה שקוראת.

מנהל הסיסמאות של Google משתמש ברשימת היתרים שזמינה לכולם כדי לבצע קריאות ל-getOrigin(). אם אתם ספק אישורים, אתם יכולים להשתמש ברשימה הזו או לספק רשימה משלכם בפורמט JSON שמתואר ב-API. הספק הוא זה שבוחר באיזו רשימה להשתמש. כדי לקבל גישת הרשאה באמצעות ספקי אישורים של צד שלישי, צריך לעיין במסמכים שסופקו על ידי הצד השלישי.

אם המקור ריק, בקשת האימות מגיעה מאפליקציית Android. המקור של האפליקציה שצריך להזין בתשובה של OpenID4VP צריך להיות android:apk-key-hash:<encoded SHA 256 fingerprint>.

val appSigningInfo = request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory[0]?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash = Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
return "android:apk-key-hash:$certHash"

רינדור ממשק המשתמש של המחזיק

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

  • אם נדרש אימות משתמש נוסף כדי לשחרר את פרטי הכניסה, צריך להשתמש ב-BiometricPrompt API. בדוגמה מוסבר איך עושים את זה.
  • אחרת, הרבה ארנקים בוחרים להחזיר את הנתונים בצורה שקטה על ידי עיבוד פעילות ריקה שמעבירה את הנתונים בחזרה לאפליקציה שקוראת לה. כך מצמצמים את מספר הקליקים של המשתמשים ומספקים חוויה חלקה יותר.

החזרת תגובת פרטי הכניסה

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

PendingIntentHandler.setGetCredentialResponse(
    resultData,
    GetCredentialResponse(DigitalCredential(response.responseJson))
)
setResult(RESULT_OK, resultData)
finish()

אם יש חריגה, אפשר לשלוח את החריגה מפרטי הכניסה באופן דומה:

PendingIntentHandler.setGetCredentialException(
    resultData,
    GetCredentialUnknownException() // Configure the proper exception
)
setResult(RESULT_OK, resultData)
finish()

בדוגמה המלאה של החזרת תגובת האישורים בהקשר, אפשר לעיין באפליקציה לדוגמה.