@icedevml/react-native-host-card-emulation 0.4.0 → 0.5.0
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 +110 -26
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEService.java +195 -67
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/HCEServiceCallback.java +6 -0
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidModule.java +118 -105
- package/android/src/main/java/com/itsecrnd/rtnhceandroid/RTNHCEAndroidPackage.java +0 -4
- package/ios/RTNHCEModuleIOS.mm +4 -0
- package/ios/RTNHCEModuleIOSImpl.swift +2 -7
- package/js/NativeHCEModule.ts +14 -1
- 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/IntentKeys.java +0 -11
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,8 @@ 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
|
-
+
|
|
67
|
+
+ <!-- set required="true" if your app would be entirely useless on devices without HCE support -->
|
|
68
|
+
+ <uses-feature android:name="android.hardware.nfc.hce" android:required="false" />
|
|
58
69
|
|
|
59
70
|
...
|
|
60
71
|
|
|
@@ -68,7 +79,7 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
68
79
|
+ android:name="com.itsecrnd.rtnhceandroid.HCEService"
|
|
69
80
|
+ android:exported="true"
|
|
70
81
|
+ android:enabled="true"
|
|
71
|
-
+ android:permission="android.permission.BIND_NFC_SERVICE"
|
|
82
|
+
+ android:permission="android.permission.BIND_NFC_SERVICE">
|
|
72
83
|
+ <intent-filter>
|
|
73
84
|
+ <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
|
|
74
85
|
+ <category android:name="android.intent.category.DEFAULT"/>
|
|
@@ -100,19 +111,35 @@ Install `@icedevml/react-native-host-card-emulation` package within your React N
|
|
|
100
111
|
|
|
101
112
|
This module provides a uniform low-level HCE API for both mobile platforms.
|
|
102
113
|
|
|
103
|
-
|
|
114
|
+
> [!NOTE]
|
|
115
|
+
> 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).
|
|
116
|
+
> Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
|
|
117
|
+
|
|
118
|
+
> [!NOTE]
|
|
119
|
+
> 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.
|
|
120
|
+
|
|
121
|
+
### Quick start guide
|
|
122
|
+
|
|
123
|
+
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
124
|
```typescript
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
const subscription = NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
/* ... implement event handlers here ... */
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// cleanup the event listener when the effect is unmounted
|
|
133
|
+
return () => {
|
|
134
|
+
subscription.remove();
|
|
108
135
|
}
|
|
109
|
-
});
|
|
136
|
+
}, []);
|
|
110
137
|
```
|
|
111
|
-
2. When user indicates that he wants to perform the HCE action, call the following function from the button's onClick routine:
|
|
138
|
+
2. When user indicates that he/she wants to perform the HCE action, call the following function from the button's onClick routine:
|
|
112
139
|
```typescript
|
|
113
140
|
await NativeHCEModule?.beginSession();
|
|
114
141
|
```
|
|
115
|
-
This will emit `sessionStarted` event on both platforms.
|
|
142
|
+
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
143
|
```typescript
|
|
117
144
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
118
145
|
case 'sessionStarted':
|
|
@@ -120,10 +147,10 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
120
147
|
await NativeHCEModule?.startHCE();
|
|
121
148
|
break;
|
|
122
149
|
```
|
|
123
|
-
|
|
124
|
-
* iOS:
|
|
125
|
-
* Android:
|
|
126
|
-
|
|
150
|
+
Calling `await NativeHCEModule?.startHCE()` causes:
|
|
151
|
+
* 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.
|
|
152
|
+
* 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).
|
|
153
|
+
3. Optionally, you can handle `readerDetected` and `readerDeselected` events to enhance user's experience.
|
|
127
154
|
```typescript
|
|
128
155
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
129
156
|
case 'readerDetected':
|
|
@@ -134,29 +161,86 @@ This module provides a uniform low-level HCE API for both mobile platforms.
|
|
|
134
161
|
NativeHCEModule?.setSessionAlertMessage('Lost reader'); // only for iOS, no-op in Android
|
|
135
162
|
break;
|
|
136
163
|
```
|
|
137
|
-
|
|
164
|
+
For those events, trigger mechanisms are platform dependent:
|
|
165
|
+
* iOS: The `readerDetected` event will be emitted as soon as the NFC reader's 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.
|
|
166
|
+
* 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.
|
|
167
|
+
4. Receive incoming C-APDU and respond to it:
|
|
138
168
|
```typescript
|
|
139
169
|
// inside NativeHCEModule?.onEvent handler's switch
|
|
140
170
|
case 'received':
|
|
141
|
-
NativeHCEModule?.setSessionAlertMessage('Keep holding the tag');
|
|
171
|
+
NativeHCEModule?.setSessionAlertMessage('Keep holding the tag'); // only for iOS, no-op on Android
|
|
142
172
|
|
|
173
|
+
// decode incoming C-APDU to bytes
|
|
143
174
|
const capdu = Buffer.from(event.arg!, 'hex');
|
|
144
175
|
console.log('Received C-APDU, capdu.toString('hex'));
|
|
145
176
|
|
|
146
|
-
//
|
|
177
|
+
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
147
178
|
await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
148
179
|
break;
|
|
149
180
|
```
|
|
150
|
-
|
|
181
|
+
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.
|
|
182
|
+
|
|
183
|
+
### iOS: Acquiring exclusive NFC access
|
|
184
|
+
|
|
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
|
+
### Android: Handle HCE calls when the app is not running
|
|
199
|
+
|
|
200
|
+
Exclusively on Android platform it is possible to handle Host Card Emulation even when the app is not running in foreground.
|
|
201
|
+
In such case, no user interface will be available, the handler will run in headless mode. This is still extremely useful in certain cases,
|
|
202
|
+
for instance - your app may emulate an NDEF tag even when it's not launched on the foreground.
|
|
203
|
+
|
|
204
|
+
1. Register `handleBackgroundHCECall` headless task in your app's `index.js`:
|
|
151
205
|
```typescript
|
|
152
|
-
|
|
206
|
+
AppRegistry.registerHeadlessTask('handleBackgroundHCECall', () => {
|
|
207
|
+
return async () => {
|
|
208
|
+
return await runBackground();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
2. Register onBackgroundEvent listener in your `runBackground()` function and call to `await NativeHCEModule?.initBackgroundHCE()` at the very end, after the event listener is fully set up.
|
|
213
|
+
```typescript
|
|
214
|
+
async function runBackground() {
|
|
215
|
+
let subscription = NativeHCEModule?.onBackgroundEvent(async (event: HCEModuleBackgroundEvent) => {
|
|
216
|
+
switch (event.type) {
|
|
217
|
+
/* ... background HCE event handler here ... */
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await NativeHCEModule?.initBackgroundHCE();
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
3. Cleanup your event listener in `readerDeselected` -- this is obligatory because lack of proper cleanup will result in bugs and unexpected behavior.
|
|
225
|
+
```typescript
|
|
226
|
+
case 'readerDeselected':
|
|
227
|
+
// cleanup, remove the event listener
|
|
228
|
+
subscription.remove();
|
|
229
|
+
break;
|
|
230
|
+
```
|
|
231
|
+
4. Implement the actual APDU handler that will process the C-APDU and generate a response.
|
|
232
|
+
```typescript
|
|
233
|
+
case 'received':
|
|
234
|
+
// decode incoming C-APDU to bytes
|
|
235
|
+
const capdu = Buffer.from(event.arg!, 'hex');
|
|
236
|
+
console.log('Received C-APDU, capdu.toString('hex'));
|
|
237
|
+
|
|
238
|
+
// for the demo purposes, we always want to respond with [0x0A] + status code 0x9000 (success)
|
|
239
|
+
await NativeHCEModule?.respondAPDU(Buffer.from([0x0A, 0x90, 0x00], "hex"));
|
|
240
|
+
break;
|
|
153
241
|
```
|
|
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).
|
|
156
242
|
|
|
157
|
-
|
|
158
|
-
> 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.
|
|
243
|
+
### More resources
|
|
159
244
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
> Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
|
|
245
|
+
* [Module's API specification](https://github.com/icedevml/react-native-host-card-emulation/blob/master/packages/native-hce-module/js/NativeHCEModule.ts)
|
|
246
|
+
* [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,107 +1,235 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Some of the code inspired by:
|
|
3
|
+
* https://github.com/appidea/react-native-hce
|
|
4
|
+
* https://github.com/transistorsoft/react-native-background-fetch
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
package com.itsecrnd.rtnhceandroid;
|
|
2
8
|
|
|
3
|
-
import static com.
|
|
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_AUTH;
|
|
8
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_CAPDU;
|
|
9
|
-
import static com.itsecrnd.rtnhceandroid.IntentKeys.KEY_RAPDU;
|
|
9
|
+
import static com.facebook.react.jstasks.HeadlessJsTaskContext.Companion;
|
|
10
10
|
|
|
11
|
-
import android.
|
|
11
|
+
import android.app.ActivityManager;
|
|
12
12
|
import android.content.Context;
|
|
13
|
-
import android.content.Intent;
|
|
14
|
-
import android.content.IntentFilter;
|
|
15
13
|
import android.nfc.cardemulation.HostApduService;
|
|
16
14
|
import android.os.Build;
|
|
17
15
|
import android.os.Bundle;
|
|
18
16
|
import android.util.Log;
|
|
19
17
|
|
|
18
|
+
import androidx.annotation.NonNull;
|
|
20
19
|
import androidx.annotation.RequiresApi;
|
|
21
20
|
|
|
21
|
+
import com.facebook.react.ReactApplication;
|
|
22
|
+
import com.facebook.react.ReactHost;
|
|
23
|
+
import com.facebook.react.ReactInstanceEventListener;
|
|
24
|
+
import com.facebook.react.bridge.Arguments;
|
|
25
|
+
import com.facebook.react.bridge.ReactContext;
|
|
26
|
+
import com.facebook.react.bridge.UiThreadUtil;
|
|
27
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
|
|
28
|
+
import com.facebook.react.jstasks.HeadlessJsTaskContext;
|
|
29
|
+
import com.facebook.react.jstasks.HeadlessJsTaskEventListener;
|
|
30
|
+
|
|
31
|
+
import java.util.List;
|
|
22
32
|
import java.util.Locale;
|
|
23
33
|
|
|
24
|
-
import javax.crypto.SecretKey;
|
|
25
|
-
import javax.crypto.spec.SecretKeySpec;
|
|
26
|
-
|
|
27
34
|
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
28
|
-
public class HCEService extends HostApduService {
|
|
35
|
+
public class HCEService extends HostApduService implements ReactInstanceEventListener, HCEServiceCallback {
|
|
29
36
|
private static final String TAG = "HCEService";
|
|
30
37
|
|
|
31
|
-
private
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
38
|
+
private boolean isForeground;
|
|
39
|
+
private RTNHCEAndroidModule hceModule;
|
|
40
|
+
private byte[] pendingCAPDU;
|
|
41
|
+
private volatile boolean needsResponse;
|
|
42
|
+
|
|
43
|
+
private boolean isAppOnForeground(Context context) {
|
|
44
|
+
/*
|
|
45
|
+
We need to check if app is in foreground otherwise the app will crash.
|
|
46
|
+
https://stackoverflow.com/questions/8489993/check-android-application-is-in-foreground-or-not
|
|
47
|
+
*/
|
|
48
|
+
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
49
|
+
List<ActivityManager.RunningAppProcessInfo> appProcesses =
|
|
50
|
+
activityManager.getRunningAppProcesses();
|
|
51
|
+
if (appProcesses == null) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
final String packageName = context.getPackageName();
|
|
55
|
+
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
|
|
56
|
+
if (appProcess.importance ==
|
|
57
|
+
ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
|
|
58
|
+
appProcess.processName.equals(packageName)) {
|
|
59
|
+
return true;
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Override
|
|
66
|
+
public void onBackgroundHCEInit() {
|
|
67
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit");
|
|
68
|
+
|
|
69
|
+
if (pendingCAPDU != null) {
|
|
70
|
+
Log.d(TAG, "HCEService:onBackgroundHCEInit send pendingCAPDU");
|
|
71
|
+
hceModule.sendBackgroundEvent("received", BinaryUtils.ByteArrayToHexString(pendingCAPDU));
|
|
72
|
+
pendingCAPDU = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
53
75
|
|
|
54
76
|
@Override
|
|
55
|
-
public
|
|
56
|
-
|
|
77
|
+
public void onRespondAPDU(String rapdu) {
|
|
78
|
+
Log.d(TAG, "HCEService:onRespondAPDU");
|
|
79
|
+
|
|
80
|
+
if (!needsResponse) {
|
|
81
|
+
Log.d(TAG, "HCEService:onRespondAPDU not waiting for a response");
|
|
82
|
+
throw new IllegalStateException();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
needsResponse = false;
|
|
86
|
+
sendResponseApdu(BinaryUtils.HexStringToByteArray(rapdu));
|
|
87
|
+
}
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
89
|
+
@Override
|
|
90
|
+
public byte[] processCommandApdu(byte[] command, Bundle extras) {
|
|
91
|
+
Log.d(TAG, "HCEService:processCommandApdu");
|
|
92
|
+
String capdu = BinaryUtils.ByteArrayToHexString(command).toUpperCase(Locale.ROOT);
|
|
93
|
+
|
|
94
|
+
if (isForeground) {
|
|
95
|
+
if (hceModule._isHCERunning() && !hceModule.isHCEBrokenConnection()) {
|
|
96
|
+
Log.d(TAG, "HCEService:processCommandApdu foreground sendEvent received");
|
|
97
|
+
needsResponse = true;
|
|
98
|
+
hceModule.sendEvent("received", capdu);
|
|
99
|
+
} else {
|
|
100
|
+
Log.d(TAG, "HCEService:processCommandApdu foreground respond 6999");
|
|
101
|
+
return new byte[] { (byte) 0x69, (byte) 0x99 };
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
if (hceModule != null && hceModule.isHCEBackgroundHandlerReady()) {
|
|
105
|
+
Log.d(TAG, "HCEService:processCommandApdu background sendBackgroundEvent received");
|
|
106
|
+
needsResponse = true;
|
|
107
|
+
hceModule.sendBackgroundEvent("received", capdu);
|
|
108
|
+
} else {
|
|
109
|
+
Log.d(TAG, "HCEService:processCommandApdu background pendingCAPDU");
|
|
110
|
+
needsResponse = true;
|
|
111
|
+
pendingCAPDU = command;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
63
114
|
|
|
64
115
|
return null;
|
|
65
116
|
}
|
|
66
117
|
|
|
67
118
|
@Override
|
|
68
119
|
public void onCreate() {
|
|
69
|
-
Log.d(TAG, "HCEService
|
|
120
|
+
Log.d(TAG, "HCEService:onCreate");
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
String encKey = getApplicationContext()
|
|
73
|
-
.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
|
|
74
|
-
.getString("encKey", "");
|
|
122
|
+
ReactHost reactHost = ((ReactApplication) getApplication()).getReactHost();
|
|
75
123
|
|
|
76
|
-
if (
|
|
77
|
-
throw new RuntimeException("
|
|
124
|
+
if (reactHost == null) {
|
|
125
|
+
throw new RuntimeException("BUG! getReactHost() returned null.");
|
|
78
126
|
}
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
128
|
+
isForeground = isAppOnForeground(getApplicationContext());
|
|
129
|
+
pendingCAPDU = null;
|
|
130
|
+
hceModule = null;
|
|
131
|
+
needsResponse = false;
|
|
82
132
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
if (isForeground) {
|
|
134
|
+
Log.d(TAG, "HCEService:onCreate foreground");
|
|
135
|
+
|
|
136
|
+
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
137
|
+
|
|
138
|
+
if (reactContext == null) {
|
|
139
|
+
throw new RuntimeException("BUG! getCurrentReactContext() returned null in foreground.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this.hceModule = ((RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule"));
|
|
143
|
+
|
|
144
|
+
if (this.hceModule == null) {
|
|
145
|
+
throw new RuntimeException("BUG! getNativeModule() returned null in foreground.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
this.hceModule.setHCEService(this);
|
|
149
|
+
|
|
150
|
+
if (this.hceModule._isHCERunning()) {
|
|
151
|
+
this.hceModule.sendEvent("readerDetected", "");
|
|
152
|
+
} else {
|
|
153
|
+
this.hceModule.setHCEBrokenConnection();
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
ReactContext reactContext = reactHost.getCurrentReactContext();
|
|
157
|
+
Log.d(TAG, "HCEService:onCreate background");
|
|
158
|
+
|
|
159
|
+
if (reactContext == null) {
|
|
160
|
+
reactHost.addReactInstanceEventListener(this);
|
|
161
|
+
reactHost.start();
|
|
162
|
+
} else {
|
|
163
|
+
setupRunJSTask(reactContext);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
94
166
|
}
|
|
95
167
|
|
|
96
168
|
@Override
|
|
97
169
|
public void onDeactivated(int reason) {
|
|
98
|
-
Log.d(TAG, "HCEService
|
|
170
|
+
Log.d(TAG, "HCEService:onDeactivated: " + reason);
|
|
171
|
+
needsResponse = false;
|
|
99
172
|
|
|
100
|
-
|
|
173
|
+
if (isForeground) {
|
|
174
|
+
if (this.hceModule != null && !this.hceModule.isHCEBrokenConnection()) {
|
|
175
|
+
this.hceModule.sendEvent("readerDeselected", "");
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
this.hceModule.sendBackgroundEvent("readerDeselected", "");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (this.hceModule != null) {
|
|
182
|
+
this.hceModule.setHCEService(null);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Override
|
|
187
|
+
public void onReactContextInitialized(@NonNull ReactContext reactContext) {
|
|
188
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
189
|
+
headlessJsTaskContext.addTaskEventListener(new HeadlessJsTaskEventListener() {
|
|
190
|
+
@Override
|
|
191
|
+
public void onHeadlessJsTaskStart(int i) {
|
|
192
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskStart: " + i);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Override
|
|
196
|
+
public void onHeadlessJsTaskFinish(int i) {
|
|
197
|
+
Log.d(TAG, "HCEService:HeadlessJsTaskEventListener:onHeadlessJsTaskFinish: " + i);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
setupRunJSTask(reactContext);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public void setupRunJSTask(@NonNull ReactContext reactContext) {
|
|
205
|
+
hceModule = (RTNHCEAndroidModule) reactContext.getNativeModule("NativeHCEModule");
|
|
206
|
+
|
|
207
|
+
if (hceModule == null) {
|
|
208
|
+
throw new RuntimeException("BUG! getNativeModule() returned null in background.");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
hceModule.setHCEService(this);
|
|
212
|
+
|
|
213
|
+
UiThreadUtil.runOnUiThread(new Runnable() {
|
|
214
|
+
@Override
|
|
215
|
+
public void run() {
|
|
216
|
+
Log.d(TAG, "HCEService:setupRunJSTask:runOnUiThread startTask");
|
|
217
|
+
HeadlessJsTaskContext headlessJsTaskContext = Companion.getInstance(reactContext);
|
|
218
|
+
headlessJsTaskContext.startTask(
|
|
219
|
+
new HeadlessJsTaskConfig(
|
|
220
|
+
"handleBackgroundHCECall",
|
|
221
|
+
Arguments.fromBundle(new Bundle()),
|
|
222
|
+
15000,
|
|
223
|
+
false // not allowed in foreground
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@Override
|
|
230
|
+
public void onDestroy() {
|
|
231
|
+
super.onDestroy();
|
|
101
232
|
|
|
102
|
-
|
|
103
|
-
intent.setPackage(getApplicationContext().getPackageName());
|
|
104
|
-
intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
105
|
-
getApplicationContext().sendBroadcast(intent);
|
|
233
|
+
Log.d(TAG, "HCEService:onDestroy");
|
|
106
234
|
}
|
|
107
235
|
}
|
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
package com.itsecrnd.rtnhceandroid;
|
|
2
2
|
|
|
3
|
-
import static android.content.Context.RECEIVER_EXPORTED;
|
|
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
|
-
|
|
9
|
-
import android.content.BroadcastReceiver;
|
|
10
|
-
import android.content.ComponentName;
|
|
11
|
-
import android.content.Context;
|
|
12
|
-
import android.content.Intent;
|
|
13
|
-
import android.content.IntentFilter;
|
|
14
3
|
import android.content.pm.PackageManager;
|
|
15
4
|
import android.os.Build;
|
|
16
5
|
import android.util.Log;
|
|
@@ -23,137 +12,118 @@ import com.facebook.react.bridge.Promise;
|
|
|
23
12
|
import com.facebook.react.bridge.ReactApplicationContext;
|
|
24
13
|
import com.facebook.react.bridge.WritableMap;
|
|
25
14
|
|
|
26
|
-
import javax.crypto.SecretKey;
|
|
27
|
-
|
|
28
|
-
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
29
15
|
public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
30
16
|
public static final String TAG = "RTNHCEAndroidModule";
|
|
31
17
|
public static final String NAME = "NativeHCEModule";
|
|
32
18
|
|
|
33
19
|
private volatile boolean sessionRunning;
|
|
34
20
|
private volatile boolean hceRunning;
|
|
35
|
-
private volatile boolean hceBreakConnection;
|
|
36
21
|
private volatile boolean hceDeselected;
|
|
37
|
-
private
|
|
22
|
+
private volatile boolean hceBrokenConnection;
|
|
38
23
|
|
|
39
|
-
private
|
|
40
|
-
WritableMap map = Arguments.createMap();
|
|
41
|
-
map.putString("type", type);
|
|
42
|
-
map.putString("arg", arg);
|
|
43
|
-
emitOnEvent(map);
|
|
44
|
-
}
|
|
24
|
+
private volatile boolean hceBackgroundReady;
|
|
45
25
|
|
|
46
|
-
private
|
|
47
|
-
@Override
|
|
48
|
-
public void onReceive(Context context, Intent intent) {
|
|
49
|
-
try {
|
|
50
|
-
String action = intent.getAction();
|
|
51
|
-
|
|
52
|
-
if (action != null) {
|
|
53
|
-
AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("auth"));
|
|
54
|
-
|
|
55
|
-
if (action.equals(ACTION_RECEIVE_C_APDU)) {
|
|
56
|
-
String capdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra("capdu"));
|
|
57
|
-
|
|
58
|
-
if (!hceBreakConnection && hceRunning) {
|
|
59
|
-
sendEvent("received", capdu);
|
|
60
|
-
} else {
|
|
61
|
-
Intent rintent = new Intent(ACTION_SEND_R_APDU);
|
|
62
|
-
rintent.setPackage(getReactApplicationContext().getPackageName());
|
|
63
|
-
rintent.putExtra("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
|
|
64
|
-
rintent.putExtra("rapdu", AESGCMUtil.encryptData(encSecretKey, "6999"));
|
|
65
|
-
getReactApplicationContext().getApplicationContext().sendBroadcast(rintent);
|
|
66
|
-
}
|
|
67
|
-
} else if (hceRunning && action.equals(ACTION_READER_DETECT)) {
|
|
68
|
-
hceDeselected = false;
|
|
69
|
-
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
|
-
}
|
|
79
|
-
}
|
|
80
|
-
} catch (Exception e) {
|
|
81
|
-
Log.e(TAG, "Failed to handle broadcast in RTNHCEAndroidModule.", e);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
};
|
|
26
|
+
private HCEServiceCallback serviceCb;
|
|
85
27
|
|
|
86
28
|
RTNHCEAndroidModule(ReactApplicationContext context) {
|
|
87
29
|
super(context);
|
|
88
|
-
|
|
30
|
+
|
|
31
|
+
this.serviceCb = null;
|
|
32
|
+
|
|
89
33
|
this.sessionRunning = false;
|
|
90
34
|
this.hceRunning = false;
|
|
91
|
-
this.hceBreakConnection = false;
|
|
92
35
|
this.hceDeselected = true;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
encSecretKey = AESGCMUtil.generateKey();
|
|
96
|
-
encKey = BinaryUtils.ByteArrayToHexString(encSecretKey.getEncoded());
|
|
97
|
-
} catch (Exception e) {
|
|
98
|
-
throw new RuntimeException("Failed to generate encryption key.", e);
|
|
99
|
-
}
|
|
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);
|
|
36
|
+
this.hceBrokenConnection = false;
|
|
37
|
+
this.hceBackgroundReady = false;
|
|
124
38
|
}
|
|
125
39
|
|
|
126
40
|
@Override
|
|
127
41
|
public void invalidate() {
|
|
128
|
-
|
|
42
|
+
Log.d(TAG, "RTNHCEAndroidModule:invalidate");
|
|
129
43
|
}
|
|
130
44
|
|
|
131
45
|
@Override
|
|
132
46
|
@NonNull
|
|
133
47
|
public String getName() {
|
|
48
|
+
Log.d(TAG, "RTNHCEAndroidModule:getName");
|
|
134
49
|
return RTNHCEAndroidModule.NAME;
|
|
135
50
|
}
|
|
136
51
|
|
|
52
|
+
void sendEvent(final String type, final String arg) {
|
|
53
|
+
Log.d(TAG, "RTNHCEAndroidModule:sendEvent");
|
|
54
|
+
WritableMap map = Arguments.createMap();
|
|
55
|
+
map.putString("type", type);
|
|
56
|
+
map.putString("arg", arg);
|
|
57
|
+
emitOnEvent(map);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void sendBackgroundEvent(final String type, final String arg) {
|
|
61
|
+
Log.d(TAG, "RTNHCEAndroidModule:sendBackgroundEvent");
|
|
62
|
+
WritableMap map = Arguments.createMap();
|
|
63
|
+
map.putString("type", type);
|
|
64
|
+
map.putString("arg", arg);
|
|
65
|
+
emitOnBackgroundEvent(map);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
boolean _isHCERunning() {
|
|
69
|
+
return this.hceRunning;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
void setHCEBrokenConnection() {
|
|
73
|
+
this.hceBrokenConnection = true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
boolean isHCEBackgroundHandlerReady() {
|
|
77
|
+
return this.hceBackgroundReady;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
boolean isHCEBrokenConnection() {
|
|
81
|
+
return this.hceBrokenConnection;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
void setHCEService(HCEServiceCallback serviceCallback) {
|
|
85
|
+
Log.d(TAG, "RTNHCEAndroidModule:setHCEService");
|
|
86
|
+
|
|
87
|
+
this.serviceCb = serviceCallback;
|
|
88
|
+
this.hceDeselected = true;
|
|
89
|
+
this.hceBackgroundReady = false;
|
|
90
|
+
this.hceBrokenConnection = false;
|
|
91
|
+
}
|
|
92
|
+
|
|
137
93
|
@Override
|
|
138
94
|
public boolean isPlatformSupported() {
|
|
95
|
+
Log.d(TAG, "RTNHCEAndroidModule:isPlatformSupported");
|
|
96
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
139
100
|
PackageManager pm = getReactApplicationContext().getPackageManager();
|
|
140
101
|
return pm.hasSystemFeature(PackageManager.FEATURE_NFC)
|
|
141
|
-
|
|
102
|
+
&& pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
|
|
142
103
|
}
|
|
143
104
|
|
|
144
105
|
@Override
|
|
145
106
|
public void acquireExclusiveNFC(Promise promise) {
|
|
107
|
+
Log.d(TAG, "RTNHCEAndroidModule:acquireExclusiveNFC");
|
|
146
108
|
promise.reject("err_platform_unsupported", "acquireExclusiveNFC() is not supported on Android.");
|
|
147
109
|
}
|
|
148
110
|
|
|
149
111
|
@Override
|
|
150
112
|
public boolean isExclusiveNFC() {
|
|
113
|
+
Log.d(TAG, "RTNHCEAndroidModule:isExclusiveNFC");
|
|
151
114
|
// unsupported on Android, always false
|
|
152
115
|
return false;
|
|
153
116
|
}
|
|
154
117
|
|
|
155
118
|
@Override
|
|
156
119
|
public void beginSession(Promise promise) {
|
|
120
|
+
Log.d(TAG, "RTNHCEAndroidModule:beginSession");
|
|
121
|
+
|
|
122
|
+
if (!isPlatformSupported()) {
|
|
123
|
+
promise.reject("err_platform_unsupported", "Unsupported Android version or missing NFC/HCE feature.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
157
127
|
if (!this.sessionRunning) {
|
|
158
128
|
this.sessionRunning = true;
|
|
159
129
|
sendEvent("sessionStarted", "");
|
|
@@ -165,41 +135,70 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
165
135
|
|
|
166
136
|
@Override
|
|
167
137
|
public void setSessionAlertMessage(String message) {
|
|
138
|
+
Log.d(TAG, "RTNHCEAndroidModule:setSessionAlertMessage");
|
|
168
139
|
// unsupported on Android, no-op
|
|
169
140
|
}
|
|
170
141
|
|
|
171
142
|
@Override
|
|
172
143
|
public void invalidateSession() {
|
|
144
|
+
Log.d(TAG, "RTNHCEAndroidModule:invalidateSession");
|
|
173
145
|
if (this.sessionRunning) {
|
|
146
|
+
this.hceRunning = false;
|
|
174
147
|
this.sessionRunning = false;
|
|
148
|
+
this.hceBrokenConnection = true;
|
|
175
149
|
sendEvent("sessionInvalidated", "userInvalidated");
|
|
176
150
|
}
|
|
177
151
|
}
|
|
178
152
|
|
|
179
153
|
@Override
|
|
180
154
|
public boolean isSessionRunning() {
|
|
155
|
+
Log.d(TAG, "RTNHCEAndroidModule:isSessionRunning");
|
|
181
156
|
return this.sessionRunning;
|
|
182
157
|
}
|
|
183
158
|
|
|
159
|
+
@Override
|
|
160
|
+
public void initBackgroundHCE(Promise promise) {
|
|
161
|
+
Log.d(TAG, "RTNHCEAndroidModule:initBackgroundHCE");
|
|
162
|
+
|
|
163
|
+
this.hceBackgroundReady = true;
|
|
164
|
+
|
|
165
|
+
promise.resolve(null);
|
|
166
|
+
|
|
167
|
+
this.serviceCb.onBackgroundHCEInit();
|
|
168
|
+
}
|
|
169
|
+
|
|
184
170
|
@Override
|
|
185
171
|
public void startHCE(Promise promise) {
|
|
172
|
+
Log.d(TAG, "RTNHCEAndroidModule:startHCE");
|
|
173
|
+
if (!this.sessionRunning) {
|
|
174
|
+
promise.reject("err_no_session", "No session is active.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
186
178
|
if (this.hceRunning) {
|
|
187
179
|
promise.reject("err_start_emulation", "Error trying to start emulation. Emulation already started.");
|
|
188
180
|
return;
|
|
189
181
|
}
|
|
190
182
|
|
|
191
183
|
this.hceRunning = true;
|
|
184
|
+
this.hceBrokenConnection = false;
|
|
192
185
|
promise.resolve(null);
|
|
193
186
|
}
|
|
194
187
|
|
|
195
188
|
@Override
|
|
196
189
|
public void stopHCE(String status, Promise promise) {
|
|
190
|
+
Log.d(TAG, "RTNHCEAndroidModule:stopHCE");
|
|
191
|
+
if (!this.sessionRunning) {
|
|
192
|
+
promise.reject("err_no_session", "No session is active.");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
197
196
|
if (this.hceRunning) {
|
|
198
197
|
this.hceRunning = false;
|
|
199
198
|
|
|
200
199
|
if (!this.hceDeselected) {
|
|
201
200
|
this.hceDeselected = true;
|
|
202
|
-
this.
|
|
201
|
+
this.hceBrokenConnection = true;
|
|
203
202
|
sendEvent("readerDeselected", "");
|
|
204
203
|
}
|
|
205
204
|
}
|
|
@@ -207,22 +206,36 @@ public class RTNHCEAndroidModule extends NativeHCEModuleSpec {
|
|
|
207
206
|
promise.resolve(null);
|
|
208
207
|
}
|
|
209
208
|
|
|
209
|
+
@RequiresApi(api = Build.VERSION_CODES.TIRAMISU)
|
|
210
210
|
@Override
|
|
211
211
|
public void respondAPDU(String rapdu, Promise promise) {
|
|
212
|
-
|
|
212
|
+
Log.d(TAG, "RTNHCEAndroidModule:respondAPDU");
|
|
213
|
+
|
|
214
|
+
if (!this.hceBackgroundReady) {
|
|
215
|
+
if (!this.sessionRunning) {
|
|
216
|
+
promise.reject("err_no_session", "No session is active.");
|
|
217
|
+
return;
|
|
218
|
+
} else if (!this.hceRunning) {
|
|
219
|
+
promise.reject("err_no_hce", "HCE is not running.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
213
223
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
224
|
+
Log.d(TAG, "respondAPDU(): Send to service");
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
serviceCb.onRespondAPDU(rapdu);
|
|
228
|
+
} catch (IllegalStateException e) {
|
|
229
|
+
promise.reject("err_no_capdu", "There is no C-APDU to respond to at the time.");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
219
232
|
|
|
220
233
|
promise.resolve(null);
|
|
221
234
|
}
|
|
222
235
|
|
|
223
236
|
@Override
|
|
224
237
|
public void isHCERunning(Promise promise) {
|
|
225
|
-
|
|
226
|
-
promise.resolve(
|
|
238
|
+
Log.d(TAG, "RTNHCEAndroidModule:isHCERunning");
|
|
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/ios/RTNHCEModuleIOS.mm
CHANGED
|
@@ -86,6 +86,10 @@
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
- (void)initBackgroundHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
90
|
+
reject(@"err_platform_unsupported", @"Background HCE is not supported on iOS.", nil);
|
|
91
|
+
}
|
|
92
|
+
|
|
89
93
|
- (void)startHCE:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject {
|
|
90
94
|
if (@available(iOS 17.4, *)) {
|
|
91
95
|
return [hcemoduleios startHCEWithResolve:resolve reject:reject];
|
|
@@ -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/js/NativeHCEModule.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type {TurboModule, CodegenTypes} from 'react-native';
|
|
2
2
|
import {TurboModuleRegistry} from 'react-native';
|
|
3
3
|
|
|
4
|
-
export type HCEModuleEventType =
|
|
4
|
+
export type HCEModuleEventType =
|
|
5
|
+
'sessionStarted'
|
|
5
6
|
| 'sessionInvalidated'
|
|
6
7
|
/**
|
|
7
8
|
* iOS: Whether the reader field was detected.
|
|
@@ -19,11 +20,20 @@ export type HCEModuleEventType = 'sessionStarted'
|
|
|
19
20
|
| 'received';
|
|
20
21
|
export type HCEModuleStopReason = 'success' | 'failure';
|
|
21
22
|
|
|
23
|
+
export type HCEModuleBackgroundEventType =
|
|
24
|
+
'readerDeselected'
|
|
25
|
+
| 'received';
|
|
26
|
+
|
|
22
27
|
export type HCEModuleEvent = {
|
|
23
28
|
type: HCEModuleEventType
|
|
24
29
|
arg: string | null,
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
export type HCEModuleBackgroundEvent = {
|
|
33
|
+
type: HCEModuleBackgroundEventType
|
|
34
|
+
arg: string | null,
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
export interface Spec extends TurboModule {
|
|
28
38
|
/**
|
|
29
39
|
* Checks if a given smartphone supports required HCE features.
|
|
@@ -63,6 +73,8 @@ export interface Spec extends TurboModule {
|
|
|
63
73
|
*/
|
|
64
74
|
isSessionRunning(): boolean;
|
|
65
75
|
|
|
76
|
+
initBackgroundHCE(): Promise<void>;
|
|
77
|
+
|
|
66
78
|
/**
|
|
67
79
|
* iOS: Start the card emulation.
|
|
68
80
|
* Android: Enable HCE service.
|
|
@@ -84,6 +96,7 @@ export interface Spec extends TurboModule {
|
|
|
84
96
|
isHCERunning(): Promise<boolean>;
|
|
85
97
|
|
|
86
98
|
readonly onEvent: CodegenTypes.EventEmitter<HCEModuleEvent>;
|
|
99
|
+
readonly onBackgroundEvent: CodegenTypes.EventEmitter<HCEModuleBackgroundEvent>;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
export default TurboModuleRegistry.getEnforcing<Spec>(
|
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,11 +0,0 @@
|
|
|
1
|
-
package com.itsecrnd.rtnhceandroid;
|
|
2
|
-
|
|
3
|
-
public class IntentKeys {
|
|
4
|
-
public static final String ACTION_RECEIVE_C_APDU = "com.itsecrnd.rtnhceandroid.ACTION_RECEIVE_C_APDU";
|
|
5
|
-
public static final String ACTION_SEND_R_APDU = "com.itsecrnd.rtnhceandroid.ACTION_SEND_R_APDU";
|
|
6
|
-
public static final String ACTION_READER_DETECT = "com.itsecrnd.rtnhceandroid.ACTION_READER_DETECT";
|
|
7
|
-
public static final String ACTION_READER_LOST = "com.itsecrnd.rtnhceandroid.ACTION_READER_LOST";
|
|
8
|
-
public static final String KEY_AUTH = "auth";
|
|
9
|
-
public static final String KEY_CAPDU = "capdu";
|
|
10
|
-
public static final String KEY_RAPDU = "rapdu";
|
|
11
|
-
}
|