Xamarin.Forms - Un exemple simple d'émulation de carte basée sur l'hôte

Dans cet article, nous allons implémenter l' émulation de carte basée sur l'hôte (HCE, émulation de carte bancaire sur le téléphone). Le réseau a beaucoup de descriptions détaillées de cette technologie, je me suis concentré ici sur le fonctionnement des applications d'émulation et de lecture et la résolution d'un certain nombre de problèmes pratiques. Oui, vous aurez besoin de 2 appareils avec nfc.

Il existe de nombreux scénarios d'utilisation: système de pass , cartes de fidélité, cartes de transport, obtention d'informations supplémentaires sur les expositions du musée, gestionnaire de mots de passe .

Dans le même temps, l'application sur le téléphone émulant la carte peut être lancée ou non, et l'écran de votre téléphone peut être verrouillé.

Pour Xamarin Android, il existe des exemples prêts à l'emploi d' émulateur et de lecteur de carte .
Essayons d'utiliser ces exemples pour créer 2 applications Xamarin Forms, un émulateur et un lecteur, et résolvons les problèmes suivants:

  1. afficher les données de l'émulateur sur l'écran du lecteur
  2. afficher les données du lecteur sur l'écran de l'émulateur
  3. l'émulateur doit fonctionner avec une application non publiée et un écran verrouillé
  4. contrôler les paramètres de l'émulateur
  5. lancer l'application émulateur lorsqu'un lecteur est détecté
  6. vérification de l'état de l'adaptateur nfc et passage aux paramètres nfc

Cet article concerne Android, par conséquent, si vous avez également une application pour iOS, il devrait y avoir une implémentation distincte.

Le minimum de théorie.

Comme écrit dans la documentation Android , à partir de la version 4.4 (kitkat), la possibilité d'émuler des cartes ISO-DEP et de traiter les commandes APDU a été ajoutée.

L'émulation de carte est basée sur des services Android appelés «services HCE».

Lorsqu'un utilisateur connecte un appareil à un lecteur NFC, l'androïde doit comprendre à quel service HCE le lecteur souhaite se connecter. ISO / CEI 7816-4 décrit une méthode de sélection d'application basée sur l'ID d'application (AID).

S'il est intéressant de se plonger dans le magnifique monde des tableaux d'octets, alors ici et ici sont plus détaillés sur les commandes APDU. Cet article utilise seulement quelques commandes nécessaires pour échanger des données.

Application lecteur


Commençons par le lecteur, car c'est plus simple.

Nous créons un nouveau projet dans Visual Studio comme «Mobile App (Xamarin.Forms)», puis sélectionnons le modèle «Blank» et ne laissons que la coche «Android» dans la section «Platforms».

Dans le projet Android, vous devez effectuer les opérations suivantes:

  • La classe CardReader - elle contient plusieurs constantes et la méthode OnTagDiscovered
  • MainActivity - initialisation de la classe CardReader, ainsi que les méthodes OnPause et OnResume pour allumer / éteindre le lecteur lors de la réduction de l'application
  • AndroidManifest.xml - autorisations pour nfc

Et dans le projet multiplateforme dans le fichier App.xaml.cs:

  • Méthode d'affichage d'un message à l'utilisateur

Classe CardReader


