├── src └── android │ ├── hce_strings.xml │ ├── HCEPlugin.java │ ├── aid_list.xml │ └── CordovaApduService.java ├── LICENSE.txt ├── package.json ├── www ├── hce.js ├── hce-util.spec.js └── hce-util.js ├── plugin.xml └── README.md /src/android/hce_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CardEmulation Sample 5 | Sample Loyalty Card 6 | 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2015 Don Coleman 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-hce", 3 | "version": "1.0.0", 4 | "description": "Host Card Emulation (HCE) Plugin", 5 | "cordova": { 6 | "id": "cordova-plugin-hce", 7 | "platforms": [ 8 | "android" 9 | ] 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/don/cordova-plugin-hce.git" 14 | }, 15 | "keywords": [ 16 | "hce", 17 | "nfc", 18 | "host", 19 | "card", 20 | "emulation", 21 | "ecosystem:cordova", 22 | "cordova-android" 23 | ], 24 | "author": "Don Coleman ", 25 | "license": "Apache-2.0", 26 | "bugs": { 27 | "url": "https://github.com/don/cordova-plugin-hce/issues" 28 | }, 29 | "homepage": "https://github.com/don/cordova-plugin-hce#readme" 30 | } 31 | -------------------------------------------------------------------------------- /www/hce.js: -------------------------------------------------------------------------------- 1 | // Cordova HCE Plugin 2 | // (c) 2015 Don Coleman 3 | 4 | module.exports = { 5 | 6 | // Register to receive APDU commands from the remote device. 7 | // Commands will be sent as Uint8Array to the success callback. The success 8 | // callback is long lived and may be called many times. 9 | // Responses to commands should be sent using hce.sendResponse() 10 | // 11 | // http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#processCommandApdu(byte[], android.os.Bundle) 12 | registerCommandCallback: function(success, failure) { 13 | cordova.exec(success, failure, 'HCE', 'registerCommandCallback', []); 14 | }, 15 | 16 | // Send a response to the APDU Command 17 | // responseApdu should be a Uint8Array 18 | // http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#sendResponseApdu(byte[]) 19 | sendResponse: function(responseApdu, success, failure) { 20 | cordova.exec(success, failure, 'HCE', 'sendResponse', [responseApdu]); 21 | }, 22 | 23 | // Register to receive callback when host service is deactivated. 24 | // http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#onDeactivated(int) 25 | registerDeactivatedCallback: function(success, failure) { 26 | cordova.exec(success, failure, 'HCE', 'registerDeactivatedCallback', []); 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /www/hce-util.spec.js: -------------------------------------------------------------------------------- 1 | // Mocha Tests 2 | 3 | // Cordova HCE Plugin 4 | // (c) 2015 Don Coleman 5 | 6 | var assert = require('assert'); 7 | var util = require('./hce-util'); 8 | 9 | describe('HCE Util', function () { 10 | 11 | it('should convert hex strings to byte arrays', function () { 12 | var test = util.hexStringToByteArray('656667'); 13 | var expected = new Uint8Array([0x65, 0x66, 0x67]); 14 | assert.equal(expected.toString(), test.toString()); 15 | }); 16 | 17 | it('should convert byte arrays to hex strings', function () { 18 | var data = new Uint8Array([0x65, 0x66, 0x67]); 19 | assert.equal('656667', util.byteArrayToHexString(data)); 20 | }); 21 | 22 | it('should convert strings to byte arrays', function () { 23 | var test = util.stringToBytes('hello'); 24 | var expected = new Uint8Array([104, 101, 108, 108, 111]); 25 | assert.equal(expected.toString(), test.toString()); 26 | 27 | }); 28 | 29 | it('should combine buffers', function () { 30 | var a = new Uint8Array([63, 64]); 31 | var b = new Uint8Array([65, 66]); 32 | 33 | var expected = new Uint8Array([63,64,65,66]).toString(); 34 | 35 | var test = util.concatenateBuffers(a, b); 36 | assert.equal(expected, new Uint8Array(test).toString()); 37 | 38 | var test2 = util.concatenateBuffers(a.buffer, b.buffer); 39 | assert.equal(expected, new Uint8Array(test2).toString()); 40 | 41 | var test3 = util.concatenateBuffers(a.buffer, b); 42 | assert.equal(expected, new Uint8Array(test2).toString()); 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /www/hce-util.js: -------------------------------------------------------------------------------- 1 | // Cordova HCE Plugin 2 | // (c) 2015 Don Coleman 3 | 4 | function hexStringToByteArray(hexString) { 5 | var a = new Uint8Array(hexString.length/2); 6 | // split into chunks of 2 7 | if (hexString.length % 2 == 1) { 8 | throw('Hex string must have even number of characters'); 9 | } 10 | 11 | for (var i = 0; i < hexString.length; i += 2) { 12 | a[i/2] = parseInt(hexString.substring(i, i+2), 16); 13 | } 14 | 15 | return a; 16 | } 17 | 18 | // expecting Uint8Array or ArrayBuffer as input 19 | function byteArrayToHexString(a) { 20 | var s = ''; 21 | 22 | if (a instanceof ArrayBuffer) { 23 | a = new Uint8Array(a); 24 | } 25 | 26 | if (!(a instanceof Uint8Array)) { 27 | console.log('Expecting Uint8Array. Got ' + a); 28 | return s; 29 | } 30 | 31 | a.forEach(function(i) { 32 | s += toHex(i); 33 | }); 34 | return s; 35 | } 36 | 37 | // one byte unsigned int to padded hex string 38 | function toHex(i) { 39 | if (i < 0 || i > 255) { 40 | throw('Input must be between 0 and 255. Got ' + i + '.'); 41 | } 42 | 43 | return ('00' + i.toString(16)).substr(-2); 44 | } 45 | 46 | // borrowed phonegap-nfc 47 | function stringToBytes(string) { 48 | // based on http://ciaranj.blogspot.fr/2007/11/utf8-characters-encoding-in-javascript.html 49 | 50 | var bytes = []; 51 | 52 | for (var n = 0; n < string.length; n++) { 53 | 54 | var c = string.charCodeAt(n); 55 | 56 | if (c < 128) { 57 | 58 | bytes[bytes.length]= c; 59 | 60 | } else if((c > 127) && (c < 2048)) { 61 | 62 | bytes[bytes.length] = (c >> 6) | 192; 63 | bytes[bytes.length] = (c & 63) | 128; 64 | 65 | } else { 66 | 67 | bytes[bytes.length] = (c >> 12) | 224; 68 | bytes[bytes.length] = ((c >> 6) & 63) | 128; 69 | bytes[bytes.length] = (c & 63) | 128; 70 | 71 | } 72 | 73 | } 74 | 75 | return new Uint8Array(bytes); 76 | } 77 | 78 | // https://gist.github.com/72lions/4528834 79 | /** 80 | * Creates a new Uint8Array based on two different ArrayBuffers 81 | * 82 | * @private 83 | * @param {ArrayBuffers} buffer1 The first buffer. 84 | * @param {ArrayBuffers} buffer2 The second buffer. 85 | * @return {ArrayBuffers} The new ArrayBuffer created out of the two. 86 | */ 87 | var _appendBuffer = function(buffer1, buffer2) { 88 | var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); 89 | tmp.set(new Uint8Array(buffer1), 0); 90 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength); 91 | return tmp.buffer; 92 | }; 93 | 94 | module.exports = { 95 | hexStringToByteArray: hexStringToByteArray, 96 | byteArrayToHexString: byteArrayToHexString, 97 | stringToBytes: stringToBytes, 98 | concatenateBuffers: _appendBuffer 99 | }; 100 | -------------------------------------------------------------------------------- /src/android/HCEPlugin.java: -------------------------------------------------------------------------------- 1 | // Cordova HCE Plugin 2 | // (c) 2015 Don Coleman 3 | 4 | package com.megster.cordova.hce; 5 | 6 | import android.util.Log; 7 | 8 | import org.apache.cordova.CallbackContext; 9 | import org.apache.cordova.CordovaArgs; 10 | import org.apache.cordova.CordovaPlugin; 11 | import org.apache.cordova.PluginResult; 12 | import org.json.JSONException; 13 | 14 | import java.util.Arrays; 15 | 16 | public class HCEPlugin extends CordovaPlugin { 17 | 18 | private static final String REGISTER_COMMAND_CALLBACK = "registerCommandCallback"; 19 | private static final String SEND_RESPONSE = "sendResponse"; 20 | private static final String REGISTER_DEACTIVATED_CALLBACK = "registerDeactivatedCallback"; 21 | private static final String TAG = "HCEPlugin"; 22 | 23 | private CallbackContext onCommandCallback; 24 | private CallbackContext onDeactivatedCallback; 25 | 26 | @Override 27 | public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) throws JSONException { 28 | 29 | Log.d(TAG, action); 30 | 31 | if (action.equalsIgnoreCase(REGISTER_COMMAND_CALLBACK)) { 32 | 33 | // TODO this would be better in an initializer 34 | CordovaApduService.setHCEPlugin(this); 35 | 36 | // save the callback` 37 | onCommandCallback = callbackContext; 38 | PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); 39 | result.setKeepCallback(true); 40 | callbackContext.sendPluginResult(result); 41 | 42 | } else if (action.equalsIgnoreCase(SEND_RESPONSE)) { 43 | 44 | byte[] data = args.getArrayBuffer(0); 45 | 46 | if (CordovaApduService.sendResponse(data)) { 47 | callbackContext.success(); 48 | } else { 49 | // TODO This message won't make sense to developers. 50 | callbackContext.error("Missing Reference to CordovaApduService."); 51 | } 52 | 53 | } else if (action.equalsIgnoreCase(REGISTER_DEACTIVATED_CALLBACK)) { 54 | 55 | // save the callback` 56 | onDeactivatedCallback = callbackContext; 57 | PluginResult result = new PluginResult(PluginResult.Status.NO_RESULT); 58 | result.setKeepCallback(true); 59 | callbackContext.sendPluginResult(result); 60 | 61 | } else { 62 | 63 | return false; 64 | 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public void deactivated(int reason) { 71 | Log.d(TAG, "deactivated " + reason); 72 | if (onDeactivatedCallback != null) { 73 | PluginResult result = new PluginResult(PluginResult.Status.OK, reason); 74 | result.setKeepCallback(true); 75 | onDeactivatedCallback.sendPluginResult(result); 76 | } 77 | } 78 | 79 | public void sendCommand(byte[] command) { 80 | Log.d(TAG, "sendCommand " + Arrays.toString(command)); 81 | if (onCommandCallback != null) { 82 | PluginResult result = new PluginResult(PluginResult.Status.OK, command); 83 | result.setKeepCallback(true); 84 | onCommandCallback.sendPluginResult(result); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/android/aid_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 31 | 32 | 35 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | HCE 9 | Host Card Emulation (HCE) Plugin 10 | Apache 2.0 11 | hce, nfc, host, card, emulation 12 | 13 | https://github.com/don/cordova-plugin-hce.git 14 | https://github.com/don/cordova-plugin-hce/issues 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 54 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 66 | 67 | 69 | 70 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/android/CordovaApduService.java: -------------------------------------------------------------------------------- 1 | // Cordova HCE Plugin 2 | // (c) 2015 Don Coleman 3 | 4 | package com.megster.cordova.hce; 5 | 6 | import android.nfc.cardemulation.HostApduService; 7 | import android.os.Bundle; 8 | import android.util.Log; 9 | 10 | public class CordovaApduService extends HostApduService { 11 | 12 | private static final String TAG = "CordovaApduService"; 13 | 14 | // tight binding between the service and plugin 15 | // future versions could use bind 16 | private static HCEPlugin hcePlugin; 17 | private static CordovaApduService cordovaApduService; 18 | 19 | static void setHCEPlugin(HCEPlugin _hcePlugin) { 20 | hcePlugin = _hcePlugin; 21 | } 22 | 23 | static boolean sendResponse(byte[] data) { 24 | if (cordovaApduService != null) { 25 | cordovaApduService.sendResponseApdu(data); 26 | return true; 27 | } else { 28 | return false; 29 | } 30 | } 31 | 32 | /** 33 | * This method will be called when a command APDU has been received from a remote device. A 34 | * response APDU can be provided directly by returning a byte-array in this method. In general 35 | * response APDUs must be sent as quickly as possible, given the fact that the user is likely 36 | * holding his device over an NFC reader when this method is called. 37 | * 38 | *

