كيفية بناء محاكي وقارئ البطاقة الذكية البسيط لنظام Android

مرحبا! أنا مهندس برامج أقدم @ TWG. في السابق ، عملت في Gemalto - أكبر شركة لتصنيع البطاقات الذكية في العالم.

المقدمة:

تحتوي البطاقات الذكية على الكثير من الوظائف المفيدة - من بطاقات الهوية ، إلى بطاقات المستودعات - وهي توفر أداة مرنة وآمنة ومحمولة لإدارة جميع أنواع البيانات.

الهدف من هذه المقالة هو التمكن من استخدام تقنية NFC الخاصة بهاتف Android لمحاكاة وقراءة البطاقة الذكية ، ولكن قبل ذلك ، من المهم للغاية فهم كيفية عمل البطاقات الذكية والتواصل مع القراء.

ماذا تحتاج:

  • المعرفة الأساسية لتطوير أندرويد
  • المعرفة الأساسية Kotlin ، كما سنفعل الأمثلة في Kotlin
  • هاتف Android A مزود بـ NFC والذي سيكون بمثابة محاكي البطاقات لاختباراتنا
  • هاتف Android B مع NFC والذي سيكون بمثابة قارئ بطاقات
  • اختياري: ePassport متوافق مع ICAO. يمكنك التحقق مما إذا كان جواز سفرك متوافقًا مع منظمة الطيران المدني الدولي ويحتوي على شريحة إلكترونية من خلال النظر إلى المقدمة ورؤية هذه العلامة:
رقاقة تسجيل في الجزء الأمامي من ePassports

بطاقات ذكية:

تعتبر البطاقات الذكية ، على كل الأغراض والأغراض ، أجهزة كمبيوتر مصغرة تحتوي على معالجات صغيرة وذاكرة. تمامًا مثل الكمبيوتر العادي ، يمكن أن يكون لديهم نظام تشغيل وتطبيقات تعمل (تسمى Applets) قادرة على القيام بعمليات معقدة بالإضافة إلى توفير وصول آمن إلى البيانات.

أنواع:

يمكن أن تكون البطاقات الذكية "جهة اتصال" أو "جهة اتصال" أو كليهما (واجهة مزدوجة). يجب أن تكون بطاقات الاتصال على اتصال مباشر مع القارئ حتى تكون جاهزة وجاهزة للتواصل. ومع ذلك ، يمكن التواصل مع البطاقات اللاصقة باستخدام موجات راديو 13.56 ميغاهيرتز من مسافة قصوى تبلغ 10 سم (4 بوصات). أنها تحتوي على هوائي يتيح هذا النوع من الاتصالات:

البطاقة الذكية ثنائية الواجهة

يحتوي كل هاتف على الأقل على قارئ بطاقات Smart Contact ، يستخدم لقراءة بطاقة SIM. تحتوي معظم هواتف Android على قارئ بطاقات ذكية بدون تماس في شكل قارئ NFC ، وسنقدم أمثلة على كيفية الاستخدام.

هيكل البيانات:

ينظم ISO7816 كيفية هيكلة البيانات في البطاقات الذكية. وهو يدعم الهيكل التالي:

ملفات DF / Dedicated: يمكنك التفكير فيها كدلائل تحتوي على ملفات. من الضروري وجود جذر DF واحد على الأقل يسمى الملف الرئيسي (MF).

ملفات EF / Elementary: هذه ملفات تحتوي على بيانات ثنائية يمكن قراءتها أو كتابتها بواسطة قارئ خارجي.

هيكل البيانات كما هو موضح في ISO7816
يمكن أن تحتوي البطاقات في بعض الأحيان على أكثر من تطبيق صغير واحد ، وأول شيء عليك القيام به هو تحديد التطبيق الصغير الذي تريد الاتصال به باستخدام معرفه.

بروتوكول الاتصالات:

يعرّف ISO7816 أيضًا بروتوكول الاتصال باستخدام البطاقات الذكية (الاتصال والتلامس). للتواصل مع البطاقة ، يتعين على القارئ إرسال "أمر APDU" (أمر وحدة بيانات بروتوكول التطبيق) إلى البطاقة ، والتي سوف تستجيب بـ "استجابة APDU".

أوامر APDU هي صفائف بايت تحتوي على ما يلي:

هام: في كل ما يلي ، سنشير إلى وحدات البايت في تمثيلاتها السداسية العشرية ، مثل `A0` ،` FE` ، `00` ...