using Android.Nfc; using Android.Nfc.Tech; using System; using System.Linq; using System.Text; namespace ApduServiceReaderApp.Droid.Services { public class CardReader : Java.Lang.Object, NfcAdapter.IReaderCallback { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // AID for our loyalty card service. private static readonly string SAMPLE_LOYALTY_CARD_AID = "F123456789"; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; public async void OnTagDiscovered(Tag tag) { IsoDep isoDep = IsoDep.Get(tag); if (isoDep != null) { try { isoDep.Connect(); var aidLength = (byte)(SAMPLE_LOYALTY_CARD_AID.Length / 2); var aidBytes = StringToByteArray(SAMPLE_LOYALTY_CARD_AID); var command = SELECT_APDU_HEADER .Concat(new byte[] { aidLength }) .Concat(aidBytes) .ToArray(); var result = isoDep.Transceive(command); var resultLength = result.Length; byte[] statusWord = { result[resultLength - 2], result[resultLength - 1] }; var payload = new byte[resultLength - 2]; Array.Copy(result, payload, resultLength - 2); var arrayEquals = SELECT_OK_SW.Length == statusWord.Length; if (Enumerable.SequenceEqual(SELECT_OK_SW, statusWord)) { var msg = Encoding.UTF8.GetString(payload); await App.DisplayAlertAsync(msg); } } catch (Exception e) { await App.DisplayAlertAsync("Error communicating with card: " + e.Message); } } } public static byte[] StringToByteArray(string hex) => Enumerable.Range(0, hex.Length) .Where(x => x % 2 == 0) .Select(x => Convert.ToByte(hex.Substring(x, 2), 16)) .ToArray(); } } 

En mode lecture de l'adaptateur nfc, lorsque la carte est détectée, la méthode OnTagDiscovered sera appelée. Dans ce document, IsoDep est un objet avec lequel nous échangerons des commandes avec la carte (isoDep.Transceive (commande)). Les commandes sont des tableaux d'octets.

Le code montre que nous envoyons à l'émulateur une séquence composée de l'en-tête SELECT_APDU_HEADER, de la longueur de notre AID en octets et de l'AID lui-même:

 0 164 4 0 // SELECT_APDU_HEADER 5 //  AID   241 35 69 103 137 // SAMPLE_LOYALTY_CARD_AID (F1 23 45 67 89) 

Lecteur MainActivity


Ici, vous devez déclarer le champ du lecteur:

 public CardReader cardReader; 

et deux méthodes d'assistance:

 private void EnableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.EnableReaderMode(this, cardReader, READER_FLAGS, null); } private void DisableReaderMode() { var nfc = NfcAdapter.GetDefaultAdapter(this); if (nfc != null) nfc.DisableReaderMode(this); } 

dans la méthode OnCreate (), initialisez le lecteur et activez le mode lecture:

 protected override void OnCreate(Bundle savedInstanceState) { ... cardReader = new CardReader(); EnableReaderMode(); LoadApplication(new App()); } 

et également, activer / désactiver le mode de lecture lors de la réduction / ouverture de l'application:

 protected override void OnPause() { base.OnPause(); DisableReaderMode(); } protected override void OnResume() { base.OnResume(); EnableReaderMode(); } 

App.xaml.cs


Méthode statique pour afficher un message:

 public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok")); 

AndroidManifest.xml


La documentation Android indique que pour utiliser nfc dans votre application et travailler correctement avec elle, vous devez déclarer ces éléments dans AndroidManifest.xml:

 <uses-permission android:name="android.permission.NFC" /> <uses-sdk android:minSdkVersion="10"/>   <uses-sdk android:minSdkVersion="14"/> <uses-feature android:name="android.hardware.nfc" android:required="true" /> 

Dans le même temps, si votre application peut utiliser nfc, mais que ce n'est pas une fonction requise, vous pouvez ignorer l'élément uses-feature et vérifier la disponibilité de nfc pendant le fonctionnement.

C'est tout pour le lecteur.

Application émulateur


Encore une fois, créez un nouveau projet dans Visual Studio comme "Mobile App (Xamarin.Forms)", puis sélectionnez le modèle "Blank" et ne laissez que la coche "Android" dans la section "Platforms".

Dans un projet Android, procédez comme suit:

  • Classe CardService - ici, vous avez besoin de constantes et de la méthode ProcessCommandApdu (), ainsi que de la méthode SendMessageToActivity ()
  • Description du service dans aid_list.xml
  • Mécanisme d'envoi de messages dans MainActivity
  • Lancement de l'application (si nécessaire)
  • AndroidManifest.xml - autorisations pour nfc

Et dans le projet multiplateforme dans le fichier App.xaml.cs:

  • Méthode d'affichage d'un message à l'utilisateur

Classe CardService


 using Android.App; using Android.Content; using Android.Nfc.CardEmulators; using Android.OS; using System; using System.Linq; using System.Text; namespace ApduServiceCardApp.Droid.Services { [Service(Exported = true, Enabled = true, Permission = "android.permission.BIND_NFC_SERVICE"), IntentFilter(new[] { "android.nfc.cardemulation.action.HOST_APDU_SERVICE" }, Categories = new[] { "android.intent.category.DEFAULT" }), MetaData("android.nfc.cardemulation.host_apdu_service", Resource = "@xml/aid_list")] public class CardService : HostApduService { // ISO-DEP command HEADER for selecting an AID. // Format: [Class | Instruction | Parameter 1 | Parameter 2] private static readonly byte[] SELECT_APDU_HEADER = new byte[] { 0x00, 0xA4, 0x04, 0x00 }; // "OK" status word sent in response to SELECT AID command (0x9000) private static readonly byte[] SELECT_OK_SW = new byte[] { 0x90, 0x00 }; // "UNKNOWN" status word sent in response to invalid APDU command (0x0000) private static readonly byte[] UNKNOWN_CMD_SW = new byte[] { 0x00, 0x00 }; public override byte[] ProcessCommandApdu(byte[] commandApdu, Bundle extras) { if (commandApdu.Length >= SELECT_APDU_HEADER.Length && Enumerable.SequenceEqual(commandApdu.Take(SELECT_APDU_HEADER.Length), SELECT_APDU_HEADER)) { var hexString = string.Join("", Array.ConvertAll(commandApdu, b => b.ToString("X2"))); SendMessageToActivity($"Recieved message from reader: {hexString}"); var messageToReader = "Hello Reader!"; var messageToReaderBytes = Encoding.UTF8.GetBytes(messageToReader); return messageToReaderBytes.Concat(SELECT_OK_SW).ToArray(); } return UNKNOWN_CMD_SW; } public override void OnDeactivated(DeactivationReason reason) { } private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); } } } 

À la réception de la commande APDU du lecteur, la méthode ProcessCommandApdu sera appelée et la commande lui sera transférée sous la forme d'un tableau d'octets.

Tout d'abord, nous vérifions que le message commence par SELECT_APDU_HEADER et, si c'est le cas, composons une réponse au lecteur. En réalité, un échange peut avoir lieu en plusieurs étapes, question-réponse question-réponse, etc.

Avant la classe, l'attribut Service décrit les paramètres du service Android. Lors de la construction, xamarin convertit cette description en un tel élément dans AndroidManifest.xml:

 <service name='md51c8b1c564e9c74403ac6103c28fa46ff.CardService' permission='android.permission.BIND_NFC_SERVICE' enabled='true' exported='true'> <meta-data name='android.nfc.cardemulation.host_apdu_service' resource='@res/0x7F100000'> </meta-data> <intent-filter> <action name='android.nfc.cardemulation.action.HOST_APDU_SERVICE'> </action> <category name='android.intent.category.DEFAULT'> </category> </intent-filter> </service> 

Description du service dans aid_list.xml


Dans le dossier xml, créez le fichier aid_list.xml:

 <?xml version="1.0" encoding="utf-8"?> <host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android" android:description="@string/service_name" android:requireDeviceUnlock="false"> <aid-group android:description="@string/card_title" android:category="other"> <aid-filter android:name="F123456789"/> </aid-group> </host-apdu-service> 

Une référence à celui-ci se trouve dans l'attribut Service de la classe CardService - Resource = "@ xml / aid_list"
Ici, nous définissons l'AID de notre application, selon lequel le lecteur y accédera et l'attribut requireDeviceUnlock = "false" afin que la carte soit lue avec un écran déverrouillé.

Il y a 2 constantes dans le code: @string/service_name et @string/card_title . Ils sont déclarés dans le fichier values ​​/ strings.xml:

 <resources> <string name="card_title">My Loyalty Card</string> <string name="service_name">My Company</string> </resources> 

Mécanisme d'envoi des messages:


Le service n'a aucun lien avec MainActivity qui, au moment de la réception de la commande APDU, peut même ne pas être démarré. Par conséquent, nous envoyons les messages de CardService à MainActivity à l'aide de BroadcastReceiver comme suit:

Méthode d'envoi d'un message depuis CardService:

 private void SendMessageToActivity(string msg) { Intent intent = new Intent("MSG_NAME"); intent.PutExtra("MSG_DATA", msg); SendBroadcast(intent); } 

Recevoir un message:
Créez la classe MessageReceiver:

 using Android.Content; namespace ApduServiceCardApp.Droid.Services { public class MessageReceiver : BroadcastReceiver { public override async void OnReceive(Context context, Intent intent) { var message = intent.GetStringExtra("MSG_DATA"); await App.DisplayAlertAsync(message); } } } 

Enregistrer MessageReceiver dans MainActivity:

 protected override void OnCreate(Bundle savedInstanceState) { ... var receiver = new MessageReceiver(); RegisterReceiver(receiver, new IntentFilter("MSG_NAME")); LoadApplication(new App()); } 

App.xaml.cs


Identique à la méthode du lecteur pour afficher un message:

 public static async Task DisplayAlertAsync(string msg) => await Device.InvokeOnMainThreadAsync(async () => await Current.MainPage.DisplayAlert("message from service", msg, "ok")); 

AndroidManifest.xml


  <uses-feature android:name="android.hardware.nfc.hce" android:required="true" /> <uses-feature android:name="FEATURE_NFC_HOST_CARD_EMULATION"/> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.BIND_NFC_SERVICE" /> <uses-sdk android:minSdkVersion="10"/>  14 

Pour le moment, nous avons déjà les fonctions suivantes:

  • afficher les données de l'émulateur sur l'écran du lecteur
  • afficher les données du lecteur sur l'écran de l'émulateur
  • L'émulateur doit fonctionner avec l'application non exécutée et l'écran éteint.

Ensuite.

Contrôle de l'émulateur


Je vais stocker les paramètres à l'aide de Xamarin.Essentials.

Faisons cela: lorsque nous redémarrons l'application d'émulation, nous mettons à jour le paramètre:

 Xamarin.Essentials.Preferences.Set("key1", Guid.NewGuid().ToString()); 

et dans la méthode ProcessCommandApdu, nous reprendrons cette valeur à chaque fois:

 var messageToReader = $"Hello Reader! - {Xamarin.Essentials.Preferences.Get("key1", "key1 not found")}"; 

Maintenant, chaque fois que vous redémarrez (et non pas minimisez) l'application émulateur, nous voyons un nouveau guide, par exemple:

 Hello Reader! - 76324a99-b5c3-46bc-8678-5650dab0529d 

De plus, via les paramètres, activez / désactivez l'émulateur:

 Xamarin.Essentials.Preferences.Set("IsEnabled", false); 

et au début de la méthode ProcessCommandApdu, ajoutez:

 var IsEnabled = Xamarin.Essentials.Preferences.Get("IsEnabled", false); if (!IsEnabled) return UNKNOWN_CMD_SW; // 0x00, 0x00 

C'est un moyen facile, mais il y en a d' autres .

Exécution de l'application d'émulateur lorsqu'un lecteur est détecté


Si vous avez juste besoin d'ouvrir l'application émulateur, ajoutez la ligne dans la méthode ProcessCommandApdu:

 StartActivity(typeof(MainActivity)); 

Si vous devez passer des paramètres à l'application, alors comme ceci:

 var activity = new Intent(this, typeof(MainActivity)); intent.PutExtra("MSG_DATA", "data for application"); this.StartActivity(activity); 

Vous pouvez lire les paramètres passés dans la classe MainActivity dans la méthode OnCreate:

 ... LoadApplication(new App()); if (Intent.Extras != null) { var message = Intent.Extras.GetString("MSG_DATA"); await App.DisplayAlertAsync(message); } 

Vérification de l'état de l'adaptateur nfc et passage aux paramètres nfc


Cette section s'applique à la fois au lecteur et à l'émulateur.

Créez NfcHelper dans le projet Android et utilisez DependencyService pour y accéder à partir du code de page MainPage.

 using Android.App; using Android.Content; using Android.Nfc; using ApduServiceCardApp.Services; using Xamarin.Forms; [assembly: Dependency(typeof(ApduServiceCardApp.Droid.Services.NfcHelper))] namespace ApduServiceCardApp.Droid.Services { public class NfcHelper : INfcHelper { public NfcAdapterStatus GetNfcAdapterStatus() { var adapter = NfcAdapter.GetDefaultAdapter(Forms.Context as Activity); return adapter == null ? NfcAdapterStatus.NoAdapter : adapter.IsEnabled ? NfcAdapterStatus.Enabled : NfcAdapterStatus.Disabled; } public void GoToNFCSettings() { var intent = new Intent(Android.Provider.Settings.ActionNfcSettings); intent.AddFlags(ActivityFlags.NewTask); Android.App.Application.Context.StartActivity(intent); } } } 

Maintenant, dans le projet multiplateforme, ajoutez l'interface INfcHelper:

 namespace ApduServiceCardApp.Services { public interface INfcHelper { NfcAdapterStatus GetNfcAdapterStatus(); void GoToNFCSettings(); } public enum NfcAdapterStatus { Enabled, Disabled, NoAdapter } } 

et utilisez tout cela dans le code MainPage.xaml.cs:

  protected override async void OnAppearing() { base.OnAppearing(); await CheckNfc(); } private async Task CheckNfc() { var nfcHelper = DependencyService.Get<INfcHelper>(); var status = nfcHelper.GetNfcAdapterStatus(); switch (status) { case NfcAdapterStatus.Enabled: default: await App.DisplayAlertAsync("nfc enabled!"); break; case NfcAdapterStatus.Disabled: nfcHelper.GoToNFCSettings(); break; case NfcAdapterStatus.NoAdapter: await App.DisplayAlertAsync("no nfc adapter found!"); break; } } 

Liens GitHub


émulateur
lecteur

Source: https://habr.com/ru/post/fr471622/


All Articles