├── .gitignore ├── .idea ├── caches │ ├── build_file_checksums.ser │ └── gradle_models.ser ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── android-hostcardemulation-sample.iml ├── apdu_exchange_tester ├── README.md └── apdu_tag_test.c ├── app ├── .gitignore ├── app.iml ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── justinribeiro │ │ └── demo │ │ └── apps │ │ └── hostcardemulation │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── justinribeiro │ │ └── demo │ │ └── apps │ │ └── hostcardemulation │ │ ├── myHostApduService.java │ │ ├── myStartActivity.java │ │ └── utils.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── layout-v21 │ └── activity_start.xml │ ├── layout │ └── activity_start.xml │ ├── menu │ └── start.xml │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── apduservice.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hostcardemulation.iml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | /.idea/libraries 5 | .DS_Store 6 | /build 7 | 8 | ### Android ### 9 | # Built application files 10 | *.apk 11 | *.ap_ 12 | 13 | # Files for the Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | 27 | # Local configuration file (sdk path, etc) 28 | local.properties 29 | 30 | # Proguard folder generated by Eclipse 31 | proguard/ 32 | 33 | # Log Files 34 | *.log 35 | 36 | -------------------------------------------------------------------------------- /.idea/caches/build_file_checksums.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/.idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /.idea/caches/gradle_models.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/.idea/caches/gradle_models.ser -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Host Card Emulation of a NFC Forum Type-4 tag 2 | 3 | This example emulates a NFC Forum Type-4 tag with an a single NDEF record (RTD_TEXT). The project uses aid-filter F0394148148100 (which is an example that is defined in the Android documentation) for the APDU SELECT command. 4 | 5 | ## Whoa there what? 6 | 7 | The NFC Forum Type 4 Tag Operation Specification 3.0 outlines how to interact with tags. Using Host Card Emulation in Android, we can do something pretty nifty: 8 | 9 | 1. We can write an application on the card reader side that sends the proper APDU SELECT and associated commands and interact with a "card". 10 | 2. That "card" in this case is emulated in our Android application. 11 | 12 | ## Where can I get the tag operational spec? 13 | 14 | There are various copies on the Internet, but the place to go is to sign the specification license and download direct from the [NFC Forum document library](http://members.nfc-forum.org/specs/spec_license/document_form/). 15 | 16 | ## How do I interact with the Android application through a card reader? 17 | 18 | You have to follow the commands in your client application. You can also compile and run the sample in the apdu_exchange_tester directory that is based on the APDU examples from libNFC (for detailed instructions, see the readme in that folder). 19 | 20 | ## Where can I learn more about Host Card Emulation on Android? 21 | 22 | You'll need API 19 (aka Android 4.4) to use HCE. See [Host-based Card Emulation @ developers.android.com](https://developer.android.com/guide/topics/connectivity/nfc/hce.html) for all the details. 23 | 24 | ## What if I want to interact with or read a Type-4 tag? 25 | 26 | This is a different sort of thing and you'll want to look into [ISO-DEP](http://developer.android.com/reference/android/nfc/tech/IsoDep.html). -------------------------------------------------------------------------------- /android-hostcardemulation-sample.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /apdu_exchange_tester/README.md: -------------------------------------------------------------------------------- 1 | # Testing the Android Host Card Emulation of a NFC Forum Type-4 tag 2 | 3 | The following program takes a command by command approach to testing our ISO14443-4 emulated tag. It is an extended version of the existing [libnfc apdu example](http://nfc-tools.org/index.php?title=Libnfc:APDU_example). 4 | 5 | ## What do I need to run this? 6 | 7 | You'll need a couple of things: 8 | 9 | 1. An NFC Reader. I've used this with the [ACR122U USB NFC Reader](http://www.acs.com.hk/en/products/3/acr122u-usb-nfc-reader) without issue. 10 | 2. [Libnfc](http://nfc-tools.org/index.php?title=Libnfc). You'll likely be able to install from your package manager of choice. 11 | 12 | ## Compile 13 | 14 | ``` 15 | gcc -o apdu_tag_test apdu_tag_test.c -lnfc 16 | ``` 17 | 18 | ## Run 19 | 20 | ``` 21 | ./apdu_tag_test 22 | ``` 23 | 24 | ## Sample Output 25 | 26 | ``` 27 | $ ./apdu_tag_test 28 | 29 | Running checks... 30 | ./apdu_tag_test uses libnfc 1.7.1 31 | NFC reader: ACS / ACR122U PICC Interface opened 32 | Polling for target... 33 | Target detected! Running command set... 34 | 35 | Sending ADPU SELECT... 36 | => 00 a4 04 00 07 f0 39 41 48 14 81 00 00 37 | <= 90 00 38 | Application selected! 39 | 40 | Sending CC SELECT... 41 | => 00 a4 00 0c 02 e1 03 42 | <= 90 00 43 | Capability Container selected! 44 | 45 | Sending ReadBinary from CC... 46 | => 00 b0 00 00 0f 47 | <= 00 0f 20 00 3b 00 34 04 06 e1 04 00 32 00 00 90 00 48 | 49 | Capability Container header: 50 | 00 0f 20 00 3b 00 34 04 06 e1 04 00 32 00 00 51 | 52 | Sending NDEF Select... 53 | => 00 a4 00 0c 02 e1 04 54 | <= 90 00 55 | 56 | Sending ReadBinary NLEN... 57 | => 00 b0 00 00 02 58 | <= 00 13 90 00 59 | 60 | Sending ReadBinary, get NDEF data... 61 | => 00 b0 00 00 0f 62 | <= 00 13 d9 01 0c 02 54 e1 04 48 65 6c 6c 6f 20 77 6f 72 6c 64 21 90 00 63 | 64 | Wrapping up, closing session. 65 | ``` 66 | 67 | ## Things this doesn't do 68 | It's not nearly as strict to the spec as it should be, but for simple testing it works well. 69 | -------------------------------------------------------------------------------- /apdu_exchange_tester/apdu_tag_test.c: -------------------------------------------------------------------------------- 1 | /** 2 | Modified from original example @ Libnfc: 3 | http://nfc-tools.org/index.php?title=Libnfc:APDU_example 4 | 5 | Allows testing of Host-based Card Emulatio Android example at 6 | https://github.com/justinribeiro/android-hostcardemulation-sample 7 | 8 | Requires Libnfc: 9 | http://nfc-tools.org/index.php?title=Libnfc 10 | 11 | To compile: 12 | gcc -o apdu_tag_test apdu_tag_test.c -lnfc 13 | */ 14 | #include 15 | #include 16 | #include 17 | int 18 | CardTransmit(nfc_device *pnd, uint8_t * capdu, size_t capdulen, uint8_t * rapdu, size_t * rapdulen) 19 | { 20 | int res; 21 | size_t szPos; 22 | printf("=> "); 23 | for (szPos = 0; szPos < capdulen; szPos++) { 24 | printf("%02x ", capdu[szPos]); 25 | } 26 | printf("\n"); 27 | if ((res = nfc_initiator_transceive_bytes(pnd, capdu, capdulen, rapdu, *rapdulen, 500)) < 0) { 28 | return -1; 29 | } else { 30 | *rapdulen = (size_t) res; 31 | printf("<= "); 32 | for (szPos = 0; szPos < *rapdulen; szPos++) { 33 | printf("%02x ", rapdu[szPos]); 34 | } 35 | printf("\n"); 36 | return 0; 37 | } 38 | } 39 | int 40 | main(int argc, const char *argv[]) 41 | { 42 | nfc_device *pnd; 43 | nfc_target nt; 44 | nfc_context *context; 45 | nfc_init(&context); 46 | 47 | printf("\nRunning checks...\n"); 48 | 49 | if (context == NULL) { 50 | printf("Unable to init libnfc (malloc)\n"); 51 | exit(EXIT_FAILURE); 52 | } 53 | 54 | const char *acLibnfcVersion = nfc_version(); 55 | (void)argc; 56 | printf("%s uses libnfc %s\n", argv[0], acLibnfcVersion); 57 | 58 | pnd = nfc_open(context, NULL); 59 | 60 | if (pnd == NULL) { 61 | printf("ERROR: %s", "Unable to open NFC device."); 62 | exit(EXIT_FAILURE); 63 | } 64 | if (nfc_initiator_init(pnd) < 0) { 65 | nfc_perror(pnd, "nfc_initiator_init"); 66 | exit(EXIT_FAILURE); 67 | } 68 | 69 | printf("NFC reader: %s opened\n", nfc_device_get_name(pnd)); 70 | 71 | const nfc_modulation nmMifare = { 72 | .nmt = NMT_ISO14443A, 73 | .nbr = NBR_106, 74 | }; 75 | // nfc_set_property_bool(pnd, NP_AUTO_ISO14443_4, true); 76 | printf("Polling for target...\n"); 77 | while (nfc_initiator_select_passive_target(pnd, nmMifare, NULL, 0, &nt) <= 0); 78 | printf("Target detected! Running command set...\n\n"); 79 | uint8_t capdu[264]; 80 | size_t capdulen; 81 | uint8_t rapdu[264]; 82 | size_t rapdulen; 83 | // Select application 84 | memcpy(capdu, "\x00\xA4\x04\x00\x07\xF0\x39\x41\x48\x14\x81\x00\x00", 13); 85 | capdulen=13; 86 | rapdulen=sizeof(rapdu); 87 | 88 | printf("Sending ADPU SELECT...\n"); 89 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) { 90 | exit(EXIT_FAILURE); 91 | } 92 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) { 93 | exit(EXIT_FAILURE); 94 | } 95 | printf("Application selected!\n\n"); 96 | 97 | // Select Capability Container 98 | memcpy(capdu, "\x00\xa4\x00\x0c\x02\xe1\x03", 7); 99 | capdulen=7; 100 | rapdulen=sizeof(rapdu); 101 | 102 | printf("Sending CC SELECT...\n"); 103 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 104 | exit(EXIT_FAILURE); 105 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) { 106 | capdu[3]='\x00'; // Maybe an older Tag4 ? 107 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 108 | exit(EXIT_FAILURE); 109 | } 110 | printf("Capability Container selected!\n\n"); 111 | 112 | // Read Capability Container 113 | memcpy(capdu, "\x00\xb0\x00\x00\x0f", 5); 114 | capdulen=5; 115 | rapdulen=sizeof(rapdu); 116 | 117 | printf("Sending ReadBinary from CC...\n"); 118 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 119 | exit(EXIT_FAILURE); 120 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) 121 | exit(EXIT_FAILURE); 122 | printf("\nCapability Container header:\n"); 123 | size_t szPos; 124 | for (szPos = 0; szPos < rapdulen-2; szPos++) { 125 | printf("%02x ", rapdu[szPos]); 126 | } 127 | printf("\n\n"); 128 | 129 | // NDEF Select 130 | memcpy(capdu, "\x00\xa4\x00\x0C\x02\xE1\x04", 7); 131 | capdulen=7; 132 | rapdulen=sizeof(rapdu); 133 | printf("Sending NDEF Select...\n"); 134 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 135 | exit(EXIT_FAILURE); 136 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) 137 | exit(EXIT_FAILURE); 138 | printf("\n"); 139 | 140 | // ReadBinary 141 | memcpy(capdu, "\x00\xb0\x00\x00\x02", 5); 142 | capdulen=5; 143 | rapdulen=sizeof(rapdu); 144 | printf("Sending ReadBinary NLEN...\n"); 145 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 146 | exit(EXIT_FAILURE); 147 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) 148 | exit(EXIT_FAILURE); 149 | printf("\n"); 150 | 151 | // ReadBinary - Get NDEF data 152 | memcpy(capdu, "\x00\xb0\x00\x00\x0f", 5); 153 | capdulen=5; 154 | rapdulen=sizeof(rapdu); 155 | printf("Sending ReadBinary, get NDEF data...\n"); 156 | if (CardTransmit(pnd, capdu, capdulen, rapdu, &rapdulen) < 0) 157 | exit(EXIT_FAILURE); 158 | if (rapdulen < 2 || rapdu[rapdulen-2] != 0x90 || rapdu[rapdulen-1] != 0x00) 159 | exit(EXIT_FAILURE); 160 | printf("\n"); 161 | 162 | printf("Wrapping up, closing session.\n\n"); 163 | 164 | nfc_close(pnd); 165 | nfc_exit(context); 166 | exit(EXIT_SUCCESS); 167 | } 168 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | buildToolsVersion '29.0.2' 6 | 7 | defaultConfig { 8 | applicationId "com.justinribeiro.demo.apps.hostcardemulation" 9 | minSdkVersion 19 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled true 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation 'androidx.appcompat:appcompat:1.0.0' 24 | } 25 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Android\android-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/justinribeiro/demo/apps/hostcardemulation/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.justinribeiro.demo.apps.hostcardemulation; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/justinribeiro/demo/apps/hostcardemulation/myHostApduService.java: -------------------------------------------------------------------------------- 1 | package com.justinribeiro.demo.apps.hostcardemulation; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.nfc.NdefRecord; 6 | import android.nfc.cardemulation.HostApduService; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | import android.view.Gravity; 10 | import android.widget.Toast; 11 | 12 | import java.math.BigInteger; 13 | import java.nio.charset.Charset; 14 | import java.util.Arrays; 15 | 16 | /** 17 | * Created by justin.ribeiro on 10/27/2014. 18 | * 19 | * The following definitions are based on two things: 20 | * 1. NFC Forum Type 4 Tag Operation Technical Specification, version 3.0 2014-07-30 21 | * 2. APDU example in libnfc: http://nfc-tools.org/index.php?title=Libnfc:APDU_example 22 | * 23 | */ 24 | public class myHostApduService extends HostApduService { 25 | 26 | private static final String TAG = "JDR HostApduService"; 27 | 28 | // 29 | // We use the default AID from the HCE Android documentation 30 | // https://developer.android.com/guide/topics/connectivity/nfc/hce.html 31 | // 32 | // Ala... 33 | // 34 | private static final byte[] APDU_SELECT = { 35 | (byte)0x00, // CLA - Class - Class of instruction 36 | (byte)0xA4, // INS - Instruction - Instruction code 37 | (byte)0x04, // P1 - Parameter 1 - Instruction parameter 1 38 | (byte)0x00, // P2 - Parameter 2 - Instruction parameter 2 39 | (byte)0x07, // Lc field - Number of bytes present in the data field of the command 40 | (byte)0xF0, (byte)0x39, (byte)0x41, (byte)0x48, (byte)0x14, (byte)0x81, (byte)0x00, // NDEF Tag Application name 41 | (byte)0x00 // Le field - Maximum number of bytes expected in the data field of the response to the command 42 | }; 43 | 44 | private static final byte[] CAPABILITY_CONTAINER = { 45 | (byte)0x00, // CLA - Class - Class of instruction 46 | (byte)0xa4, // INS - Instruction - Instruction code 47 | (byte)0x00, // P1 - Parameter 1 - Instruction parameter 1 48 | (byte)0x0c, // P2 - Parameter 2 - Instruction parameter 2 49 | (byte)0x02, // Lc field - Number of bytes present in the data field of the command 50 | (byte)0xe1, (byte)0x03 // file identifier of the CC file 51 | }; 52 | 53 | private static final byte[] READ_CAPABILITY_CONTAINER = { 54 | (byte)0x00, // CLA - Class - Class of instruction 55 | (byte)0xb0, // INS - Instruction - Instruction code 56 | (byte)0x00, // P1 - Parameter 1 - Instruction parameter 1 57 | (byte)0x00, // P2 - Parameter 2 - Instruction parameter 2 58 | (byte)0x0f // Lc field - Number of bytes present in the data field of the command 59 | }; 60 | 61 | // In the scenario that we have done a CC read, the same byte[] match 62 | // for ReadBinary would trigger and we don't want that in succession 63 | private boolean READ_CAPABILITY_CONTAINER_CHECK = false; 64 | 65 | private static final byte[] READ_CAPABILITY_CONTAINER_RESPONSE = { 66 | (byte)0x00, (byte)0x0F, // CCLEN length of the CC file 67 | (byte)0x20, // Mapping Version 2.0 68 | (byte)0x00, (byte)0x3B, // MLe maximum 59 bytes R-APDU data size 69 | (byte)0x00, (byte)0x34, // MLc maximum 52 bytes C-APDU data size 70 | (byte)0x04, // T field of the NDEF File Control TLV 71 | (byte)0x06, // L field of the NDEF File Control TLV 72 | (byte)0xE1, (byte)0x04, // File Identifier of NDEF file 73 | (byte)0x00, (byte)0x32, // Maximum NDEF file size of 50 bytes 74 | (byte)0x00, // Read access without any security 75 | (byte)0x00, // Write access without any security 76 | (byte)0x90, (byte)0x00 // A_OKAY 77 | }; 78 | 79 | private static final byte[] NDEF_SELECT = { 80 | (byte)0x00, // CLA - Class - Class of instruction 81 | (byte)0xa4, // Instruction byte (INS) for Select command 82 | (byte)0x00, // Parameter byte (P1), select by identifier 83 | (byte)0x0c, // Parameter byte (P1), select by identifier 84 | (byte)0x02, // Lc field - Number of bytes present in the data field of the command 85 | (byte)0xE1, (byte)0x04 // file identifier of the NDEF file retrieved from the CC file 86 | }; 87 | 88 | private static final byte[] NDEF_READ_BINARY_NLEN = { 89 | (byte)0x00, // Class byte (CLA) 90 | (byte)0xb0, // Instruction byte (INS) for ReadBinary command 91 | (byte)0x00, (byte)0x00, // Parameter byte (P1, P2), offset inside the CC file 92 | (byte)0x02 // Le field 93 | }; 94 | 95 | private static final byte[] NDEF_READ_BINARY_GET_NDEF = { 96 | (byte)0x00, // Class byte (CLA) 97 | (byte)0xb0, // Instruction byte (INS) for ReadBinary command 98 | (byte)0x00, (byte)0x00, // Parameter byte (P1, P2), offset inside the CC file 99 | (byte)0x0f // Le field 100 | }; 101 | 102 | private static final byte[] A_OKAY = { 103 | (byte)0x90, // SW1 Status byte 1 - Command processing status 104 | (byte)0x00 // SW2 Status byte 2 - Command processing qualifier 105 | }; 106 | 107 | private static final byte[] NDEF_ID = { 108 | (byte)0xE1, 109 | (byte)0x04 110 | }; 111 | 112 | private NdefRecord NDEF_URI = new NdefRecord( 113 | NdefRecord.TNF_WELL_KNOWN, 114 | NdefRecord.RTD_TEXT, 115 | NDEF_ID, 116 | "Hello world!".getBytes(Charset.forName("UTF-8")) 117 | ); 118 | private byte[] NDEF_URI_BYTES = NDEF_URI.toByteArray(); 119 | private byte[] NDEF_URI_LEN = BigInteger.valueOf(NDEF_URI_BYTES.length).toByteArray(); 120 | 121 | @Override 122 | public int onStartCommand(Intent intent, int flags, int startId) { 123 | 124 | if (intent.hasExtra("ndefMessage")) { 125 | NDEF_URI = new NdefRecord( 126 | NdefRecord.TNF_WELL_KNOWN, 127 | NdefRecord.RTD_TEXT, 128 | NDEF_ID, 129 | intent.getStringExtra("ndefMessage").getBytes(Charset.forName("UTF-8")) 130 | ); 131 | 132 | NDEF_URI_BYTES = NDEF_URI.toByteArray(); 133 | NDEF_URI_LEN = BigInteger.valueOf(NDEF_URI_BYTES.length).toByteArray(); 134 | 135 | Context context = getApplicationContext(); 136 | CharSequence text = "Your NDEF text has been set!"; 137 | int duration = Toast.LENGTH_SHORT; 138 | Toast toast = Toast.makeText(context, text, duration); 139 | toast.setGravity(Gravity.CENTER, 0, 0); 140 | toast.show(); 141 | } 142 | 143 | Log.i(TAG, "onStartCommand() | NDEF" + NDEF_URI.toString()); 144 | 145 | return 0; 146 | } 147 | 148 | @Override 149 | public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { 150 | 151 | // 152 | // The following flow is based on Appendix E "Example of Mapping Version 2.0 Command Flow" 153 | // in the NFC Forum specification 154 | // 155 | Log.i(TAG, "processCommandApdu() | incoming commandApdu: " + utils.bytesToHex(commandApdu)); 156 | 157 | // 158 | // First command: NDEF Tag Application select (Section 5.5.2 in NFC Forum spec) 159 | // 160 | if (utils.isEqual(APDU_SELECT, commandApdu)) { 161 | Log.i(TAG, "APDU_SELECT triggered. Our Response: " + utils.bytesToHex(A_OKAY)); 162 | return A_OKAY; 163 | } 164 | 165 | // 166 | // Second command: Capability Container select (Section 5.5.3 in NFC Forum spec) 167 | // 168 | if (utils.isEqual(CAPABILITY_CONTAINER, commandApdu)) { 169 | Log.i(TAG, "CAPABILITY_CONTAINER triggered. Our Response: " + utils.bytesToHex(A_OKAY)); 170 | return A_OKAY; 171 | } 172 | 173 | // 174 | // Third command: ReadBinary data from CC file (Section 5.5.4 in NFC Forum spec) 175 | // 176 | if (utils.isEqual(READ_CAPABILITY_CONTAINER, commandApdu) && !READ_CAPABILITY_CONTAINER_CHECK) { 177 | Log.i(TAG, "READ_CAPABILITY_CONTAINER triggered. Our Response: " + utils.bytesToHex(READ_CAPABILITY_CONTAINER_RESPONSE)); 178 | READ_CAPABILITY_CONTAINER_CHECK = true; 179 | return READ_CAPABILITY_CONTAINER_RESPONSE; 180 | } 181 | 182 | // 183 | // Fourth command: NDEF Select command (Section 5.5.5 in NFC Forum spec) 184 | // 185 | if (utils.isEqual(NDEF_SELECT, commandApdu)) { 186 | Log.i(TAG, "NDEF_SELECT triggered. Our Response: " + utils.bytesToHex(A_OKAY)); 187 | return A_OKAY; 188 | } 189 | 190 | // 191 | // Fifth command: ReadBinary, read NLEN field 192 | // 193 | if (utils.isEqual(NDEF_READ_BINARY_NLEN, commandApdu)) { 194 | 195 | byte[] start = { 196 | (byte)0x00 197 | }; 198 | 199 | // Build our response 200 | byte[] response = new byte[start.length + NDEF_URI_LEN.length + A_OKAY.length]; 201 | 202 | System.arraycopy(start, 0, response, 0, start.length); 203 | System.arraycopy(NDEF_URI_LEN, 0, response, start.length, NDEF_URI_LEN.length); 204 | System.arraycopy(A_OKAY, 0, response, start.length + NDEF_URI_LEN.length, A_OKAY.length); 205 | 206 | Log.i(TAG, response.toString()); 207 | Log.i(TAG, "NDEF_READ_BINARY_NLEN triggered. Our Response: " + utils.bytesToHex(response)); 208 | 209 | return response; 210 | } 211 | 212 | // 213 | // Sixth command: ReadBinary, get NDEF data 214 | // 215 | if (utils.isEqual(NDEF_READ_BINARY_GET_NDEF, commandApdu)) { 216 | Log.i(TAG, "processCommandApdu() | NDEF_READ_BINARY_GET_NDEF triggered"); 217 | 218 | byte[] start = { 219 | (byte)0x00 220 | }; 221 | 222 | // Build our response 223 | byte[] response = new byte[start.length + NDEF_URI_LEN.length + NDEF_URI_BYTES.length + A_OKAY.length]; 224 | 225 | System.arraycopy(start, 0, response, 0, start.length); 226 | System.arraycopy(NDEF_URI_LEN, 0, response, start.length, NDEF_URI_LEN.length); 227 | System.arraycopy(NDEF_URI_BYTES, 0, response, start.length + NDEF_URI_LEN.length, NDEF_URI_BYTES.length); 228 | System.arraycopy(A_OKAY, 0, response, start.length + NDEF_URI_LEN.length + NDEF_URI_BYTES.length, A_OKAY.length); 229 | 230 | Log.i(TAG, NDEF_URI.toString()); 231 | Log.i(TAG, "NDEF_READ_BINARY_GET_NDEF triggered. Our Response: " + utils.bytesToHex(response)); 232 | 233 | Context context = getApplicationContext(); 234 | CharSequence text = "NDEF text has been sent to the reader!"; 235 | int duration = Toast.LENGTH_SHORT; 236 | Toast toast = Toast.makeText(context, text, duration); 237 | toast.setGravity(Gravity.CENTER, 0, 0); 238 | toast.show(); 239 | 240 | READ_CAPABILITY_CONTAINER_CHECK = false; 241 | return response; 242 | } 243 | 244 | // 245 | // We're doing something outside our scope 246 | // 247 | Log.wtf(TAG, "processCommandApdu() | I don't know what's going on!!!."); 248 | return "Can I help you?".getBytes(); 249 | } 250 | 251 | @Override 252 | public void onDeactivated(int reason) { 253 | Log.i(TAG, "onDeactivated() Fired! Reason: " + reason); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /app/src/main/java/com/justinribeiro/demo/apps/hostcardemulation/myStartActivity.java: -------------------------------------------------------------------------------- 1 | package com.justinribeiro.demo.apps.hostcardemulation; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import androidx.appcompat.app.ActionBar; 7 | 8 | import android.view.View; 9 | import android.widget.Button; 10 | import android.widget.TextView; 11 | 12 | public class myStartActivity extends AppCompatActivity { 13 | private static final String TAG = "JDR HostCardEmulation"; 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_start); 19 | 20 | ActionBar toolbar = getSupportActionBar(); 21 | toolbar.setTitle(R.string.app_name); 22 | 23 | Button setNdef = (Button) findViewById(R.id.set_ndef_button); 24 | setNdef.setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View view) { 27 | 28 | // 29 | // Technically, if this is past our byte limit, 30 | // it will cause issues. 31 | // 32 | // TODO: add validation 33 | // 34 | TextView getNdefString = (TextView) findViewById(R.id.ndef_text); 35 | String test = getNdefString.getText().toString(); 36 | 37 | Intent intent = new Intent(view.getContext(), myHostApduService.class); 38 | intent.putExtra("ndefMessage", test); 39 | startService(intent); 40 | } 41 | } 42 | ); 43 | } 44 | 45 | @Override 46 | public void onResume() { 47 | super.onResume(); 48 | } 49 | 50 | @Override 51 | public void onPause() { 52 | super.onPause(); 53 | } 54 | 55 | @Override 56 | public void onStop() { 57 | super.onStop(); 58 | } 59 | 60 | @Override 61 | public void onDestroy() { 62 | super.onDestroy(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/justinribeiro/demo/apps/hostcardemulation/utils.java: -------------------------------------------------------------------------------- 1 | package com.justinribeiro.demo.apps.hostcardemulation; 2 | 3 | /** 4 | * Created by justin.ribeiro on 10/28/2014. 5 | */ 6 | 7 | /** 8 | * Just a tiny class to dump things that I may want to use 9 | * 10 | * AKA: you probably don't need this, but you might :-) 11 | */ 12 | public class utils { 13 | 14 | final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); 15 | 16 | /** 17 | * Simple way to output byte[] to hex (my readable preference) 18 | * This version quite speedy; originally from: http://stackoverflow.com/a/9855338 19 | * 20 | * @param bytes yourByteArray 21 | * @return string 22 | * 23 | */ 24 | public static String bytesToHex(byte[] bytes) { 25 | char[] hexChars = new char[bytes.length * 2]; 26 | for ( int j = 0; j < bytes.length; j++ ) { 27 | int v = bytes[j] & 0xFF; 28 | hexChars[j * 2] = hexArray[v >>> 4]; 29 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 30 | } 31 | return new String(hexChars); 32 | } 33 | 34 | /** 35 | * Constant-time Byte Array Comparison 36 | * Less overheard, safer. Originally from: http://codahale.com/a-lesson-in-timing-attacks/ 37 | * 38 | * @param bytes yourByteArrayA 39 | * @param bytes yourByteArrayB 40 | * @return boolean 41 | * 42 | */ 43 | public static boolean isEqual(byte[] a, byte[] b) { 44 | if (a.length != b.length) { 45 | return false; 46 | } 47 | 48 | int result = 0; 49 | for (int i = 0; i < a.length; i++) { 50 | result |= a[i] ^ b[i]; 51 | } 52 | return result == 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justinribeiro/android-hostcardemulation-sample/a078d90ddd59b4ba4ad7cb52ce6cc30f21780405/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout-v21/activity_start.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 28 | 29 | 37 | 38 | 47 | 48 |