CLA: يتم استخدام بايت الفئة للإشارة إلى أي مدى يتوافق الأمر مع ISO7816 ، وإذا كان الأمر كذلك ، فما نوع "المراسلة الآمنة" التي سيتم استخدامها. لإبقاء الأمور بسيطة ، لن نستخدم هذه البايتة في مثالنا وسنمرر "00" طوال الوقت.

الباحث: يتم استخدام بايت التعليمات للإشارة إلى الطريقة التي نريد تنفيذها ، ويمكن أن يكون هذا مجموعة متنوعة من الطرق مثل: "A4" لتحديد ملف أو تطبيق صغير ، "B0" لقراءة ثنائي ، D0` لكتابة ثنائي ... (انظر القائمة الكاملة للتعليمات هنا)

P1 و P2: يتم استخدام هاتين البايتات المعلمتين لمزيد من التخصيص للتعليمات ، وهما يعتمدان على الأوامر المخصصة التي تحددها البطاقة. انقر هنا للحصول على قائمة الحالات المحتملة

Lc: هو طول البيانات المراد إرسالها

البيانات: هي البيانات الفعلية للتعليمات

Le: هو طول الاستجابة المتوقعة

ومن الأمثلة على ذلك اختيار ملف أو تطبيق صغير بمعرف `A0000002471001` كما يلي:` 00 A4 0400 07 A0000002471001 00`

بمجرد أن تتلقى البطاقة الأمر ، سوف تستجيب باستجابة APDU على النحو التالي:

حقل البيانات: هو نص الاستجابة

SW1 و SW2: هي بايتات الحالة ، يتم الفصل بينهما لأنه في بعض الأحيان يمكن للبايت الأول أن يخبرنا بالحالة الفعلية ، ويمكن للبايت الثاني تحديد مزيد من المعلومات حول هذه الحالة. على سبيل المثال ، إذا استخدمنا أمرًا "للتحقق من رقم PIN" برمز PIN غير صحيح ، فستُرجع البطاقة حالة `63 CX` حيث يكون X هو عدد المحاولات المتبقية ، وبهذه الطريقة ، يمكن للقارئ التطبيق التحقق من الأول بسهولة بايت للحالة والثانية لعدد من المحاولات اليسار.

عندما يتم تنفيذ الأمر بنجاح ، عادة ما نحصل على حالة "90 00" (انظر القائمة الكاملة للردود المحتملة هنا)

باستخدام المعلومات المذكورة أعلاه ، نحن الآن على استعداد لإنشاء محاكي البطاقة الذكية وقارئ البطاقة الذكية في نظام Android ، دعنا ندخله:

تأكد من تشغيل Android Studio 3.1 Canary أو إصدار أحدث لدعم تطوير Kotlin

مضاهاة البطاقة المستندة إلى المضيف (HCE):

بدءًا من Android 4.4 ، لدينا إمكانية إنشاء خدمة مضاهاة البطاقة ، والتي ستكون بمثابة بطاقة ذكية من خلال اتخاذ أوامر APDU وإعادة ردود APDU. للقيام بذلك ، لنقم بإنشاء مشروع Android جديد: جديد → مشروع جديد

تأكد من تحديد مربع الاختيار "تضمين دعم Kotlin" ، وانقر فوق "التالي"

تأكد من تحديد API 19 أو أعلى ، لأن بطاقة مضاهاة معتمدة فقط بدء تشغيل Android 4.4. انقر فوق "التالي" ، واختر "نشاط فارغ" و "إنهاء".
السبب الوحيد لإضافة نشاط ما هو جعل الأمور أكثر بساطة ، فسيتم تشغيل محاكي البطاقات لدينا كخدمة طوال الوقت في الخلفية ، لذلك لا يحتاج في الواقع إلى نشاط ، ولكن من أجل البساطة ، سوف نستخدم واحدًا .

أول ما سنقوم بإضافته هو إعلان تصريح Manifest لاستخدام NFC ، في "AndroidManifest.xml" ، أضف ما يلي داخل العلامة "manifest" قبل علامة التطبيق:

<يستخدم - إذن android: name = "android.permission.NFC" />

بعد ذلك ، سنضيف المتطلبات الخاصة بأجهزة HCE بحيث يتم تثبيت التطبيق فقط على الهواتف التي يمكنها تشغيل HCE ، مباشرة أسفل السطر السابق ، أضف هذا السطر:

<يستخدم-feature android: name = "android.hardware.nfc.hce"
    android: required = "true" />

بعد ذلك ، سوف نعلن عن خدمة HCE الخاصة بنا داخل العلامة `application`:

<الخدمة
    الروبوت ". HostCardEmulatorService" اسم =
    الروبوت: تصدير = "صحيح"
    الروبوت: إذن = "android.permission.BIND_NFC_SERVICE">
    <نية تصفية>
        
    

    <الفوقية البيانات
        الروبوت: اسم = "android.nfc.cardemulation.host_apdu_service"
        android: resource = "@ xml / apduservice" />

هناك الكثير مما يحدث في هذا الإعلان ، لذلك دعونا نمر عبره:

  • الاسم: هو اسم الفئة التي ستنفذ عمليات الاستدعاء الخدمة (سننشئ ذلك في غضون دقيقة).
  • تصدير: يجب أن يكون هذا صحيحًا حتى يمكن الوصول إلى خدماتنا بواسطة تطبيقات أخرى. إذا كان هذا خطأ ، فلا يمكن لأي تطبيق خارجي التفاعل مع الخدمة ، وهذا ليس ما نريده.
  • إذن: يجب أن ترتبط الخدمة بخدمة NFC حتى تتمكن من استخدام NFC.
  • تصفية Intent: عندما يكتشف نظام Android أن قارئ بطاقة خارجي يحاول قراءة بطاقة ، فإنه يطلق إجراءً "HOST_APDU_SERVICE" ، وسيتم استدعاء خدمتنا بعد تسجيله في هذا الإجراء ، ومن ثم يمكننا القيام بما نريد بمجرد أن يسمى الخدمة في العمل.
  • البيانات الوصفية: لكي يعرف النظام الخدمات التي يجب الاتصال بها بناءً على معرف المعونة الذي يحاول القارئ التواصل معه ، نحتاج إلى إعلان علامة "البيانات الوصفية" والإشارة إلى مورد XML.

لنقم الآن بإنشاء ملف XML "apduservice": انقر بزر الماوس الأيمن على مجلد "res" في مشروعك واختر "دليل" جديدًا ، واسمه "xml". ثم قم بإنشاء ملف xml جديد داخل هذا الدليل الجديد يسمى `apduservice` واكتب ما يلي داخل:


    
        
    

الجزء الأكثر أهمية هنا هو مرشح AID ، الذي يسجل خدمتنا ليتم تشغيلها إذا تم تحديد هذا المعرف بواسطة قارئ بطاقة.

يجب عليك إنشاء قيم @ string للأوصاف

الآن سننشئ خدمتنا ، انقر بزر الماوس الأيمن فوق الحزمة الخاصة بك ، حدد جديد → ملف / فئة Kotlin

حدد `Class` ونفس الاسم الذي وضعناه في البيان. انقر فوق موافق.

الآن سنقوم بتوسيع `ملخص HostApduService` وننفذ طرقه المجردة:

class HostCardEmulatorService: HostApduService () {
    تجاوز المتعة عند إلغاء التنشيط (السبب: Int) {
        TODO ("غير مطبق") // لتغيير نص الإنشاء
        / / وظائف استخدام ملف | الإعدادات | قوالب الملفات.
    }

    تجاوز عملية المرح CommandApdu (commandApdu: ByteArray ؟،
                                    إضافات: باقة؟): ByteArray {
        TODO ("غير مطبق") // لتغيير نص الإنشاء
        / / وظائف استخدام ملف | الإعدادات | قوالب الملفات.
    }
}

سيتم استدعاء الأسلوب "onDeactiveted" عند اختيار معرف AID مختلف أو فقد اتصال NFC.

سيتم استدعاء طريقة `processCommandApdu` في كل مرة يرسل فيها قارئ البطاقة أمر APDU يتم تصفيته بواسطة مرشح البيان.

دعنا نحدد بعض الثوابت للعمل ، قبل الطريقة الأولى ، أضف ما يلي:

كائن مصاحب
    val TAG = "محاكي بطاقة المضيف"
    val STATUS_SUCCESS = "9000"
    val STATUS_FAILED = "6F00"
    val CLA_NOT_SUPPORTED = "6E00"
    val INS_NOT_SUPPORTED = "6D00"
    val AID = "A0000002471001"
    val SELECT_INS = "A4"
    val DEFAULT_CLA = "00"
    val MIN_APDU_LENGTH = 12
}

فقط اذهب إلى الأمام واكتب هذا داخل `onDeactivated`:

Log.d (TAG ، "غير نشط:" + سبب)

قبل تطبيق طريقة "processCommandApdu" ، سنحتاج إلى عدة طرق مساعدة. قم بإنشاء فئة Kotlin جديدة باسم `Utils` وانسخ الطريقتين التاليتين (ثابت في Java):

كائن مصاحب
    خاص HEX_CHARS = "0123456789ABCDEF"
    متعة hexStringToByteArray (البيانات: سلسلة): ByteArray {

        نتيجة val = ByteArray (data.length / 2)

        لـ (i في 0 حتى data.length الخطوة 2) {
            val firstIndex = HEX_CHARS.indexOf (data [i])؛
            val secondIndex = HEX_CHARS.indexOf (data [i + 1]) ؛

            val octet = firstIndex.shl (4) .or (secondIndex)
            result.set (i.shr (1) ، octet.toByte ())
        }

        نتيجة العودة
    }

    private val HEX_CHARS_ARRAY = "0123456789ABCDEF" .toCharArray ()
    fun toHex (byteArray: ByteArray): String {
        نتيجة فال = StringBuffer ()

        byteArray.forEach {
            فال الثماني = it.toInt ()
            val firstIndex = (ثماني بتات و 0xF0) .ushr (4)
            فال secondIndex = ثماني و 0x0F
            result.append (HEX_CHARS_ARRAY [firstIndex])
            result.append (HEX_CHARS_ARRAY [secondIndex])
        }

        إرجاع النتيجة. إلى سلسلة ()
    }
}

هذه الطرق موجودة هنا للتحويل بين صفائف البايت والسلاسل السداسية العشرية.

الآن دعنا نكتب ما يلي في طريقة "processCommandApdu":

if (commandApdu == null) {
    إرجاع Utils.hexStringToByteArray (STATUS_FAILED)
}

val hexCommandApdu = Utils.toHex (commandApdu)
if (hexCommandApdu.length 

من الواضح أن هذا مجرد نموذج بالحجم الطبيعي لإنشاء أول محاكاة لنا. يمكنك تخصيص هذا ما تريد ، ولكن ما أنشأته هنا هو فحص بسيط للطول و CLA و INS يقوم بإرجاع APDU ناجح (9000) فقط عندما نختار AID المحدد مسبقًا.

قارئ بطاقة Android مع مثال NFC:

تمامًا مثل المشروع السابق ، قم بإنشاء مشروع جديد باستخدام Android 4.4 كحد أدنى من SDK ، وبدعم Kotlin ، مع نشاط فارغ.

داخل `AndroidManifest.xml` يعلن نفس إذن NFC:

<يستخدم - إذن android: name = "android.permission.NFC" />

أعلن أيضًا عن متطلبات NFC:

<يستخدم-feature android: name = "android.hardware.nfc"
    android: required = "true" />

في تخطيط activity_main.xml الخاص بك ، ما عليك سوى إضافة TextView لمشاهدة استجابات البطاقة مع

انسخ فئة "Utils" من المشروع السابق لأننا سنستخدم نفس الأساليب هنا أيضًا.

في نشاطك ، بجانب ملحق `AppCompatActivity () ، أضف ما يلي:

