@icedevml/react-native-host-card-emulation 0.4.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 +68 -29
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/IntentKeys.java +1 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +80 -67
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +0 -4
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/{HCEService.java → service/HCEService.java} +19 -36
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/{BinaryUtils.java → util/BinaryUtils.java} +1 -1
- package/ios/RTNHCEModuleIOSImpl.swift +2 -7
- package/package.json +1 -1
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/AESGCMUtil.java +0 -89
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
82
|
+ android:enabled="true"
|
|
71
|
-
+ android:permission="android.permission.BIND_NFC_SERVICE"
|
|
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"/>
|
|
@@ -100,19 +112,35 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
100
112
|
|
|
101
113
|
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
102
114
|
|
|
103
|
-
|
|
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.
|
|
104
125
|
```typescript
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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();
|
|
108
136
|
}
|
|
109
|
-
});
|
|
137
|
+
}, []);
|
|
110
138
|
```
|
|
111
|
-
2. When user indicates that he wants to perform the HCE action, call the following function from the button's onClick routine:
|
|
139
|
+
2. When user indicates that he/she wants to perform the HCE action, call the following function from the button's onClick routine:
|
|
112
140
|
```typescript
|
|
113
141
|
await NativeHCEModule?.beginSession();
|
|
114
142
|
```
|
|
115
|
-
This will emit `sessionStarted` event on both platforms.
|
|
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:
|
|
116
144
|
```typescript
|
|
117
145
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
118
146
|
case 'sessionStarted':
|
|
@@ -120,10 +148,10 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
120
148
|
await NativeHCEModule?.startHCE();
|
|
121
149
|
break;
|
|
122
150
|
```
|
|
123
|
-
|
|
124
|
-
* iOS:
|
|
125
|
-
* Android:
|
|
126
|
-
|
|
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.
|
|
127
155
|
```typescript
|
|
128
156
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
129
157
|
case 'readerDetected':
|
|
@@ -134,29 +162,40 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
134
162
|
NativeHCEModule?.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
|
|
135
163
|
break;
|
|
136
164
|
```
|
|
137
|
-
|
|
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:
|
|
138
169
|
```typescript
|
|
139
170
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
140
171
|
case 'received':
|
|
141
|
-
NativeHCEModule?.setSessionAlertMessage('Keep holding the tag');
|
|
172
|
+
NativeHCEModule?.setSessionAlertMessage('Keep holding the tag'); // only for iOS, no-op on Android
|
|
142
173
|
|
|
174
|
+
// decode incoming C-APDU to bytes
|
|
143
175
|
const capdu = Buffer.from(event.arg!, 'hex');
|
|
144
176
|
console.log('Received C-APDU, capdu.toString('hex'));
|
|
145
177
|
|
|
146
|
-
//
|
|
178
|
+
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
147
179
|
await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
148
180
|
break;
|
|
149
181
|
```
|
|
150
|
-
|
|
151
|
-
```typescript
|
|
152
|
-
NativeHCEModule?.acquireExclusiveNFC();
|
|
153
|
-
```
|
|
154
|
-
Which will throw an exception if the presentment intent assertion was already acquired and is still valid
|
|
155
|
-
(usually 15 seconds) or if the cooldown period for acquiring new assertion was not yet expired (usually also 15 seconds).
|
|
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.
|
|
156
183
|
|
|
157
|
-
|
|
158
|
-
|
|
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.
|
|
159
190
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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,11 +1,11 @@
|
|
|
1
1
|
package com.itsecrnd.rtnhceandroid;
|
|
2
2
|
|
|
3
3
|
public class IntentKeys {
|
|
4
|
+
public static final String PERMISSION_HCE_BROADCAST = "com.itsecrnd.rtnhceandroid.HCE_BROADCAST";
|
|
4
5
|
public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
|
|
5
6
|
public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
|
|
6
7
|
public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
|
|
7
8
|
public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
|
|
8
|
-
public static final String KEY_AUTH = "auth";
|
|
9
9
|
public static final String KEY_CAPDU = "capdu";
|
|
10
10
|
public static final String KEY_RAPDU = "rapdu";
|
|
11
11
|
}
|
|
@@ -5,9 +5,9 @@ import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
|
|
|
5
5
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
|
|
6
6
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
|
|
7
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,9 +23,6 @@ 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";
|
|
@@ -34,7 +31,6 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
34
31
|
private volatile boolean hceRunning;
|
|
35
32
|
private volatile boolean hceBreakConnection;
|
|
36
33
|
private volatile boolean hceDeselected;
|
|
37
|
-
private final SecretKey encSecretKey;
|
|
38
34
|
|
|
39
35
|
private void sendEvent(final String type, final String arg) {
|
|
40
36
|
WritableMap map = Arguments.createMap();
|
|
@@ -43,39 +39,46 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
43
39
|
emitOnEvent(map);
|
|
44
40
|
}
|
|
45
41
|
|
|
42
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
46
43
|
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
47
44
|
@Override
|
|
48
45
|
public void onReceive(Context context, Intent intent) {
|
|
49
46
|
try {
|
|
50
47
|
String action = intent.getAction();
|
|
51
48
|
|
|
52
|
-
if (action
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
49
|
+
if (action == null) {
|
|
50
|
+
Log.w(TAG, "Received intent with null action in module receiver.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (action.equals(ACTION_RECEIVE_C_APDU)) {
|
|
55
|
+
String capdu = intent.getStringExtra("capdu");
|
|
56
|
+
|
|
57
|
+
if (!hceBreakConnection && hceRunning) {
|
|
58
|
+
sendEvent("received", capdu);
|
|
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) {
|
|
68
69
|
hceDeselected = false;
|
|
69
70
|
sendEvent("readerDetected", "");
|
|
70
|
-
} else if (action.equals(ACTION_READER_LOST)) {
|
|
71
|
-
if (hceRunning && !hceBreakConnection && !hceDeselected) {
|
|
72
|
-
// only if we haven't send a fake disconnect event already
|
|
73
|
-
hceDeselected = true;
|
|
74
|
-
sendEvent("readerDeselected", "");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
hceBreakConnection = false;
|
|
78
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;
|
|
76
|
+
sendEvent("readerDeselected", "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
hceBreakConnection = false;
|
|
80
|
+
} else {
|
|
81
|
+
Log.w(TAG, "Received unknown action intent in module receiver: " + action);
|
|
79
82
|
}
|
|
80
83
|
} catch (Exception e) {
|
|
81
84
|
Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
|
|
@@ -85,47 +88,33 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
85
88
|
|
|
86
89
|
RTNHCEAndroidModule(ReactApplicationContext context) {
|
|
87
90
|
super(context);
|
|
88
|
-
|
|
91
|
+
|
|
89
92
|
this.sessionRunning = false;
|
|
90
93
|
this.hceRunning = false;
|
|
91
94
|
this.hceBreakConnection = false;
|
|
92
95
|
this.hceDeselected = true;
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
);
|
|
99
110
|
}
|
|
100
|
-
|
|
101
|
-
String prefsName = getReactApplicationContext().getPackageName() + ".nativehcemodule";
|
|
102
|
-
getReactApplicationContext()
|
|
103
|
-
.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
104
|
-
.edit()
|
|
105
|
-
.putString("encKey", encKey)
|
|
106
|
-
.commit();
|
|
107
|
-
|
|
108
|
-
IntentFilter filter = new IntentFilter();
|
|
109
|
-
filter.addAction(ACTION_RECEIVE_C_APDU);
|
|
110
|
-
filter.addAction(ACTION_READER_DETECT);
|
|
111
|
-
filter.addAction(ACTION_READER_LOST);
|
|
112
|
-
|
|
113
|
-
/*
|
|
114
|
-
* FIXME It seems like those intents are not going through across this module and HostApduService.
|
|
115
|
-
* This happens even though those things are within the same application and package.
|
|
116
|
-
* It also seems like there is no way to bind HostApduService (or it's a skill issue).
|
|
117
|
-
*
|
|
118
|
-
* For now we will just encrypt&authenticate all messages using AES/GCM and use exported
|
|
119
|
-
* receivers. This is not a perfect solution but it is going to be secure and sufficient for now.
|
|
120
|
-
*
|
|
121
|
-
* If somebody knows a better way then any pull requests would be greatly appreciated.
|
|
122
|
-
*/
|
|
123
|
-
getReactApplicationContext().getApplicationContext().registerReceiver(receiver, filter, RECEIVER_EXPORTED);
|
|
124
111
|
}
|
|
125
112
|
|
|
126
113
|
@Override
|
|
127
114
|
public void invalidate() {
|
|
128
|
-
|
|
115
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
116
|
+
getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
|
|
117
|
+
}
|
|
129
118
|
}
|
|
130
119
|
|
|
131
120
|
@Override
|
|
@@ -136,9 +125,13 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
136
125
|
|
|
137
126
|
@Override
|
|
138
127
|
public boolean isPlatformSupported() {
|
|
128
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
139
132
|
PackageManager pm = getReactApplicationContext().getPackageManager();
|
|
140
133
|
return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
|
|
141
|
-
|
|
134
|
+
&& pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
|
|
142
135
|
}
|
|
143
136
|
|
|
144
137
|
@Override
|
|
@@ -154,6 +147,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
154
147
|
|
|
155
148
|
@Override
|
|
156
149
|
public void beginSession(Promise promise) {
|
|
150
|
+
if (!isPlatformSupported()) {
|
|
151
|
+
promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
157
155
|
if (!this.sessionRunning) {
|
|
158
156
|
this.sessionRunning = true;
|
|
159
157
|
sendEvent("sessionStarted", "");
|
|
@@ -183,6 +181,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
183
181
|
|
|
184
182
|
@Override
|
|
185
183
|
public void startHCE(Promise promise) {
|
|
184
|
+
if (!this.sessionRunning) {
|
|
185
|
+
promise.reject("err_no_session", "No session is active.");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
186
189
|
if (this.hceRunning) {
|
|
187
190
|
promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
|
|
188
191
|
return;
|
|
@@ -194,6 +197,11 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
194
197
|
|
|
195
198
|
@Override
|
|
196
199
|
public void stopHCE(String status, Promise promise) {
|
|
200
|
+
if (!this.sessionRunning) {
|
|
201
|
+
promise.reject("err_no_session", "No session is active.");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
197
205
|
if (this.hceRunning) {
|
|
198
206
|
this.hceRunning = false;
|
|
199
207
|
|
|
@@ -207,22 +215,27 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
207
215
|
promise.resolve(null);
|
|
208
216
|
}
|
|
209
217
|
|
|
218
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
210
219
|
@Override
|
|
211
220
|
public void respondAPDU(String rapdu, Promise promise) {
|
|
212
|
-
|
|
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.");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
213
228
|
|
|
214
229
|
Intent intent = new Intent(ACTION_SEND_R_APDU);
|
|
215
230
|
intent.setPackage(getReactApplicationContext().getPackageName());
|
|
216
|
-
intent.putExtra("
|
|
217
|
-
|
|
218
|
-
getReactApplicationContext().getApplicationContext().sendBroadcast(intent);
|
|
231
|
+
intent.putExtra("rapdu", rapdu);
|
|
232
|
+
getReactApplicationContext().getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
219
233
|
|
|
220
234
|
promise.resolve(null);
|
|
221
235
|
}
|
|
222
236
|
|
|
223
237
|
@Override
|
|
224
238
|
public void isHCERunning(Promise promise) {
|
|
225
|
-
|
|
226
|
-
promise.resolve(true);
|
|
239
|
+
promise.resolve(this.hceRunning);
|
|
227
240
|
}
|
|
228
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
|
package/android/src/main/java/com/itsecrnd/rtnhceandroid/{HCEService.java → service/HCEService.java}
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
package com.itsecrnd.rtnhceandroid;
|
|
1
|
+
package com.itsecrnd.rtnhceandroid.service;
|
|
2
2
|
|
|
3
3
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_DETECT;
|
|
4
4
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_READER_LOST;
|
|
5
5
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_RECEIVE_C_APDU;
|
|
6
6
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.ACTION_SEND_R_APDU;
|
|
7
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_AUTH;
|
|
8
7
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_CAPDU;
|
|
9
8
|
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
|
|
9
|
+
import static com.itsecrnd.rtnhceandroid.IntentKeys.PERMISSION_HCE_BROADCAST;
|
|
10
10
|
|
|
11
11
|
import android.content.BroadcastReceiver;
|
|
12
12
|
import android.content.Context;
|
|
@@ -19,31 +19,31 @@ import android.util.Log;
|
|
|
19
19
|
|
|
20
20
|
import androidx.annotation.RequiresApi;
|
|
21
21
|
|
|
22
|
-
import
|
|
22
|
+
import com.itsecrnd.rtnhceandroid.util.BinaryUtils;
|
|
23
23
|
|
|
24
|
-
import
|
|
25
|
-
import javax.crypto.spec.SecretKeySpec;
|
|
24
|
+
import java.util.Locale;
|
|
26
25
|
|
|
27
26
|
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
28
27
|
public class HCEService extends HostApduService {
|
|
29
28
|
private static final String TAG = "HCEService";
|
|
30
29
|
|
|
31
|
-
private SecretKey encSecretKey;
|
|
32
|
-
|
|
33
30
|
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
|
34
31
|
@Override
|
|
35
32
|
public void onReceive(Context context, Intent intent) {
|
|
36
33
|
try {
|
|
37
34
|
String action = intent.getAction();
|
|
38
35
|
|
|
39
|
-
if (action != null) {
|
|
40
|
-
|
|
36
|
+
if (action != null && action.equals(ACTION_SEND_R_APDU)) {
|
|
37
|
+
String rapdu = intent.getStringExtra(KEY_RAPDU);
|
|
41
38
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
byte[] dec = BinaryUtils.HexStringToByteArray(rapdu.toUpperCase(Locale.ROOT));
|
|
45
|
-
sendResponseApdu(dec);
|
|
39
|
+
if (rapdu == null) {
|
|
40
|
+
throw new RuntimeException("Bug! ACTION_SEND_RAPDU intent KEY_RAPDU is null.");
|
|
46
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
47
|
}
|
|
48
48
|
} catch (Exception e) {
|
|
49
49
|
Log.e(TAG, "Failed to handle broadcast in HCEService.", e);
|
|
@@ -53,13 +53,12 @@ public class HCEService extends HostApduService {
|
|
|
53
53
|
|
|
54
54
|
@Override
|
|
55
55
|
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
56
|
-
String capdu =
|
|
56
|
+
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
57
57
|
|
|
58
58
|
Intent intent = new Intent(ACTION_RECEIVE_C_APDU);
|
|
59
59
|
intent.setPackage(getApplicationContext().getPackageName());
|
|
60
|
-
intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
61
60
|
intent.putExtra(KEY_CAPDU, capdu);
|
|
62
|
-
getApplicationContext().sendBroadcast(intent);
|
|
61
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
63
62
|
|
|
64
63
|
return null;
|
|
65
64
|
}
|
|
@@ -68,29 +67,14 @@ public class HCEService extends HostApduService {
|
|
|
68
67
|
public void onCreate() {
|
|
69
68
|
Log.d(TAG, "HCEService onCreate");
|
|
70
69
|
|
|
71
|
-
String prefsName = getApplicationContext().getPackageName() + ".nativehcemodule";
|
|
72
|
-
String encKey = getApplicationContext()
|
|
73
|
-
.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
74
|
-
.getString("encKey", "");
|
|
75
|
-
|
|
76
|
-
if (encKey.isEmpty()) {
|
|
77
|
-
throw new RuntimeException("Failed to load encKey.");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
byte[] bArr = BinaryUtils.HexStringToByteArray(encKey);
|
|
81
|
-
encSecretKey = new SecretKeySpec(bArr, 0, bArr.length, "AES");
|
|
82
|
-
|
|
83
|
-
/*
|
|
84
|
-
* See a comment about registerReceiver() in RTNHCEAndroidModule.
|
|
85
|
-
*/
|
|
86
70
|
IntentFilter filter = new IntentFilter();
|
|
87
71
|
filter.addAction(ACTION_SEND_R_APDU);
|
|
88
|
-
getApplicationContext().registerReceiver(
|
|
72
|
+
getApplicationContext().registerReceiver(
|
|
73
|
+
receiver, filter, PERMISSION_HCE_BROADCAST, null, RECEIVER_EXPORTED);
|
|
89
74
|
|
|
90
75
|
Intent intent = new Intent(ACTION_READER_DETECT);
|
|
91
76
|
intent.setPackage(getApplicationContext().getPackageName());
|
|
92
|
-
|
|
93
|
-
getApplicationContext().sendBroadcast(intent);
|
|
77
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
94
78
|
}
|
|
95
79
|
|
|
96
80
|
@Override
|
|
@@ -101,7 +85,6 @@ public class HCEService extends HostApduService {
|
|
|
101
85
|
|
|
102
86
|
Intent intent = new Intent(ACTION_READER_LOST);
|
|
103
87
|
intent.setPackage(getApplicationContext().getPackageName());
|
|
104
|
-
|
|
105
|
-
getApplicationContext().sendBroadcast(intent);
|
|
88
|
+
getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
|
|
106
89
|
}
|
|
107
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
|
}
|
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
|
-
}
|