הצגת פרטי הכניסה באמצעות אפליקציית הארנק

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

הוספת יחסי תלות

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

dependencies {
    // Used to implement credential registries
    implementation "androidx.credentials.registry:registry-digitalcredentials-mdoc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-openid:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-digitalcredentials-sdjwtvc:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider:1.0.0-alpha05"
    implementation "androidx.credentials.registry:registry-provider-play-services:1.0.0-alpha05"
}

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

קודם כל, יוצרים מכונה של RegistryManager שבה תשתמשו כדי לרשום בקשת OpenId4VpRegistry.

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

יצירת בקשת OpenId4VpRegistry

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

הוספת 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
}

הוספת מסמכי mdoc למרשם

ממפים את פרטי הכניסה המקומיים של 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.
)

אופציונלי: הנפקה מוטמעת

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

val openidRegistryRequest = OpenId4VpRegistry(
    credentialEntries = credentialEntries,
    inlineIssuanceEntries = emptyList(),
                listOf(
                MdocInlineIssuanceEntry(
                    id = "Issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedMdocs = setOf(
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "eu.europa.ec.eudi.pid.1"
                        ),
                        MdocInlineIssuanceEntry.SupportedMdoc(
                        "org.iso.18013.5.1.mDL1"
                        ),
                    )
                ),
                SdJwtInlineIssuanceEntry(
                    id = "sd-jwt-issuance",
                    display = InlineIssuanceEntry.InlineIssuanceDisplayProperties(
                        subtitle = "Mobile Drivers License, State Id, and Others",
                    ),
                    supportedSdJwts = setOf(
                        SdJwtInlineIssuanceEntry.SupportedSdJwt("urn:openid:interop:id:1")
                    )
                )
            ),
    id = "my-wallet-openid-registry-v1"
)

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

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

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

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

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

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

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

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

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

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

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

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

אפשר לעבד את בקשת ההצגה באמצעות הקוד הבא:

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

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

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

קודם כל, מחלצים את ProviderGetCredentialRequest מהכוונה:

val request = PendingIntentHandler.retrieveProviderGetCredentialRequest(intent)

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

val origin = request?.callingAppInfo?.getOrigin(
    privilegedAppsJson // Your allow list JSON
)

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

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

val appSigningInfo =
    request?.callingAppInfo?.signingInfoCompat?.signingCertificateHistory?.get(0)?.toByteArray()
val md = MessageDigest.getInstance("SHA-256")
val certHash =
    Base64.encodeToString(md.digest(appSigningInfo), Base64.NO_WRAP or Base64.NO_PADDING)
val computedOrigin = "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()

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