فئة MainActivity: AppCompatActivity () ، NfcAdapter.ReaderCallback

سيؤدي ذلك إلى تطبيق واجهة ReaderCallback وسوف يتعين عليك تطبيق طريقة onTagDiscovered.

داخل نشاطك ، قم بتعريف المتغير التالي:

فار فار nfcAdapter: NfcAdapter؟ = لاغية

في طريقة onCreate الخاصة بك ، أضف ما يلي:

nfcAdapter = NfcAdapter.getDefaultAdapter (هذا) ؛

سيحصل هذا على NfcAdapter الافتراضي لكي نستخدمه.

تجاوز طريقة `onResume` على النحو التالي:

تجاوز المتعة العامة onResume () {
    super.onResume ()
    nfcAdapter؟ .enableReaderMode (هذا ، هذا ،
            NfcAdapter.FLAG_READER_NFC_A أو
                    NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK،
            لا شيء)
}

سيؤدي هذا إلى تمكين وضع القارئ أثناء تشغيل هذا النشاط. عند التعامل مع البطاقة الذكية ، تأكد من أنك تبحث في التكنولوجيا التي تستخدمها ، لذلك تعلن عنها ، ولكن يمكنك استخدام NFC_A في الغالب. توجد العلامة الثانية ، لذلك نتخطى واجهات NDEF ، وهذا يعني في حالتنا ، أننا لا نريد أن يتم استدعاء Android Beam عندما يكون في هذا النشاط ، وإلا ، فسوف يتداخل مع قارئنا ، لأن Android يعطي أولوية لـ NDEF الكتابة قبل نوع TECH (أو البطاقات الذكية بشكل عام)

