ממשקי ה-API להעברת פרטי כניסה של Credential Manager מאפשרים להעביר באופן מאובטח את פרטי הכניסה של המשתמשים בין ספקי פרטי כניסה באותו מכשיר. במדריך הזה מוסבר איך ספקי פרטי כניסה ב-Android יכולים לשלב את ממשקי ה-API שספריית androidx.credentials:providerevents מספקת. התכונה הזו תומכת בסיסמאות, במפתחות גישה, בפרטי כתובות ובשדות בהתאמה אישית באמצעות פורמט FIDO Credential Exchange Format (CXF) הסטנדרטי.
מושגי ליבה
מסגרת העברת פרטי הכניסה מאפשרת העברת פרטי כניסה בין אפליקציות באותו מכשיר בלי לחשוף את פרטי הכניסה הגולמיים ל-Android OS או לאפליקציות לא מאומתות.
המסגרת מגדירה שני תפקידים עיקריים:
- מייצא (ספק המקור): ספק פרטי כניסה שבו שמורים כרגע פרטי הכניסה של המשתמש. הוא רושם מראש במערכת מטא-נתונים על חשבונות זמינים שאפשר לייצא (
ExportEntry) ומגיב לבקשות העברה כשהמשתמש בוחר בכך. - מייבא (ספק לקוח או אשף הגדרה): ספק פרטי כניסה או אשף הגדרה שיוזם בקשת ייבוא (
ImportCredentialsRequest) ומציין אילו סוגים של פרטי כניסה ותוספים הוא יכול לקבל.
תאימות לגרסת Android
Credentials Transfer API פועל במכשירים עם Android מגרסה 8 (רמת API 26) ואילך.
הוספת יחסי תלות
מוסיפים את התלות ב-androidx.credentials:providerevents לקובץ build.gradle או build.gradle.kts של המודול:
dependencies {
implementation("androidx.credentials:providerevents:1.0.0-alpha06")
}
יוצרים מופע של המחלקה הנדרשת
יוצרים מופע של ProviderEventsManager הנדרש.
val providerEventsManager = ProviderEventsManager.create(context)
הטמעת הייצוא
כדי לאפשר למשתמשים לייצא את פרטי הכניסה מהאפליקציה שלכם לספקי פרטי כניסה אחרים במכשיר, אתם צריכים להטמיע את תפקיד הייצוא.
רישום רשומות ייצוא
כשספק פרטי הכניסה משתנה (לדוגמה, משתמש נכנס, מוסיף פרטי כניסה או משנה חשבונות), צריך לרשום או לעדכן את הפריטים של ExportEntry באמצעות ProviderEventsManager.registerExport().
כל ExportEntry דורש:
-
id: מזהה מחרוזת סודי שנוצר באופן אקראי ומייצג באופן ייחודי את רשומת הייצוא הזו. אתם צריכים לשמור את המזהה הזה באופן מאובטח, כי תצטרכו אותו בהמשך כדי לאמת בקשות העברה נכנסות. -
accountDisplayName: תווית חשבון אופציונלית (לדוגמה,"Personal Account"). -
userDisplayName: המזהה הראשי של המשתמש (לדוגמה,"alice@example.com"). -
icon: סמלBitmapשמייצג את הספק או החשבון (הספרייה משנה את הגודל שלו באופן אוטומטי ל-PNG בגודל 32x32). -
supportedCredentialTypes: קבוצה של קבועי מחרוזת מ-CredentialTypesשמייצגים את הסוגים שהרשומה הזו מכילה.
suspend fun registerMyProviderForExport( providerEventsManager: ProviderEventsManager, providerIcon: Bitmap, // Randomly generated and stored in encrypted storage secretEntryId: String ) { val entry = ExportEntry( id = secretEntryId, accountDisplayName = "MyProvider Personal", userDisplayName = "alice@example.com", icon = providerIcon, supportedCredentialTypes = setOf( CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, // Passwords CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, // Passkeys CredentialTypes.CREDENTIAL_TYPE_ADDRESS, CredentialTypes.CREDENTIAL_TYPE_CREDIT_CARD ) ) // RegisterExportRequest.create() attaches the default WASM matcher from assets val request = RegisterExportRequest.create(context, listOf(entry)) try { val response = providerEventsManager.registerExport(request) // Registration successful } catch (e: Exception) { // Handle registration exceptions (e.g., RegisterExportProviderConfigurationException) } }
הצהרה על פעילות הייצוא בקובץ המניפסט
כשהמשתמש בוחר את ExportEntry בממשק המשתמש של בורר המערכת, המערכת מפעילה את Activity הטיפול הייעודי. מצהירים על הפעילות הזו באמצעות פעולת ה-Intent החובה וסכמת ה-URI של התוכן.
כדי להגן מפני הפעלות לא מורשות ומתקפות של הרשאות יתר, צריך להצהיר על android:requireContentUriPermissionFromCaller="write" ב-Android מגרסה 14 (רמת API 34) ואילך. כך מערכת ההפעלה Android מאמתת שלגורם שמפעיל את ההפעלה יש הרשאות כתיבה ל-URI של תוכן היעד לפני שהיא מאפשרת לפעילות להתחיל, כמו בדוגמה הבאה:
<activity
android:name="com.example.CredentialExportActivity"
android:exported="true"
android:requireContentUriPermissionFromCaller="write"
android:label="@string/export_activity_label">
<intent-filter>
<!-- This intent action is required for Credential Manager to invoke this activity -->
<action android:name="androidx.identitycredentials.action.IMPORT_CREDENTIALS" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
</intent-filter>
</activity>
טיפול ב-Intent של ההעברה בפעילות שלכם
ב-CredentialExportActivity, משתמשים ב-IntentHandler.retrieveProviderImportCredentialsRequest(intent) כדי לנתח את הבקשה, לאמת את הרכיב ששולח את הקריאה ואת המטא-נתונים של הבקשה, לבצע את האימות הביומטרי הנדרש ולכתוב את המטען הייעודי (payload) של FIDO CXF בחזרה ל-URI של התוכן שסופק.
אימות הבקשה
מכיוון שהפעילות שלכם מיוצאת, חשוב להגן על תהליך הייצוא של פרטי הכניסה על ידי אימות של כל אחת מהשכבות הבאות:
- אימות ה-nonce של הסשן ב-
credId(הגנה ראשית): כשרושמים את ה-ExportEntry, יוצריםid(nonce) אקראי מבחינה קריפטוגרפית עם אנטרופיה גבוהה, ומשייכים אותו לסשן קצר. בפעילות, מוודאים שהערך שלrequest.credIdתואם ל-nonce של סשן פעיל שלא פג תוקפו, שהונפק בעבר על ידי הספק. אפליקציות של צד שלישי לא יכולות לנחש את המספר החד-פעמי הזה, ולכן הוא מבטיח שה-Intent תואם לבחירה אותנטית של המשתמש בבורר המערכת. - אימות
Activity.getCallingPackage()(בדיקת המתקשר): מכיוון שבורר פרטי הכניסה מפעיל את הפעילות באמצעותstartActivityForResult, הפעילות יכולה לבדוק אתgetCallingPackage()כדי לוודא שהמתקשר המיידי שמפעיל את ממשק המשתמש הוא מסגרת בורר פרטי הכניסה המהימנה של המערכת (במכשירי Android עם Google Play Services,"com.google.android.gms"). אם החבילה המתקשרת לא מזוהה, צריך לדחות את הבקשה. - בדיקת
request.callingAppInfo(פרטי אפליקציית היעד): השדהcallingAppInfoמזהה את אפליקציית היעד שתקבל את פרטי הכניסה המיובאים. אתם יכולים להשתמש במידע הזה כדי להציג את שם האפליקציה והסמל שלה בממשק המשתמש לאישור, או כדי לאמת אם אפליקציית היעד מהימנה.
class CredentialExportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 1. Extract the transfer request from the incoming Intent val request: ProviderImportCredentialsRequest? = IntentHandler.retrieveProviderImportCredentialsRequest(intent) if (request == null) { finishWithError() return } // 2. Validate CallingAppInfo and secret `credId` val callingAppPackage = request.callingAppInfo.packageName val receivedCredId = request.credId if (!verifySecretEntryId(receivedCredId) || !isTrustedImporter(callingAppPackage)) { // Secret ID mismatch or untrusted caller -> abort sendExceptionAndFinish(ImportCredentialsNoExportOptionException("Unauthorized request")) return } // 3. Optional: Prompt user for Biometric / PIN authentication before exporting authenticateUserThenExport(request) } private fun authenticateUserThenExport(request: ProviderImportCredentialsRequest) { // ... Biometric prompt logic ... // Once authenticated, generate the FIDO CXF JSON string matching the requested types val cxfJsonPayload = buildFidoCxfJsonPayload( requestedTypes = request.request.credentialTypes, requestedExtensions = request.request.knownExtensions ) val response = ImportCredentialsResponse(cxfJsonPayload) // 4. Write the JSON payload to the Content URI and set Activity result IntentHandler.setImportCredentialsResponse( context = this, uri = request.uri, intent = intent, response = response ) setResult(Activity.RESULT_OK, intent) finish() } private fun sendExceptionAndFinish(exception: androidx.credentials.providerevents.exception.ImportCredentialsException) { IntentHandler.setImportCredentialsException(intent, exception) setResult(Activity.RESULT_OK, intent) finish() } private fun verifySecretEntryId(credentialId: String): Boolean { // Check if credentialId matches what you stored when calling RegisterExportRequest return credentialId == getStoredSecretEntryId() } private fun isTrustedImporter(packageName: String): Boolean { // Implement any specific allowlisting / caller checks if required return true } private fun finishWithError() { setResult(Activity.RESULT_CANCELED) finish() } }
רישום הייצוא ואישור חריגות
כל החריגים במודול הזה הם מחלקות משנה של ImportCredentialsException, RegisterExportException או ClearExportException.
-
RegisterExportProviderConfigurationExceptionאוClearExportProviderConfigurationException: נזרקת כשהרישום או הניקוי נכשלים בגלל בעיות בהגדרת הספק (לדוגמה,supportedCredentialTypesריק ב-ExportEntry, או קריאה לרמת מערכת הפעלה לא נתמכת). -
RegisterExportUnknownErrorExceptionאוClearExportUnknownErrorException: שגיאה לא מסווגת במערכת או באחסון בזמן עדכון הרישום של הייצוא.
הטמעת הייבוא
כדי לייבא פרטי כניסה לאפליקציה (לדוגמה, במהלך תהליך ההצטרפות או ייבוא מספק), מטמיעים את תפקיד הייבוא ומתחילים את התהליך באמצעות קריאה ל-ProviderEventsManager.importCredentials().
יצירת בקשת הייבוא והפעלת התהליך
מציינים באילו CredentialTypes ו-KnownExtensions כלי הייבוא תומך:
suspend fun startCredentialImport( activityContext: Context, providerEventsManager: ProviderEventsManager ) { val importRequest = ImportCredentialsRequest( credentialTypes = setOf( CredentialTypes.CREDENTIAL_TYPE_BASIC_AUTH, CredentialTypes.CREDENTIAL_TYPE_PUBLIC_KEY, CredentialTypes.CREDENTIAL_TYPE_ADDRESS, CredentialTypes.CREDENTIAL_TYPE_NOTE ), knownExtensions = setOf( KnownExtensions.KNOWN_EXTENSION_SHARED ) ) try { // Launches the system Selector UI; suspends until user selects a provider and completes transfer val response = providerEventsManager.importCredentials(activityContext, importRequest) // 1. Inspect the source exporter's package info val exporterPackageName = response.callingAppInfo.packageName // 2. Parse the FIDO CXF JSON string val cxfJsonString = response.response.responseJson parseAndSaveImportedCredentials(cxfJsonString) } catch (e: ImportCredentialsException) { // Handle specific import exceptions (e.g., ImportCredentialsCancellationException) handleImportFailure(e) } } private fun parseAndSaveImportedCredentials(cxfJsonString: String) { val rootJson = JSONObject(cxfJsonString) // Parse according to FIDO Credential Exchange Format (CXF v1.0) specification: // https://fidoalliance.org/specs/cx/cxf-v1.0-ps-20250814.html } // Helper function to make it compile private fun handleImportFailure(e: ImportCredentialsException) {}
חריגים בתהליך הייבוא
כל החריגים במודול הזה הם מחלקות משנה של ImportCredentialsException, RegisterExportException או ClearExportException.
-
ImportCredentialsCancellationException: המשתמש סגר את ממשק המשתמש של הבורר או ביטל את פעילות הייצוא (Activity.RESULT_CANCELED). -
ImportCredentialsNoExportOptionException: לא נמצאו רשומות ייצוא רשומות שתואמות לסוגי פרטי הכניסה המבוקשים, או שהמייצא שנבחר זרק חריגה של דחייה. -
ImportCredentialsProviderConfigurationException: שגיאה בהגדרה (לדוגמה,credentialTypesריק שמוגדר ב-ImportCredentialsRequestאו הרשאות חסרות). -
ImportCredentialsInvalidJsonException: המייצא החזיר מטען ייעודי (payload) של JSON ריק או בפורמט שגוי, שנכשל באימות הבקשה. -
ImportCredentialsSystemErrorException: אירעה שגיאה פנימית במערכת Android או שגיאה בהעברת Binder במהלך הייבוא. -
ImportCredentialsUnknownCallerException: לא ניתן לאמת את האפליקציה שמבצעת את הקריאה באמצעות המסגרת. -
ImportCredentialsUnknownErrorException: שגיאה לא מסווגת או לא צפויה במהלך תהליך הייבוא.
סוגי פרטי הכניסה והתוספים הנתמכים
האובייקט androidx.credentials.providerevents.transfer.CredentialTypes מגדיר קבועים של מחרוזות סטנדרטיות שתואמים לסוגי הפריטים של FIDO CXF:
| קבוע | ערך (סוג cxf) |
תיאור |
|---|---|---|
CREDENTIAL_TYPE_BASIC_AUTH |
"basic-auth" |
פרטי כניסה של שם משתמש וסיסמה. |
CREDENTIAL_TYPE_PUBLIC_KEY |
"passkey" |
פרטי כניסה של מפתח ציבורי למפתח גישה מסוג FIDO2 או WebAuthn. |
CREDENTIAL_TYPE_ADDRESS |
"address" |
פרטי הכתובת למשלוח דואר או חבילות למילוי אוטומטי של טפסים. |
CREDENTIAL_TYPE_API_KEY |
"api-key" |
מפתחות גישה וטוקנים ל-API. |
CREDENTIAL_TYPE_CREDIT_CARD |
"credit-card" |
פרטי תשלום בכרטיסי אשראי וכרטיסי חיוב מיידי. |
CREDENTIAL_TYPE_CUSTOM_FIELDS |
"custom-fields" |
קבוצות בהתאמה אישית או שדות שמוגדרים על ידי המשתמש. |
CREDENTIAL_TYPE_DRIVERS_LICENSE |
"drivers-license" |
פרטי רישיון הנהיגה. |
CREDENTIAL_TYPE_FILE |
"file" |
הפניות למטא-נתונים ולפלייסהולדרים של קבצים בינאריים. |
CREDENTIAL_TYPE_GENERATED_PASSWORD |
"generated-password" |
סיסמאות מאובטחות שנוצרות על ידי מכונה. |
CREDENTIAL_TYPE_IDENTITY_DOCUMENT |
"identity-document" |
תעודות זהות, מספר תעודת זהות, מספר זיהוי לצורכי מס או הפניות לדרכון. |
CREDENTIAL_TYPE_ITEM_REFERENCE |
"item-reference" |
קישורים לוגיים שמפנים לפריט אחר במטען הייעודי (payload). |
CREDENTIAL_TYPE_NOTE |
"note" |
הערות מאובטחות שמוגדרות על ידי המשתמש (מחרוזת UTF-8). |
CREDENTIAL_TYPE_PASSPORT |
"passport" |
פרטי מסמך הנסיעה בדרכון. |
CREDENTIAL_TYPE_PERSON_NAME |
"person-name" |
פרטי הזהות והשם של האדם. |
CREDENTIAL_TYPE_SSH_KEY |
"ssh-key" |
זוגות מפתחות ציבוריים ופרטיים של SSH. |
CREDENTIAL_TYPE_TOTP |
"totp" |
סודות של סיסמה חד-פעמית מבוססת-זמן (אימות דו-שלבי). |
CREDENTIAL_TYPE_WIFI |
"wifi" |
מזהי SSID וביטויי סיסמה של רשתות Wi-Fi. |
התאמות WASM (WebAssembly) בהתאמה אישית (מתקדם)
כברירת מחדל, קריאה ל-RegisterExportRequest.create(context, entries) כוללת את credential_transfer_matcher.wasm שמוגדר כברירת מחדל במערכת מנכסי הספרייה, שמסנן רשומות רק על סמך החיתוך של supportedCredentialTypes.
אם ספק פרטי הכניסה שלכם דורש לוגיקה מורכבת של התאמה (לדוגמה, בדיקות יכולת דינמיות או סינון מותנה על סמך שדות בהתאמה אישית), אתם יכולים לקמפל מודול WebAssembly משלכם שתואם ל-API של העברת פרטי הכניסה של WASM, ולהעביר את מערך הבייטים הגולמיים ישירות:
// Loading a custom WASM matcher val customMatcherBytes = context.assets.open("my_custom_matcher.wasm").use { it.readBytes() } val customRequest = RegisterExportRequest( entries = myEntries, exportMatcher = customMatcherBytes ) providerEventsManager.registerExport(customRequest)