Xamarin.Forms - um exemplo simples de emulação de cartão baseada em host

Neste artigo, implementaremos a chamada emulação de cartão baseada em host (HCE, emulação de cartão bancário no telefone). A rede possui muitas descrições detalhadas dessa tecnologia. Aqui, concentrei-me em trabalhar com aplicativos de emulador e leitor e resolver vários problemas práticos. Sim, você precisará de 2 dispositivos com nfc.

Existem muitos cenários de uso: sistema de passes , cartões de fidelidade, cartões de transporte, obtenção de informações adicionais sobre exposições no museu, gerenciador de senhas .

Nesse caso, o aplicativo no telefone que emula o cartão pode ou não ser iniciado e a tela do seu telefone pode estar bloqueada.

Para o Xamarin Android, existem exemplos prontos de um emulador e leitor de cartão .
Vamos tentar usar esses exemplos para criar 2 aplicativos Xamarin Forms, um emulador e um leitor, e resolver os seguintes problemas:

  1. exibir dados do emulador na tela do leitor
  2. exibe dados do leitor na tela do emulador
  3. o emulador deve funcionar com um aplicativo não lançado e uma tela bloqueada
  4. controlar configurações do emulador
  5. iniciar o aplicativo emulador quando um leitor for detectado
  6. verificando o status do adaptador nfc e alternando para as configurações nfc

Este artigo é sobre Android, portanto, se você tiver um aplicativo também para iOS, deverá haver uma implementação separada.

O mínimo de teoria.

Conforme escrito na documentação do Android , começando com a versão 4.4 (kitkat), foi adicionada a capacidade de emular cartões ISO-DEP e processar comandos APDU.

A emulação de cartão é baseada em serviços Android conhecidos como "serviços HCE".

Quando um usuário conecta um dispositivo a um leitor NFC, o Android precisa entender a qual serviço HCE o leitor deseja se conectar. A ISO / IEC 7816-4 descreve um método de seleção de aplicativos com base no ID do aplicativo (AID).

Se for interessante explorar o belo mundo das matrizes de bytes, aqui e aqui são mais detalhados sobre os comandos do APDU. Este artigo usa apenas alguns dos comandos necessários para trocar dados.

Aplicativo Reader


Vamos começar com o leitor, porque é mais simples.

Criamos um novo projeto no Visual Studio como “Mobile App (Xamarin.Forms)”, depois selecionamos o modelo em branco e deixamos apenas a marca de seleção “Android” na seção “Plataformas”.

No projeto android, você precisa fazer o seguinte:

  • A classe CardReader - contém várias constantes e o método OnTagDiscovered
  • MainActivity - inicialização da classe CardReader, bem como os métodos OnPause e OnResume para ativar / desativar o leitor ao minimizar o aplicativo
  • AndroidManifest.xml - permissões para nfc

E no projeto de plataforma cruzada no arquivo App.xaml.cs:

  • Método para exibir uma mensagem para o usuário

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

No modo de leitura do adaptador nfc, quando a placa é detectada, o método OnTagDiscovered será chamado. Nele, IsoDep é um objeto com o qual trocaremos comandos com o mapa (isoDep.Transceive (command)). Comandos são matrizes de bytes.

O código mostra que estamos enviando ao emulador uma sequência que consiste no cabeçalho SELECT_APDU_HEADER, no comprimento do nosso AID em bytes e no próprio 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


Aqui você precisa declarar o campo do leitor:

 public CardReader cardReader; 

e dois métodos auxiliares:

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

no método OnCreate (), inicialize o leitor e ative o modo de leitura:

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

e também, ative / desative o modo de leitura ao minimizar / abrir o aplicativo:

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

App.xaml.cs


Método estático para exibir uma mensagem:

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

AndroidManifest.xml


A documentação do Android diz que, para usar o nfc em seu aplicativo e funcionar corretamente com ele, é necessário declarar esses elementos no 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" /> 

Ao mesmo tempo, se seu aplicativo puder usar o nfc, mas essa não for uma função necessária, você poderá pular o elemento usos-recurso e verificar a disponibilidade do nfc durante a operação.