لا تنسَ تجاوز طريقة `onPause ()` وتعطيل NfcAdapter:

تجاوز المتعة العامة onPause () {
    super.onPause ()
    nfcAdapter؟ .disableReaderMode (هذا)
}

الآن آخر ما تبقى هو بدء إرسال أوامر APDU بمجرد اكتشاف البطاقة ، لذلك في طريقة `onTagDiscovered` ، دعنا نكتب ما يلي:

تجاوز متعة onTagDiscovered (علامة: علامة؟) {
    val isoDep = IsoDep.get (علامة)
    isoDep.connect ()
    استجابة val = isoDep.transceive (Utils.hexStringToByteArray (
            "00A4040007A0000002471001"))
    runOnUiThread {textView.append ("\ n استجابة البطاقة:"
            + Utils.toHex (استجابة))}
    isoDep.close ()
}

الآن إذا قمت بتشغيل التطبيق الأول على Phone A ، والتطبيق الثاني على Phone B ، فقمت بإعادة الهواتف إلى الوراء ، بحيث تواجه NFCs بعضها البعض ، إليك ما سيحدث بمجرد اكتشاف Phone B:

diagam التفاعل

سيقوم الهاتف B بإرسال أمر APDU أعلاه إلى HCE للهاتف A ، والذي سيرسله إلى خدمتنا ويعود إلى حالة 9000. يمكنك تغيير الأمر عن قصد لرؤية أحد الأخطاء التي وضعناها في الخدمة ، مثل استخدام CLA أو INS آخر ...