If there are multiple services that have registered for the same AIDs in 39 | * their meta-data entry, you will only get called if the user has explicitly selected your 40 | * service, either as a default or just for the next tap. 41 | * 42 | *

This method is running on the main thread of your application. If you 43 | * cannot return a response APDU immediately, return null and use the {@link 44 | * #sendResponseApdu(byte[])} method later. 45 | * 46 | * @param commandApdu The APDU that received from the remote device 47 | * @param extras A bundle containing extra data. May be null. 48 | * @return a byte-array containing the response APDU, or null if no response APDU can be sent 49 | * at this point. 50 | */ 51 | @Override 52 | public byte[] processCommandApdu(byte[] commandApdu, Bundle extras) { 53 | Log.i(TAG, "Received APDU: " + ByteArrayToHexString(commandApdu)); 54 | 55 | // save a reference in static variable (hack) 56 | cordovaApduService = this; 57 | 58 | if (hcePlugin != null) { 59 | hcePlugin.sendCommand(commandApdu); 60 | } else { 61 | Log.e(TAG, "No reference to HCE Plugin."); 62 | } 63 | 64 | // return null since JavaScript code will send the response 65 | return null; 66 | } 67 | 68 | /** 69 | * Called if the connection to the NFC card is lost, in order to let the application know the 70 | * cause for the disconnection (either a lost link, or another AID being selected by the 71 | * reader). 72 | * 73 | * @param reason Either DEACTIVATION_LINK_LOSS or DEACTIVATION_DESELECTED 74 | */ 75 | @Override 76 | public void onDeactivated(int reason) { 77 | 78 | if (hcePlugin != null) { 79 | hcePlugin.deactivated(reason); 80 | } else { 81 | Log.e(TAG, "No reference to HCE Plugin."); 82 | } 83 | 84 | } 85 | 86 | /** 87 | * Utility method to convert a byte array to a hexadecimal string. 88 | * 89 | * @param bytes Bytes to convert 90 | * @return String, containing hexadecimal representation. 91 | */ 92 | public static String ByteArrayToHexString(byte[] bytes) { 93 | final char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'}; 94 | char[] hexChars = new char[bytes.length * 2]; // Each byte has two hex characters (nibbles) 95 | int v; 96 | for (int j = 0; j < bytes.length; j++) { 97 | v = bytes[j] & 0xFF; // Cast bytes[j] to int, treating as unsigned value 98 | hexChars[j * 2] = hexArray[v >>> 4]; // Select hex character from upper nibble 99 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; // Select hex character from lower nibble 100 | } 101 | return new String(hexChars); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordova HCE Plugin 2 | 3 | This plugin provides Host Card Emulation (HCE) for Apache Cordova. Host-based card emulation allows a Cordova application emulate a NFC Smart Card (without using the secure element) and talk directly to the NFC reader. 4 | 5 | This plugin provides a low-level access. The plugin receives commands as Uint8Arrays and expects responses to be Uint8Arrays. As a developer, you must implement higher level protocols based on your applications needs. 6 | 7 | Host Card Emulation requires NFC. 8 | 9 | ## Supported Platforms 10 | * Android (API Level 19 KitKat) 11 | 12 | ## Installing 13 | 14 | The AID for your application must be passed as a variable when installing the plugin. 15 | 16 | cordova plugin add cordova-plugin-hce --variable AID_FILTER=F222222222 17 | 18 | # HCE 19 | 20 | > The hce object provides functions that allow your application to emulate a smart card. 21 | 22 | ## Methods 23 | 24 | - [hce.registerCommandCallback](#hceregistercommandcallback) 25 | - [hce.sendResponse](#hcesendresponse) 26 | - [hce.registerDeactivatedCallback](#hceregisterdeactivatedcallback) 27 | 28 | ## hce.registerCommandCallback 29 | 30 | Register to receive APDU commands from the remote device. 31 | 32 | hce.registerCommandCallback(onCommand); 33 | 34 | #### Parameters 35 | - __success__: Success callback function that is invoked when an APDU command arrives. 36 | - __failure__: Error callback function, invoked when error occurs. [optional] 37 | 38 | 39 | #### Description 40 | Function `registerCommandCallback` allows your JavaScript code to handle APDU responses from the NFC reader. Commands will be sent as Uint8Array to the success callback. The success callback is long lived and may be called many times. 41 | 42 | Responses are sent back using `hce.sendResponse`. Android recommends "...response APDUs must be sent as quickly as possible, given the fact that the user is likely holding his device over an NFC reader when this method is called." For more info see [HostApduService.processCommandApdu](http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#processCommandApdu(byte[], android.os.Bundle)). 43 | 44 | #### Quick Example 45 | 46 | hce.registerCommandCallback(onCommand); 47 | 48 | var onCommand = function(command) { 49 | 50 | var commandAsBytes = new Uint8Array(command); 51 | var commandAsString = hce.util.byteArrayToHexString(commandAsBytes); 52 | 53 | // do something with the command 54 | 55 | // send the response 56 | hce.sendReponse(commandResponse); 57 | } 58 | 59 | 60 | ## hce.sendResponse 61 | Sends a response APDU back to the remote device. 62 | 63 | hce.sendResponse(responseApdu, success); 64 | 65 | #### Parameters 66 | - __responseApdu__: Response for NFC reader. Should be a Uint8Array. 67 | - __success__: Success callback function that is invoked when an APDU command arrives. 68 | - __failure__: Error callback function, invoked when error occurs. [optional] 69 | 70 | #### Description 71 | Function `sendResponse` is intended to be called from within the success handler of `hce.registerCommandCallback`. Response commands should be sent a Uint8Array. 72 | 73 | See [HostApduService.sendResponseApdu](http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#sendResponseApdu(byte[])). 74 | 75 | 76 | ## hce.registerDeactivatedCallback 77 | Register to receive callback when host service is deactivated. 78 | 79 | hce.registerDeactivatedCallback(onDeactivated); 80 | 81 | #### Parameters 82 | 83 | - __success__: Success callback function that is invoked when the service is deactivated. 84 | - __failure__: Error callback function, invoked when error occurs. [optional] 85 | 86 | #### Description 87 | Function `registerDeactivatedCallback` allows the plugin to be notified when the host service is deactivated. A reason code is passed to the success callback. 88 | 89 | See [HostApduService.onDeactivated](http://developer.android.com/reference/android/nfc/cardemulation/HostApduService.html#onDeactivated(int)). 90 | 91 | #### Quick Example 92 | 93 | hce.registerDeactivatedCallback(onDeactivated); 94 | 95 | var onDeactivated = function(reason) { 96 | console.log("Deactivated. Reason code = " + reason); 97 | } 98 | 99 | # HCE Util 100 | 101 | > The hce.util object provides utility function for APDU operations. 102 | 103 | - hexStringToByteArray - convert hex string to ArrayBuffer 104 | - byteArrayToHexString - convert ArrayBuffer to hex string 105 | - stringToBytes - convert ascii string into ArrayBuffer 106 | - concatenateBuffers - concatenate two ArrayBuffer together, returning a new ArrayBuffer 107 | 108 | ## Android HCE documentation 109 | 110 | This plugin is a wrapper around the Android HCE functionality. Reference the [Android HCE documentation](http://developer.android.com/guide/topics/connectivity/nfc/hce.html) for more info. 111 | 112 | ## HCE Demo App 113 | See the [HCE Demo application](http://github.com/don/cordova-hce-demo) for one possible use of this plugin. The HCE demo application duplicates Android's [card emulation example](http://developer.android.com/samples/CardEmulation/index.html) in Cordova and is intended to work with the Android [card reader example](http://developer.android.com/samples/CardReader/index.html). 114 | 115 | # License 116 | 117 | Apache 2.0 118 | --------------------------------------------------------------------------------