Xamarin.Forms-基于主机的卡模拟的简单示例

在本文中,我们将实现所谓的基于主机的卡模拟 (HCE,电话上的银行卡模拟)。 网络上对此技术有很多详细的描述,在这里,我重点介绍如何获得有效的仿真器和阅读器应用程序以及解决许多实际问题。 是的,您将需要2台配备nfc的设备。

有很多使用场景: 通行证系统 ,会员卡,运输卡,获取有关博物馆展品的其他信息, 密码管理器

同时,手机上模拟卡的应用程序可能无法启动,并且手机的屏幕可能已锁定。

对于Xamarin Android,有现成的卡模拟器读卡器示例。
让我们尝试使用这些示例制作2个Xamarin Forms应用程序,一个仿真器和一个阅读器,并解决其中的以下问题:

  1. 在阅读器屏幕上显示来自仿真器的数据
  2. 在仿真器屏幕上显示来自阅读器的数据
  3. 模拟器应与未发布的应用程序和锁定的屏幕一起使用
  4. 控制模拟器设置
  5. 当检测到阅读器时启动模拟器应用程序
  6. 检查NFC适配器的状态并切换到NFC设置

本文是关于android的,因此,如果您也有一个适用于iOS的应用程序,那么应该有一个单独的实现。

最低限度的理论。

android文档中所述,从版本4.4(kitkat)开始,已添加了仿真ISO-DEP卡和处理APDU命令的功能。

卡仿真基于称为“ HCE服务”的android服务。

当用户将设备连接到NFC阅读器时,Android需要了解阅读器要连接到的HCE服务。 ISO / IEC 7816-4描述了一种基于应用程序ID(AID)的应用程序选择方法。

如果有必要深入研究字节数组的美丽世界,那么这里这里将更详细地介绍APDU命令。 本文仅使用交换数据所需的几个命令。

读卡器应用


让我们从读者开始,因为 这比较简单。

我们在Visual Studio中创建一个新项目,例如“ Mobile App(Xamarin.Forms)”,然后选择“空白”模板,并在“平台”部分中仅保留“ Android”复选标记。

在android项目中,您需要执行以下操作:

  • CardReader类-它包含几个常量和OnTagDiscovered方法
  • MainActivity-CardReader类的初始化,以及在最小化应用程序时用于打开/关闭阅读器的OnPause和OnResume方法
  • AndroidManifest.xml-nfc的权限

在App.xaml.cs文件的跨平台项目中:

  • 向用户显示消息的方法

读卡器类


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(); } } 

在nfc适配器的读取模式下,当检测到卡时,将调用OnTagDiscovered方法。 在其中,IsoDep是一个对象,我们将通过该对象与地图交换命令(isoDep.Transceive(命令))。 命令是字节数组。

该代码表明,我们正在向模拟器发送一个序列,该序列由SELECT_APDU_HEADER标头,AID的字节长度和AID本身组成:

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

MainActivity Reader


在这里,您需要声明读者字段:

 public CardReader cardReader; 

和两个辅助方法:

 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); } 

在OnCreate()方法中,初始化读取器并启用读取模式:

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

并且,在最小化/打开应用程序时启用/禁用阅读模式:

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

App.xaml.cs


显示消息的静态方法:

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

AndroidManifest.xml


android文档说,要在应用程序中使用nfc并正确使用它,您需要在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" /> 

同时,如果您的应用程序可以使用nfc,但这不是必需的功能,则可以跳过uses-feature元素,并在操作过程中检查nfc的可用性。

这些都给读者。

仿真器应用


同样,在Visual Studio中创建一个新项目,例如“ Mobile App(Xamarin.Forms)”,然后选择“空白”模板,并在“平台”部分中仅保留“ Android”复选标记。

在一个Android项目中,执行以下操作:

  • CardService类-这里需要常量和ProcessCommandApdu()方法以及SendMessageToActivity()方法
  • aid_list.xml中的服务描述
  • MainActivity中的消息发送机制
  • 启动应用程序(如有必要)
  • AndroidManifest.xml-nfc的权限

在App.xaml.cs文件的跨平台项目中:

  • 向用户显示消息的方法

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); } } } 

从阅读器收到APDU命令后,将调用ProcessCommandApdu方法,并且该命令将作为字节数组传输到该方法。

首先,我们验证消息以SELECT_APDU_HEADER开头,如果是,则组成对阅读器的响应。 实际上,交换可以分几个步骤进行,例如问答。

在该类之前,Service属性描述了android服务的参数。 构建时,xamarin将此描述转换为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> 

aid_list.xml中的服务描述


在xml文件夹中,创建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> 

对它的引用在CardService类的Service属性中-Resource =“ @ xml / aid_list”
在这里,我们设置应用程序的AID,根据该AID读者可以访问它,并且属性requireDeviceUnlock =“ false”,以便使用未锁定的屏幕读取卡。

代码中有2个常量: @string/service_name@string/card_title 。 它们在values / strings.xml文件中声明:

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

消息发送机制:


该服务没有指向MainActivity的链接,该链接在接收APDU命令时可能甚至没有启动。 因此,我们使用BroadcastReceiver从CardService向MainActivity发送消息,如下所示:

从CardService发送消息的方法:

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

收到消息:
创建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); } } } 

在MainActivity中注册MessageReceiver:

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

App.xaml.cs


与显示消息的阅读器方法相同:

 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 

目前,我们已经具有以下功能:

  • 在阅读器屏幕上显示来自仿真器的数据
  • 在仿真器屏幕上显示来自阅读器的数据
  • 仿真器应在应用程序未运行且屏幕关闭的情况下工作。

下一个

仿真器控制


我将使用Xamarin.Essentials存储设置。

我们这样做:重新启动模拟器应用程序时,我们将更新设置:

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

并在ProcessCommandApdu方法中,每次都将再次使用该值:

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

现在,每次您重新启动(而不是最小化)仿真器应用程序时,我们都会看到一个新的guid,例如:

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

同样,通过设置,打开/关闭仿真器:

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

并在ProcessCommandApdu方法的开头添加:

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

这是一个简单的方法,但是还有其他方法

检测到读取器时运行仿真器应用程序


如果只需要打开仿真器应用程序,则在ProcessCommandApdu方法中添加以下行:

 StartActivity(typeof(MainActivity)); 

如果您需要将参数传递给应用程序,则如下所示:

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

您可以在OnCreate方法的MainActivity类中读取传递的参数:

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

检查NFC适配器的状态并切换到NFC设置


本部分适用于阅读器和仿真器。

在android项目中创建NfcHelper并使用DependencyService从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); } } } 

现在,在跨平台项目中,添加INfcHelper接口:

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

并在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; } } 

GitHub链接


仿真器
读者

Source: https://habr.com/ru/post/zh-CN471622/


All Articles