نقل بيانات الاعتماد

تتيح واجهات برمجة التطبيقات Credentials Transfer في Credential Manager نقل بيانات اعتماد المستخدمين بشكل آمن بين مقدّمي بيانات الاعتماد على الجهاز نفسه. يوضّح هذا الدليل بالتفصيل كيفية دمج مزوّدي بيانات الاعتماد على Android مع واجهات برمجة التطبيقات التي توفّرها مكتبة androidx.credentials:providerevents. تتيح هذه الميزة نقل كلمات المرور ومفاتيح المرور ومعلومات العناوين والحقول المخصّصة باستخدام تنسيق تبادل بيانات الاعتماد (CXF) المتوافق مع معايير FIDO.

المفاهيم الأساسية

تسهّل عملية نقل بيانات الاعتماد نقل بيانات الاعتماد من جهاز إلى آخر على الجهاز نفسه بدون الكشف عن بيانات الاعتماد الأولية لنظام التشغيل Android أو التطبيقات التي لم تتم مصادقتها.

يحدّد إطار العمل دورَين أساسيَّين:

  • المصدِّر (مقدّم الخدمة المصدر): هو مقدّم بيانات الاعتماد الذي يحتفظ حاليًا ببيانات اعتماد المستخدمين. وتسجّل هذه الخدمة مسبقًا البيانات الوصفية حول الحسابات المتاحة القابلة للتصدير (ExportEntry) في النظام وتستجيب لطلبات النقل عندما يختارها المستخدم.
  • المستورِد (موفّر العميل أو معالج الإعداد): هو موفّر بيانات اعتماد أو معالج إعداد يبدأ طلب استيراد (ImportCredentialsRequest) يحدّد أنواع بيانات الاعتماد والإضافات التي يمكنه تلقّيها.

التوافق مع إصدارات Android

تعمل واجهة Credentials Transfer API على الأجهزة التي تعمل بنظام التشغيل Android 8 (المستوى 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 يمثّل مقدّم الخدمة أو الحساب (يتم تغيير حجمه تلقائيًا إلى 32x32 بتنسيق PNG من خلال المكتبة).
  • ‫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 (مستوى واجهة برمجة التطبيقات 34) والإصدارات الأحدث. يؤدي ذلك إلى توجيه نظام التشغيل Android للتحقّق من أنّ المتصل الذي يبدأ عملية التشغيل لديه أذونات تعديل لمعرّف الموارد المنتظم للمحتوى المقصود قبل السماح ببدء النشاط، كما هو موضّح في المثال التالي:

<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 في معرّف الموارد المنتظم للمحتوى المقدَّم.

مصادقة الطلب

بما أنّه يتم تصدير نشاطك، يمكنك حماية عملية تصدير بيانات الاعتماد من خلال التحقّق من كل من الطبقات التالية:

  1. التحقّق من قيمة الـ nonce الخاصة بجلسة credId (الدفاع الأساسي): عند تسجيل ExportEntry، أنشِئ قيمة id (nonce) عشوائية مشفّرة وذات إنتروبيا عالية واربطها بجلسة قصيرة الأجل. في نشاطك، تأكَّد من أنّ request.credId يتطابق مع رقم عشوائي نشط وغير منتهي الصلاحية للجلسة سبق أن أصدره مقدّم الخدمة. بما أنّ التطبيقات الخارجية لا يمكنها تخمين هذا الرقم الخاص، يضمن ذلك أنّ الطلب يتوافق مع اختيار مستخدم حقيقي في أداة اختيار النظام.
  2. التحقّق من Activity.getCallingPackage() (التحقّق من المتصل): بما أنّ أداة اختيار بيانات الاعتماد تطلق نشاطك باستخدام startActivityForResult، يمكن لنشاطك فحص getCallingPackage() للتأكّد من أنّ المتصل الفوري الذي يطلق واجهة المستخدم هو إطار عمل أداة اختيار بيانات الاعتماد الموثوق به في النظام (على أجهزة Android التي تتضمّن خدمات Google Play، "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" مفاتيح ورموز الوصول إلى واجهة برمجة التطبيقات
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" روابط منطقية تشير إلى عنصر آخر في الحمولة
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) المخصّصة (متقدّمة)

By default, calling RegisterExportRequest.create(context, entries) bundles the system-default credential_transfer_matcher.wasm from library assets, which filters entries purely based on intersection of supportedCredentialTypes.

إذا كان موفّر بيانات الاعتماد يتطلّب منطق مطابقة معقّدًا (على سبيل المثال، عمليات التحقّق من الإمكانات الديناميكية أو الفلترة المشروطة استنادًا إلى حقول مخصّصة)، يمكنك تجميع وحدة WebAssembly الخاصة بك التي تتطابق مع واجهة برمجة التطبيقات WASM credential transfer وتمرير مصفوفة البايت الأولية مباشرةً:

// 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)