Isso é tudo para o leitor.

Aplicativo emulador


Novamente, crie um novo projeto no Visual Studio como "Mobile App (Xamarin.Forms)", selecione o modelo "Em branco" e deixe apenas a marca de seleção "Android" na seção "Plataformas".

Em um projeto Android, faça o seguinte:

  • Classe CardService - aqui você precisa constantes e o método ProcessCommandApdu (), bem como o método SendMessageToActivity ()
  • Descrição do serviço em aid_list.xml
  • Mecanismo de envio de mensagens no MainActivity
  • Lançamento do aplicativo (se necessário)
  • AndroidManifest.xml - permissões para nfc

E no projeto de plataforma cruzada no arquivo App.xaml.cs:

  • Método para exibir uma mensagem para o usuário

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

Após o recebimento do comando APDU do leitor, o método ProcessCommandApdu será chamado e o comando será transferido para ele como uma matriz de bytes.

Primeiro, verificamos que a mensagem começa com SELECT_APDU_HEADER e, se houver, redigimos uma resposta ao leitor. Na realidade, uma troca pode ocorrer em várias etapas, pergunta-resposta, pergunta-resposta, etc.

Antes da classe, o atributo Serviço descreve os parâmetros do serviço Android. Ao criar, o xamarin converte essa descrição em um elemento desse no 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> 

Descrição do serviço em aid_list.xml


Na pasta xml, crie o arquivo 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> 

Uma referência a ele está no atributo Serviço na classe CardService - Resource = "@ xml / aid_list"
Aqui, definimos o AUXÍLIO do nosso aplicativo, segundo o qual o leitor o acessará e o atributo requireDeviceUnlock = "false" para que o cartão seja lido com uma tela desbloqueada.

Existem 2 constantes no código: @string/service_name e @string/card_title . Eles são declarados no arquivo values ​​/ strings.xml:

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

Mecanismo de envio de mensagens:


O serviço não possui links para MainActivity, que no momento do recebimento do comando APDU nem podem ser iniciados. Portanto, enviamos mensagens do CardService para MainActivity usando BroadcastReceiver da seguinte maneira:

Método para enviar uma mensagem do CardService:

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

Recebendo uma mensagem:
Crie a 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); } } } 

Registrar o MessageReceiver no MainActivity:

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

App.xaml.cs


O mesmo que no método do leitor para exibir uma mensagem:

 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 

No momento, já temos as seguintes funções:

  • exibir dados do emulador na tela do leitor
  • exibe dados do leitor na tela do emulador
  • O emulador deve funcionar com o aplicativo não em execução e com a tela desligada.

Próximo.

Controle de emulador


Armazenarei as configurações usando o Xamarin.Essentials.

Vamos fazer o seguinte: quando reiniciarmos o aplicativo emulador, atualizaremos a configuração:

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

e no método ProcessCommandApdu, pegaremos esse valor novamente toda vez:

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

Agora, toda vez que você reinicia (não minimiza) o aplicativo emulador, vemos um novo guia, por exemplo:

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

Além disso, pelas configurações, ligue / desligue o emulador:

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

e no início do método ProcessCommandApdu, adicione:

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

Esta é uma maneira fácil, mas existem outras .

Executando o aplicativo emulador quando um leitor é detectado


Se você precisar apenas abrir o aplicativo emulador, adicione a linha no método ProcessCommandApdu:

 StartActivity(typeof(MainActivity)); 

Se você precisar passar parâmetros para o aplicativo, faça o seguinte:

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

Você pode ler os parâmetros passados ​​na classe MainActivity no método OnCreate:

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

Verificando o status do adaptador nfc e alternando para as configurações nfc


Esta seção se aplica ao leitor e ao emulador.

Crie NfcHelper no projeto Android e use DependencyService para acessá-lo no código da página 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); } } } 

Agora, no projeto de plataforma cruzada, adicione a interface INfcHelper:

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

e use tudo isso no código 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; } } 

Links para o GitHub


emulador
leitor

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


All Articles