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

ממשקי ה-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 של התוכן שסופק.

אימות הבקשה

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

  1. אימות ה-nonce של הסשן ב-credId (הגנה ראשית): כשרושמים את ה-ExportEntry, יוצרים id (nonce) אקראי מבחינה קריפטוגרפית עם אנטרופיה גבוהה, ומשייכים אותו לסשן קצר. בפעילות, מוודאים שהערך של request.credId תואם ל-nonce של סשן פעיל שלא פג תוקפו, שהונפק בעבר על ידי הספק. אפליקציות של צד שלישי לא יכולות לנחש את המספר החד-פעמי הזה, ולכן הוא מבטיח שה-Intent תואם לבחירה אותנטית של המשתמש בבורר המערכת.
  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.

הטמעת הייבוא

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