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