الآن هنا هو الجزء الجيد. إذا كنت تأخذ الهاتف B من خلال تطبيق قارئ البطاقات ، وأعدت ePassport (حيث توجد رقاقة) على قارئ NFC للهاتف ، فسترى استجابة 9000 من Chip of the Passport.

انتظر! ما حدث للتو هناك؟

الأمر APDU الذي نرسله إلى البطاقة هو نفسه الذي رأيناه في الفقرة الأولى. تحاول تحديد AID `A0000002471001` ، وهو معرف التطبيق الصغير الذي يحتوي على البيانات الشخصية لصاحب التسجيل داخل كل جواز سفر إلكتروني متوافق مع ICAO.

أين أذهب من هنا:

يمكنك في الواقع استخدام ما مررنا به هنا لقراءة البيانات الشخصية داخل جواز السفر ، ولكن هذا موضوع مختلف تمامًا ، وسأغطيه في الجزء الثاني من هذه السلسلة. بشكل أساسي ، نحتاج إلى إجراء التحكم في الوصول الأساسي (BAC) حيث نثبت للرقاقة أننا نعرف المفاتيح (يتم إنشاء المفاتيح استنادًا إلى عدد المستندات وتاريخ انتهاء الصلاحية وتاريخ الميلاد). إذا كنت مهتمًا بإنشاء قارئ لجوازات السفر ، فعليك قراءة المزيد حول BAC وهيكل البيانات داخل Passport ، حيث تبدأ صفحة 106 حيث يتم شرح BAC.

من خلال معرفة كيفية محاكاة البطاقة الذكية ، يمكنك كتابة نموذج بالحساب الخاص بك للحصول على بطاقة لا تملكها فعليًا ، ولكن لديك المواصفات الخاصة بها. بعد ذلك ، يمكنك استخدام القارئ الذي أنشأناه لقراءته ، وسيعمل بشكل مثالي مع البطاقة الحقيقية بمجرد الحصول عليها.

يمكنك أيضًا استخدام هاتفك بدلاً من البطاقة الفعلية - إذا كنت تعرف ما تحتويه البطاقات - عن طريق كتابة محاكي يعمل تمامًا مثل البطاقة. هذا يعني أنه ليس عليك حمل بطاقتك - يمكنك فقط استخدام تطبيق Emulator لفعل ما تريد باستخدام البطاقة.

قم بتنزيل مصادر لمضاهاة البطاقة المستندة إلى المضيف.
قم بتنزيل مصادر نموذج قارئ البطاقة الذكية.

كتبه محمد حمداوي لـ TWG: صناع البرمجيات للمبتكرين في العالم. تصفح صفحتنا المهنية لمعرفة المزيد عن فريقنا.