העברת פרטי כניסה

ממשקי ה-API של העברת פרטי כניסה ב-Credential Manager מאפשרים העברה מאובטחת של פרטי כניסה של משתמשים בין ספקי פרטי כניסה באותו מכשיר. במדריך הזה מוסבר איך ספקי פרטי כניסה ב-Android יכולים להשתלב עם ממשקי ה-API שמסופקים על ידי ספריית androidx.credentials:providerevents. התכונה הזו תומכת בסיסמאות, במפתחות גישה, בפרטי כתובת ובשדות בהתאמה אישית באמצעות פורמט חילופי האישורים (CXF) של FIDO.

מושגי ליבה

מסגרת העברת פרטי הכניסה מאפשרת העברה של פרטי כניסה בין אפליקציות באותו מכשיר, בלי לחשוף את פרטי הכניסה הגולמיים ל-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 של התוכן שהן חובה.

כדי להגן מפני הפעלות לא מורשות ומתקפות מסוג confused-deputy, צריך להצהיר על 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>

טיפול בכוונת ההעברה בפעילות

ב-CredentialExportActivity, משתמשים ב-IntentHandler.retrieveProviderImportCredentialsRequest(intent) כדי לנתח את הבקשה, לאמת את רכיב הקריאה ואת המטא-נתונים של הבקשה, לבצע אימות ביומטרי שנדרש ולכתוב את מטען ה-FIDO CXF בחזרה ל-URI של התוכן שסופק.

אימות הבקשה

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

  1. אימות של credId nonce של הסשן (הגנה ראשונית): כשרושמים את ExportEntry, יוצרים id (nonce) אקראי קריפטוגרפית עם אנטרופיה גבוהה ומשייכים אותו לסשן לטווח קצר. בפעילות שלכם, מוודאים שהערך request.credId תואם לערך nonce של סשן פעיל שלא פג תוקפו, שהונפק בעבר על ידי הספק. אפליקציות של צד שלישי לא יכולות לנחש את ה-nonce הזה, ולכן זה מבטיח שהכוונה תתאים לבחירה אותנטית של המשתמש בכלי לבחירת המערכת.
  2. אימות Activity.getCallingPackage() (בדיקת המתקשר): מכיוון שכלי בחירת אמצעי הזיהוי מפעיל את הפעילות באמצעות startActivityForResult, הפעילות יכולה לבדוק את getCallingPackage() כדי לוודא שהמתקשר המיידי שמפעיל את ממשק המשתמש הוא מסגרת כלי בחירת אמצעי הזיהוי המהימנה של המערכת (במכשירי Android עם Google Play Services,‏ "com.google.android.gms"). צריך לדחות את הבקשה אם חבילת ההתקנה של המתקשר לא מזוהה.
  3. בדיקת 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.

הטמעה של הכלי לייבוא

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

סוגי פרטי הכניסה והתוספים הנתמכים

אובייקט 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" תעודות זהות, מספרי SSN, מספרי TIN או מספרי דרכון.
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" סודות של סיסמה חד-פעמית מבוססת-זמן (2FA).
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)