",
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 |
--------------------------------------------------------------------------------