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

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

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

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

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

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

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

تعمل واجهة برمجة التطبيقات لنقل بيانات الاعتماد على الأجهزة التي تعمل بالإصدار 8 من نظام التشغيل Android (المستوى 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 المحدّد للمعالجة. عليك تعريف هذا النشاط باستخدام إجراء النية ومخطط معرّف الموارد المنتظم للمحتوى الإلزاميَين.

للحماية من عمليات الاستدعاء غير المصرّح بها وهجمات "النائب المرتبك"، عليك تعريف android:requireContentUriPermissionFromCaller="write" على الإصدار 14 من نظام التشغيل Android (المستوى 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 مرة أخرى إلى معرّف موارد منتظم للمحتوى المقدَّم.

مصادقة الطلب

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

  1. التحقّق من قيمة credId العشوائية للجلسة (الدفاع الأساسي): عند تسجيل ExportEntry، أنشئ قيمة id (رقم خاص) مشفّرة وعالية العشوائية، واربطها بجلسة قصيرة الأمد. في نشاطك، تأكَّد من أنّ قيمة 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) المخصّصة (متقدّمة)

بشكلٍ تلقائي، يؤدي طلب RegisterExportRequest.create(context, entries) إلى تجميع credential_transfer_matcher.wasm التلقائي من النظام من مواد العرض في المكتبة، ما يؤدي إلى فلترة الإدخالات استنادًا إلى تقاطع supportedCredentialTypes فقط.

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

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