@icedevml/react-native-host-card-emulation 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -7
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/IntentKeys.java +7 -4
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +119 -85
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +0 -4
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/service/HCEService.java +90 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/{BinaryUtils.java → util/BinaryUtils.java} +1 -1
- package/ios/RTNHCEModuleIOSImpl.swift +10 -8
- package/package.json +1 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/AESGCMUtil.java +0 -89
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +0 -104
package/README.md
CHANGED
|
@@ -23,7 +23,17 @@ Make sure that your project has it enabled. Note that the New Architecture is en
|
|
|
23
23
|
*(no prerequisite steps are required for Android)*
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
|
-
Install
|
|
26
|
+
Install this package within your React Native project:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
yarn add @icedevml/react-native-host-card-emulation
|
|
30
|
+
```
|
|
31
|
+
or using npm
|
|
32
|
+
```
|
|
33
|
+
npm install --save @icedevml/react-native-host-card-emulation
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Afterwards, follow the subsections below for each platform that you need to support.
|
|
27
37
|
|
|
28
38
|
### iOS
|
|
29
39
|
1. Make sure to set up a proper signing team and bundle identifier in "Signing & Capabilities" configuration section of your project.
|
|
@@ -54,7 +64,9 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
54
64
|
...
|
|
55
65
|
|
|
56
66
|
+ <uses-permission android:name="android.permission.NFC" />
|
|
57
|
-
+ <uses-feature android:name="android.hardware.nfc.hce" android:required="
|
|
67
|
+
+ <uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
|
|
68
|
+
+ <permission android:name="com.itsecrnd.rtnhceandroid.HCE_BROADCAST" android:protectionLevel="signature" />
|
|
69
|
+
+ <uses-permission android:name="com.itsecrnd.rtnhceandroid.HCE_BROADCAST"/>
|
|
58
70
|
|
|
59
71
|
...
|
|
60
72
|
|
|
@@ -65,10 +77,10 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
65
77
|
...
|
|
66
78
|
|
|
67
79
|
+ <service
|
|
68
|
-
+ android:name="com.itsecrnd.rtnhceandroid.HCEService"
|
|
80
|
+
+ android:name="com.itsecrnd.rtnhceandroid.service.HCEService"
|
|
69
81
|
+ android:exported="true"
|
|
70
|
-
+ android:enabled="
|
|
71
|
-
+ android:permission="android.permission.BIND_NFC_SERVICE"
|
|
82
|
+
+ android:enabled="true"
|
|
83
|
+
+ android:permission="android.permission.BIND_NFC_SERVICE">
|
|
72
84
|
+ <intent-filter>
|
|
73
85
|
+ <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
|
|
74
86
|
+ <category android:name="android.intent.category.DEFAULT"/>
|
|
@@ -98,5 +110,92 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
98
110
|
|
|
99
111
|
## API Specification & Demo App
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
114
|
+
|
|
115
|
+
> [!NOTE]
|
|
116
|
+
> Raw native module's API specification is available in [packages/native-hce-module/js/NativeHCEModule.ts](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts).
|
|
117
|
+
> Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
|
|
118
|
+
|
|
119
|
+
> [!NOTE]
|
|
120
|
+
> See [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx) example for more insignt about the library's API.
|
|
121
|
+
|
|
122
|
+
### Quick start guide
|
|
123
|
+
|
|
124
|
+
1. Subscribe to the event stream. There is a single global event stream, so you can subscribe early and remain subscribed to events for the entire application's lifetime.
|
|
125
|
+
```typescript
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const removeListener = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
|
|
128
|
+
switch (event.type) {
|
|
129
|
+
/* ... implement event handlers here ... */
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// cleanup the event listener when the effect is unmounted
|
|
134
|
+
return () => {
|
|
135
|
+
removeListener();
|
|
136
|
+
}
|
|
137
|
+
}, []);
|
|
138
|
+
```
|
|
139
|
+
2. When user indicates that he/she wants to perform the HCE action, call the following function from the button's onClick routine:
|
|
140
|
+
```typescript
|
|
141
|
+
await NativeHCEModule?.beginSession();
|
|
142
|
+
```
|
|
143
|
+
This will emit `sessionStarted` event right after the function call, on both iOS and Android platforms. After the session is started, you can decide to start HCE emulation right away in the event handler:
|
|
144
|
+
```typescript
|
|
145
|
+
// inside NativeHCEModule?.onEvent handler's switch
|
|
146
|
+
case 'sessionStarted':
|
|
147
|
+
NativeHCEModule?.setSessionAlertMessage('Tap towards the reader'); // only for iOS, no-op in Android
|
|
148
|
+
await NativeHCEModule?.startHCE();
|
|
149
|
+
break;
|
|
150
|
+
```
|
|
151
|
+
Calling `await NativeHCEModule?.startHCE()` causes:
|
|
152
|
+
* iOS: Your smartphone will start listening for C-APDUs (Command APDUs originating from reader devices) right after that function is called, which will be additionally indicated by the operating system popping out the NFC scanning user interface prompt.
|
|
153
|
+
* Android: HCE commands will be forwarded to your application from that point on. No specific user interface is displayed (you have to implement it on your own, within your app).
|
|
154
|
+
3. Optionally, you can handle `readerDetected` and `readerDeselected` events to enhance user's experience.
|
|
155
|
+
```typescript
|
|
156
|
+
// inside NativeHCEModule?.onEvent handler's switch
|
|
157
|
+
case 'readerDetected':
|
|
158
|
+
NativeHCEModule?.setSessionAlertMessage('Reader detected'); // only for iOS, no-op in Android
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'readerDeselected':
|
|
162
|
+
NativeHCEModule?.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
|
|
163
|
+
break;
|
|
164
|
+
```
|
|
165
|
+
For those events, trigger mechanisms are platform dependent:
|
|
166
|
+
* iOS: The `readerDetected` event will be emitted as soon as the NFC tag field presence is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
|
|
167
|
+
* Android: The `readerDetected` event will be emitted as soon as the first matching SELECT AID command is observed. The `readerDeselected` event will be emitted if the reader is physically disconnected or a non-matching AID is selected by the reader.
|
|
168
|
+
4. Receive incoming C-APDU and respond to it:
|
|
169
|
+
```typescript
|
|
170
|
+
// inside NativeHCEModule?.onEvent handler's switch
|
|
171
|
+
case 'received':
|
|
172
|
+
NativeHCEModule?.setSessionAlertMessage('Keep holding the tag'); // only for iOS, no-op on Android
|
|
173
|
+
|
|
174
|
+
// decode incoming C-APDU to bytes
|
|
175
|
+
const capdu = Buffer.from(event.arg!, 'hex');
|
|
176
|
+
console.log('Received C-APDU, capdu.toString('hex'));
|
|
177
|
+
|
|
178
|
+
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
179
|
+
await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
180
|
+
break;
|
|
181
|
+
```
|
|
182
|
+
You don't have to respond to the APDU right away from within the event handler, but please remember that the reader might time out if you will be lingering with the response for too long.
|
|
183
|
+
|
|
184
|
+
### iOS: Acquiring exclusive NFC access
|
|
185
|
+
If you need to utilize `NFCPresentmentIntentAssertion` for enhanced user experience, call:
|
|
186
|
+
```typescript
|
|
187
|
+
NativeHCEModule?.acquireExclusiveNFC();
|
|
188
|
+
```
|
|
189
|
+
This function will acquire an exclusive NFC access for 15 seconds. On system services or other applications will be able to interfere with NFC during that period. For example, the NFC background tag reading will be disabled so it would not generate any distracting notifications.
|
|
190
|
+
|
|
191
|
+
This function will throw an exception if:
|
|
192
|
+
* the presentment intent assertion was already acquired and is still active;
|
|
193
|
+
* you are in the cooldown period where you are not allowed to acquire the presentment intent assertion (cooldown is 15 seconds after the previous assertion had expired);
|
|
194
|
+
* the feature is not supported or the device is not eligible for whatever reason;
|
|
195
|
+
|
|
196
|
+
Call `NativeHCEModule?.isExclusiveNFC()` to check if exclusive NFC access is still active.
|
|
197
|
+
|
|
198
|
+
### More resources
|
|
199
|
+
|
|
200
|
+
* [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
|
|
201
|
+
* [Demo App's Code (Example Library Usage)](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/demo-hce-module-app/App.tsx)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
package com.itsecrnd.rtnhceandroid;
|
|
2
2
|
|
|
3
3
|
public class IntentKeys {
|
|
4
|
-
public static final String
|
|
5
|
-
public static final String
|
|
6
|
-
public static final String
|
|
7
|
-
public static final String
|
|
4
|
+
public static final String PERMISSION_HCE_BROADCAST = "com.itsecrnd.rtnhceandroid.HCE_BROADCAST";
|
|
5
|
+
public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
|
|
6
|
+
public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
|
|
7
|
+
public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
|
|
8
|
+
public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
|
|
9
|
+
public static final String KEY_CAPDU = "capdu";
|
|
10
|
+
public static final String KEY_RAPDU = "rapdu";
|
|
8
11
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
package com.itsecrnd.rtnhceandroid;
|
|
2
2
|
|
|
3
3
|
import static android.content.Context.RECEIVER_EXPORTED;
|
|
4
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.
|
|
5
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.
|
|
6
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.
|
|
7
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.
|
|
4
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
|
|
5
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
|
|
6
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
|
|
7
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
|
|
8
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.PERMISSION_HCE_BROADCAST;
|
|
8
9
|
|
|
9
10
|
import android.content.BroadcastReceiver;
|
|
10
|
-
import android.content.ComponentName;
|
|
11
11
|
import android.content.Context;
|
|
12
12
|
import android.content.Intent;
|
|
13
13
|
import android.content.IntentFilter;
|
|
@@ -23,27 +23,14 @@ import com.facebook.react.bridge.Promise;
|
|
|
23
23
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
24
24
|
import com.facebook.react.bridge.WritableMap;
|
|
25
25
|
|
|
26
|
-
import javax.crypto.SecretKey;
|
|
27
|
-
|
|
28
|
-
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
29
26
|
public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
30
27
|
public static final String TAG = "RTNHCEAndroidModule";
|
|
31
28
|
public static final String NAME = "NativeHCEModule";
|
|
32
29
|
|
|
33
|
-
private
|
|
34
|
-
private boolean
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
getReactApplicationContext()
|
|
38
|
-
.getPackageManager()
|
|
39
|
-
.setComponentEnabledSetting(
|
|
40
|
-
new ComponentName(getReactApplicationContext(), HCEService.class),
|
|
41
|
-
enabled
|
|
42
|
-
? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
43
|
-
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
44
|
-
PackageManager.DONT_KILL_APP
|
|
45
|
-
);
|
|
46
|
-
}
|
|
30
|
+
private volatile boolean sessionRunning;
|
|
31
|
+
private volatile boolean hceRunning;
|
|
32
|
+
private volatile boolean hceBreakConnection;
|
|
33
|
+
private volatile boolean hceDeselected;
|
|
47
34
|
|
|
48
35
|
private void sendEvent(final String type, final String arg) {
|
|
49
36
|
WritableMap map = Arguments.createMap();
|
|
@@ -52,23 +39,46 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
52
39
|
emitOnEvent(map);
|
|
53
40
|
}
|
|
54
41
|
|
|
42
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
55
43
|
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
56
44
|
@Override
|
|
57
45
|
public void onReceive(Context context, Intent intent) {
|
|
58
46
|
try {
|
|
59
47
|
String action = intent.getAction();
|
|
60
48
|
|
|
61
|
-
if (action
|
|
62
|
-
|
|
49
|
+
if (action == null) {
|
|
50
|
+
Log.w(TAG, "Received intent with null action in module receiver.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
63
53
|
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
if (action.equals(ACTION_RECEIVE_C_APDU)) {
|
|
55
|
+
String capdu = intent.getStringExtra("capdu");
|
|
56
|
+
|
|
57
|
+
if (!hceBreakConnection && hceRunning) {
|
|
66
58
|
sendEvent("received", capdu);
|
|
67
|
-
} else
|
|
59
|
+
} else {
|
|
60
|
+
// HCE is already disabled or was just disabled while the HCEService was running
|
|
61
|
+
// don't forward C-APDUs to the application and just respond 6999 to everything
|
|
62
|
+
Intent rintent = new Intent(ACTION_SEND_R_APDU);
|
|
63
|
+
rintent.setPackage(getReactApplicationContext().getPackageName());
|
|
64
|
+
rintent.putExtra("rapdu", "6999");
|
|
65
|
+
getReactApplicationContext().getApplicationContext().sendBroadcast(rintent, PERMISSION_HCE_BROADCAST);
|
|
66
|
+
}
|
|
67
|
+
} else if (action.equals(ACTION_READER_DETECT)) {
|
|
68
|
+
if (hceRunning) {
|
|
69
|
+
hceDeselected = false;
|
|
68
70
|
sendEvent("readerDetected", "");
|
|
69
|
-
}
|
|
71
|
+
}
|
|
72
|
+
} else if (action.equals(ACTION_READER_LOST)) {
|
|
73
|
+
if (hceRunning && !hceBreakConnection && !hceDeselected) {
|
|
74
|
+
// only if we haven't send a fake disconnect event already
|
|
75
|
+
hceDeselected = true;
|
|
70
76
|
sendEvent("readerDeselected", "");
|
|
71
77
|
}
|
|
78
|
+
|
|
79
|
+
hceBreakConnection = false;
|
|
80
|
+
} else {
|
|
81
|
+
Log.w(TAG, "Received unknown action intent in module receiver: " + action);
|
|
72
82
|
}
|
|
73
83
|
} catch (Exception e) {
|
|
74
84
|
Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
|
|
@@ -79,40 +89,32 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
79
89
|
RTNHCEAndroidModule(ReactApplicationContext context) {
|
|
80
90
|
super(context);
|
|
81
91
|
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
this.sessionRunning = false;
|
|
93
|
+
this.hceRunning = false;
|
|
94
|
+
this.hceBreakConnection = false;
|
|
95
|
+
this.hceDeselected = true;
|
|
96
|
+
|
|
97
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
98
|
+
IntentFilter filter = new IntentFilter();
|
|
99
|
+
filter.addAction(ACTION_RECEIVE_C_APDU);
|
|
100
|
+
filter.addAction(ACTION_READER_DETECT);
|
|
101
|
+
filter.addAction(ACTION_READER_LOST);
|
|
102
|
+
|
|
103
|
+
getReactApplicationContext().getApplicationContext().registerReceiver(
|
|
104
|
+
receiver,
|
|
105
|
+
filter,
|
|
106
|
+
PERMISSION_HCE_BROADCAST,
|
|
107
|
+
null,
|
|
108
|
+
RECEIVER_EXPORTED
|
|
109
|
+
);
|
|
91
110
|
}
|
|
111
|
+
}
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.commit();
|
|
99
|
-
|
|
100
|
-
IntentFilter filter = new IntentFilter();
|
|
101
|
-
filter.addAction(INTENT_RECEIVE_C_APDU);
|
|
102
|
-
filter.addAction(INTENT_READER_DETECT);
|
|
103
|
-
filter.addAction(INTENT_READER_LOST);
|
|
104
|
-
|
|
105
|
-
/*
|
|
106
|
-
* FIXME It seems like those intents are not going through across this module and HostApduService.
|
|
107
|
-
* This happens even though those things are within the same application and package.
|
|
108
|
-
* It also seems like there is no way to bind HostApduService (or it's a skill issue).
|
|
109
|
-
*
|
|
110
|
-
* For now we will just encrypt&authenticate all messages using AES/GCM and use exported
|
|
111
|
-
* receivers. This is not a perfect solution but it is going to be secure and sufficient for now.
|
|
112
|
-
*
|
|
113
|
-
* If somebody knows a better way then any pull requests would be greatly appreciated.
|
|
114
|
-
*/
|
|
115
|
-
getReactApplicationContext().getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
|
|
113
|
+
@Override
|
|
114
|
+
public void invalidate() {
|
|
115
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
116
|
+
getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
|
|
117
|
+
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
@Override
|
|
@@ -123,9 +125,13 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
123
125
|
|
|
124
126
|
@Override
|
|
125
127
|
public boolean isPlatformSupported() {
|
|
128
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
126
132
|
PackageManager pm = getReactApplicationContext().getPackageManager();
|
|
127
133
|
return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
|
|
128
|
-
|
|
134
|
+
&& pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
@Override
|
|
@@ -135,18 +141,23 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
135
141
|
|
|
136
142
|
@Override
|
|
137
143
|
public boolean isExclusiveNFC() {
|
|
138
|
-
// unsupported on Android
|
|
144
|
+
// unsupported on Android, always false
|
|
139
145
|
return false;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
@Override
|
|
143
149
|
public void beginSession(Promise promise) {
|
|
144
|
-
if (
|
|
145
|
-
promise.reject("
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
150
|
+
if (!isPlatformSupported()) {
|
|
151
|
+
promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!this.sessionRunning) {
|
|
156
|
+
this.sessionRunning = true;
|
|
149
157
|
sendEvent("sessionStarted", "");
|
|
158
|
+
promise.resolve(null);
|
|
159
|
+
} else {
|
|
160
|
+
promise.reject("err_card_session_exists", "Session already exists.");
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
163
|
|
|
@@ -157,51 +168,74 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
157
168
|
|
|
158
169
|
@Override
|
|
159
170
|
public void invalidateSession() {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
171
|
+
if (this.sessionRunning) {
|
|
172
|
+
this.sessionRunning = false;
|
|
173
|
+
sendEvent("sessionInvalidated", "userInvalidated");
|
|
174
|
+
}
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
@Override
|
|
166
178
|
public boolean isSessionRunning() {
|
|
167
|
-
return this.
|
|
179
|
+
return this.sessionRunning;
|
|
168
180
|
}
|
|
169
181
|
|
|
170
182
|
@Override
|
|
171
183
|
public void startHCE(Promise promise) {
|
|
172
|
-
|
|
184
|
+
if (!this.sessionRunning) {
|
|
185
|
+
promise.reject("err_no_session", "No session is active.");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (this.hceRunning) {
|
|
190
|
+
promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.hceRunning = true;
|
|
173
195
|
promise.resolve(null);
|
|
174
196
|
}
|
|
175
197
|
|
|
176
198
|
@Override
|
|
177
199
|
public void stopHCE(String status, Promise promise) {
|
|
178
|
-
|
|
200
|
+
if (!this.sessionRunning) {
|
|
201
|
+
promise.reject("err_no_session", "No session is active.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (this.hceRunning) {
|
|
206
|
+
this.hceRunning = false;
|
|
207
|
+
|
|
208
|
+
if (!this.hceDeselected) {
|
|
209
|
+
this.hceDeselected = true;
|
|
210
|
+
this.hceBreakConnection = true;
|
|
211
|
+
sendEvent("readerDeselected", "");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
179
215
|
promise.resolve(null);
|
|
180
216
|
}
|
|
181
217
|
|
|
218
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
182
219
|
@Override
|
|
183
220
|
public void respondAPDU(String rapdu, Promise promise) {
|
|
184
|
-
if (!this.
|
|
221
|
+
if (!this.sessionRunning) {
|
|
222
|
+
promise.reject("err_no_session", "No session is active.");
|
|
223
|
+
return;
|
|
224
|
+
} else if (!this.hceRunning) {
|
|
225
|
+
promise.reject("err_no_hce", "HCE is not running.");
|
|
185
226
|
return;
|
|
186
227
|
}
|
|
187
228
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
Intent intent = new Intent(INTENT_SEND_R_APDU);
|
|
229
|
+
Intent intent = new Intent(ACTION_SEND_R_APDU);
|
|
191
230
|
intent.setPackage(getReactApplicationContext().getPackageName());
|
|
192
|
-
intent.putExtra("
|
|
193
|
-
|
|
194
|
-
getReactApplicationContext().getApplicationContext().sendBroadcast(intent);
|
|
231
|
+
intent.putExtra("rapdu", rapdu);
|
|
232
|
+
getReactApplicationContext().getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
195
233
|
|
|
196
234
|
promise.resolve(null);
|
|
197
235
|
}
|
|
198
236
|
|
|
199
237
|
@Override
|
|
200
238
|
public void isHCERunning(Promise promise) {
|
|
201
|
-
|
|
202
|
-
.getPackageManager()
|
|
203
|
-
.getComponentEnabledSetting(new ComponentName(getReactApplicationContext(), HCEService.class));
|
|
204
|
-
|
|
205
|
-
promise.resolve(enabledSetting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED);
|
|
239
|
+
promise.resolve(this.hceRunning);
|
|
206
240
|
}
|
|
207
241
|
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
package com.itsecrnd.rtnhceandroid;
|
|
2
2
|
|
|
3
|
-
import android.os.Build;
|
|
4
|
-
|
|
5
3
|
import androidx.annotation.Nullable;
|
|
6
|
-
import androidx.annotation.RequiresApi;
|
|
7
4
|
|
|
8
5
|
import com.facebook.react.TurboReactPackage;
|
|
9
6
|
import com.facebook.react.bridge.NativeModule;
|
|
@@ -14,7 +11,6 @@ import com.facebook.react.module.model.ReactModuleInfoProvider;
|
|
|
14
11
|
import java.util.HashMap;
|
|
15
12
|
import java.util.Map;
|
|
16
13
|
|
|
17
|
-
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
18
14
|
public class RTNHCEAndroidPackage extends TurboReactPackage {
|
|
19
15
|
@Nullable
|
|
20
16
|
@Override
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package com.itsecrnd.rtnhceandroid.service;
|
|
2
|
+
|
|
3
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
|
|
4
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
|
|
5
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
|
|
6
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
|
|
7
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_CAPDU;
|
|
8
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
|
|
9
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.PERMISSION_HCE_BROADCAST;
|
|
10
|
+
|
|
11
|
+
import android.content.BroadcastReceiver;
|
|
12
|
+
import android.content.Context;
|
|
13
|
+
import android.content.Intent;
|
|
14
|
+
import android.content.IntentFilter;
|
|
15
|
+
import android.nfc.cardemulation.HostApduService;
|
|
16
|
+
import android.os.Build;
|
|
17
|
+
import android.os.Bundle;
|
|
18
|
+
import android.util.Log;
|
|
19
|
+
|
|
20
|
+
import androidx.annotation.RequiresApi;
|
|
21
|
+
|
|
22
|
+
import com.itsecrnd.rtnhceandroid.util.BinaryUtils;
|
|
23
|
+
|
|
24
|
+
import java.util.Locale;
|
|
25
|
+
|
|
26
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
27
|
+
public class HCEService extends HostApduService {
|
|
28
|
+
private static final String TAG = "HCEService";
|
|
29
|
+
|
|
30
|
+
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
31
|
+
@Override
|
|
32
|
+
public void onReceive(Context context, Intent intent) {
|
|
33
|
+
try {
|
|
34
|
+
String action = intent.getAction();
|
|
35
|
+
|
|
36
|
+
if (action != null && action.equals(ACTION_SEND_R_APDU)) {
|
|
37
|
+
String rapdu = intent.getStringExtra(KEY_RAPDU);
|
|
38
|
+
|
|
39
|
+
if (rapdu == null) {
|
|
40
|
+
throw new RuntimeException("Bug! ACTION_SEND_RAPDU intent KEY_RAPDU is null.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
|
|
44
|
+
sendResponseApdu(dec);
|
|
45
|
+
} else {
|
|
46
|
+
Log.w(TAG, "Received unknown action: " + action);
|
|
47
|
+
}
|
|
48
|
+
} catch (Exception e) {
|
|
49
|
+
Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
@Override
|
|
55
|
+
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
56
|
+
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
57
|
+
|
|
58
|
+
Intent intent = new Intent(ACTION_RECEIVE_C_APDU);
|
|
59
|
+
intent.setPackage(getApplicationContext().getPackageName());
|
|
60
|
+
intent.putExtra(KEY_CAPDU, capdu);
|
|
61
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Override
|
|
67
|
+
public void onCreate() {
|
|
68
|
+
Log.d(TAG, "HCEService onCreate");
|
|
69
|
+
|
|
70
|
+
IntentFilter filter = new IntentFilter();
|
|
71
|
+
filter.addAction(ACTION_SEND_R_APDU);
|
|
72
|
+
getApplicationContext().registerReceiver(
|
|
73
|
+
receiver, filter, PERMISSION_HCE_BROADCAST, null, RECEIVER_EXPORTED);
|
|
74
|
+
|
|
75
|
+
Intent intent = new Intent(ACTION_READER_DETECT);
|
|
76
|
+
intent.setPackage(getApplicationContext().getPackageName());
|
|
77
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Override
|
|
81
|
+
public void onDeactivated(int reason) {
|
|
82
|
+
Log.d(TAG, "HCEService onDeactivated: " + reason);
|
|
83
|
+
|
|
84
|
+
getApplicationContext().unregisterReceiver(receiver);
|
|
85
|
+
|
|
86
|
+
Intent intent = new Intent(ACTION_READER_LOST);
|
|
87
|
+
intent.setPackage(getApplicationContext().getPackageName());
|
|
88
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -13,13 +13,8 @@ extension StringProtocol {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
extension Data {
|
|
16
|
-
|
|
17
|
-
let
|
|
18
|
-
static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
func hexEncodedString(options: HexEncodingOptions = []) -> String {
|
|
22
|
-
let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
|
|
16
|
+
func hexEncodedString() -> String {
|
|
17
|
+
let format = "%02hhX"
|
|
23
18
|
return map { String(format: format, $0) }.joined()
|
|
24
19
|
}
|
|
25
20
|
}
|
|
@@ -170,7 +165,14 @@ extension Data {
|
|
|
170
165
|
|
|
171
166
|
@objc public func startHCE(resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
|
|
172
167
|
Task.init {
|
|
173
|
-
|
|
168
|
+
do {
|
|
169
|
+
try await self.cardSession?.startEmulation()
|
|
170
|
+
} catch(let error) {
|
|
171
|
+
reject("err_start_emulation", "Error trying to start emulation. \(error)", nil)
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
resolve(nil)
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
package/package.json
CHANGED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
package com.itsecrnd.rtnhceandroid;
|
|
2
|
-
|
|
3
|
-
import android.annotation.SuppressLint;
|
|
4
|
-
|
|
5
|
-
import java.nio.charset.StandardCharsets;
|
|
6
|
-
import java.security.InvalidAlgorithmParameterException;
|
|
7
|
-
import java.security.InvalidKeyException;
|
|
8
|
-
import java.security.NoSuchAlgorithmException;
|
|
9
|
-
import java.security.SecureRandom;
|
|
10
|
-
import java.util.Base64;
|
|
11
|
-
|
|
12
|
-
import javax.crypto.BadPaddingException;
|
|
13
|
-
import javax.crypto.Cipher;
|
|
14
|
-
import javax.crypto.IllegalBlockSizeException;
|
|
15
|
-
import javax.crypto.KeyGenerator;
|
|
16
|
-
import javax.crypto.NoSuchPaddingException;
|
|
17
|
-
import javax.crypto.SecretKey;
|
|
18
|
-
import javax.crypto.spec.GCMParameterSpec;
|
|
19
|
-
|
|
20
|
-
@SuppressLint("NewApi")
|
|
21
|
-
public class AESGCMUtil {
|
|
22
|
-
public static String randomString() {
|
|
23
|
-
byte[] rnd = new byte[32];
|
|
24
|
-
new SecureRandom().nextBytes(rnd);
|
|
25
|
-
return BinaryUtils.ByteArrayToHexString(rnd);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public static SecretKey generateKey() {
|
|
29
|
-
KeyGenerator keyGen;
|
|
30
|
-
try {
|
|
31
|
-
keyGen = KeyGenerator.getInstance("AES");
|
|
32
|
-
} catch (NoSuchAlgorithmException e) {
|
|
33
|
-
throw new RuntimeException("Failed to get AES algorithm.", e);
|
|
34
|
-
}
|
|
35
|
-
keyGen.init(256);
|
|
36
|
-
return keyGen.generateKey();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
public static String encryptData(SecretKey key, String plaintext) {
|
|
40
|
-
try {
|
|
41
|
-
byte[] iv = new byte[32];
|
|
42
|
-
new SecureRandom().nextBytes(iv);
|
|
43
|
-
|
|
44
|
-
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
45
|
-
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
|
|
46
|
-
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
|
|
47
|
-
|
|
48
|
-
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
|
|
49
|
-
byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
|
|
50
|
-
|
|
51
|
-
return Base64.getEncoder().encodeToString(iv) + ';' +
|
|
52
|
-
Base64.getEncoder().encodeToString(ciphertextBytes);
|
|
53
|
-
} catch (NoSuchPaddingException
|
|
54
|
-
| NoSuchAlgorithmException
|
|
55
|
-
| IllegalBlockSizeException
|
|
56
|
-
| InvalidAlgorithmParameterException
|
|
57
|
-
| BadPaddingException
|
|
58
|
-
| InvalidKeyException e) {
|
|
59
|
-
throw new RuntimeException("Crypto error.", e);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public static String decryptData(SecretKey key, String ciphertext) {
|
|
64
|
-
try {
|
|
65
|
-
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
|
66
|
-
String[] parts = ciphertext.split(";");
|
|
67
|
-
|
|
68
|
-
if (parts.length != 2) {
|
|
69
|
-
throw new IllegalArgumentException("Invalid ciphertext parameter.");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
byte[] iv = Base64.getDecoder().decode(parts[0]);
|
|
73
|
-
byte[] ciphertextBytes = Base64.getDecoder().decode(parts[1]);
|
|
74
|
-
|
|
75
|
-
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);
|
|
76
|
-
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
|
|
77
|
-
|
|
78
|
-
byte[] plaintextBytes = cipher.doFinal(ciphertextBytes);
|
|
79
|
-
return new String(plaintextBytes, StandardCharsets.UTF_8);
|
|
80
|
-
} catch (NoSuchPaddingException
|
|
81
|
-
| IllegalBlockSizeException
|
|
82
|
-
| InvalidAlgorithmParameterException
|
|
83
|
-
| BadPaddingException
|
|
84
|
-
| InvalidKeyException
|
|
85
|
-
| NoSuchAlgorithmException e) {
|
|
86
|
-
throw new RuntimeException("Crypto error.", e);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
package com.itsecrnd.rtnhceandroid;
|
|
2
|
-
|
|
3
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_DETECT;
|
|
4
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_READER_LOST;
|
|
5
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_RECEIVE_C_APDU;
|
|
6
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.INTENT_SEND_R_APDU;
|
|
7
|
-
|
|
8
|
-
import android.content.BroadcastReceiver;
|
|
9
|
-
import android.content.Context;
|
|
10
|
-
import android.content.Intent;
|
|
11
|
-
import android.content.IntentFilter;
|
|
12
|
-
import android.nfc.cardemulation.HostApduService;
|
|
13
|
-
import android.os.Build;
|
|
14
|
-
import android.os.Bundle;
|
|
15
|
-
import android.util.Log;
|
|
16
|
-
|
|
17
|
-
import androidx.annotation.RequiresApi;
|
|
18
|
-
|
|
19
|
-
import java.util.Locale;
|
|
20
|
-
|
|
21
|
-
import javax.crypto.SecretKey;
|
|
22
|
-
import javax.crypto.spec.SecretKeySpec;
|
|
23
|
-
|
|
24
|
-
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
25
|
-
public class HCEService extends HostApduService {
|
|
26
|
-
private static final String TAG = "HCEService";
|
|
27
|
-
|
|
28
|
-
private SecretKey encSecretKey;
|
|
29
|
-
|
|
30
|
-
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
31
|
-
@Override
|
|
32
|
-
public void onReceive(Context context, Intent intent) {
|
|
33
|
-
try {
|
|
34
|
-
String action = intent.getAction();
|
|
35
|
-
|
|
36
|
-
if (action != null) {
|
|
37
|
-
AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
|
|
38
|
-
|
|
39
|
-
if (action.equals(INTENT_SEND_R_APDU)) {
|
|
40
|
-
String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("rapdu"));
|
|
41
|
-
byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
|
|
42
|
-
sendResponseApdu(dec);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
} catch (Exception e) {
|
|
46
|
-
Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
@Override
|
|
52
|
-
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
53
|
-
String capdu = AESGCMUtil.encryptData(encSecretKey, BinaryUtils.ByteArrayToHexString(command));
|
|
54
|
-
|
|
55
|
-
Intent intent = new Intent(INTENT_RECEIVE_C_APDU);
|
|
56
|
-
intent.setPackage(getApplicationContext().getPackageName());
|
|
57
|
-
intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
58
|
-
intent.putExtra("capdu", capdu);
|
|
59
|
-
getApplicationContext().sendBroadcast(intent);
|
|
60
|
-
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@Override
|
|
65
|
-
public void onCreate() {
|
|
66
|
-
Log.d(TAG, "HCEService onCreate");
|
|
67
|
-
|
|
68
|
-
String prefsName = getApplicationContext().getPackageName() + ".nativehcemodule";
|
|
69
|
-
String encKey = getApplicationContext()
|
|
70
|
-
.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
71
|
-
.getString("encKey", "");
|
|
72
|
-
|
|
73
|
-
if (encKey.isEmpty()) {
|
|
74
|
-
throw new RuntimeException("Failed to load encKey.");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
byte[] bArr = BinaryUtils.HexStringToByteArray(encKey);
|
|
78
|
-
encSecretKey = new SecretKeySpec(bArr, 0, bArr.length, "AES");
|
|
79
|
-
|
|
80
|
-
/*
|
|
81
|
-
* See a comment about registerReceiver() in RTNHCEAndroidModule.
|
|
82
|
-
*/
|
|
83
|
-
IntentFilter filter = new IntentFilter();
|
|
84
|
-
filter.addAction(INTENT_SEND_R_APDU);
|
|
85
|
-
getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
|
|
86
|
-
|
|
87
|
-
Intent intent = new Intent(INTENT_READER_DETECT);
|
|
88
|
-
intent.setPackage(getApplicationContext().getPackageName());
|
|
89
|
-
intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
90
|
-
getApplicationContext().sendBroadcast(intent);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
@Override
|
|
94
|
-
public void onDeactivated(int reason) {
|
|
95
|
-
Log.d(TAG, "HCEService onDeactivated: " + reason);
|
|
96
|
-
|
|
97
|
-
getApplicationContext().unregisterReceiver(receiver);
|
|
98
|
-
|
|
99
|
-
Intent intent = new Intent(INTENT_READER_LOST);
|
|
100
|
-
intent.setPackage(getApplicationContext().getPackageName());
|
|
101
|
-
intent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
102
|
-
getApplicationContext().sendBroadcast(intent);
|
|
103
|
-
}
|
|
104
|
-
}
|