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