@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 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 `@icedevml/react-native-host-card-emulation` package within your React Native project. Then, follow the subsections below for each platform that you need to support.
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="true" />
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
- 1. Subscribe to the event stream (there is a single global event stream for the entire module):
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
- NativeHCEModule?.onEvent(async (event: HCEModuleEvent) => {
106
- switch (event.type) {
107
- /* ... your handler here ... */
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. You can start the HCE straight from that event:
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
- 3. Calling `startHCE()` causes:
124
- * iOS: Launch of the NFC scan system UI. The smartphone will start listening for C-APDUs from the readers right after that UI appears on screen. 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.
125
- * Android: No user interface appears (you have to implement it on your own scanning UI, within your app). 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.
126
- 4. Optionally, you can handle `readerDetected` and `readerDeselected events to enhance user's experience.
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
- 5. Handle CAPDU
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
- // let's say we always want to respond with [0x0A] + Status Word 0x9000
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
- 6. (iOS) Optionally, if you need to utilize `NFCPresentmentIntentAssertion` then at any time you can call:
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
- > [!NOTE]
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.
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
- > [!NOTE]
161
- > 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).
162
- > Check it out in order to understand what methods you can call against the module and what are the expected parameters/return values.
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 != 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)) {
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
- String encKey;
91
+
89
92
  this.sessionRunning = false;
90
93
  this.hceRunning = false;
91
94
  this.hceBreakConnection = false;
92
95
  this.hceDeselected = true;
93
96
 
94
- try {
95
- encSecretKey = AESGCMUtil.generateKey();
96
- encKey = BinaryUtils.ByteArrayToHexString(encSecretKey.getEncoded());
97
- } catch (Exception e) {
98
- throw new RuntimeException("Failed to generate encryption key.", e);
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
- getReactApplicationContext().getApplicationContext().unregisterReceiver(receiver);
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
- && pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
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
- String encRapdu = AESGCMUtil.encryptData(encSecretKey, rapdu);
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("auth", AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
217
- intent.putExtra("rapdu", encRapdu);
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
- // unsupported on Android, always true
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
@@ -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 java.util.Locale;
22
+ import com.itsecrnd.rtnhceandroid.util.BinaryUtils;
23
23
 
24
- import javax.crypto.SecretKey;
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
- AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_AUTH));
36
+ if (action != null && action.equals(ACTION_SEND_R_APDU)) {
37
+ String rapdu = intent.getStringExtra(KEY_RAPDU);
41
38
 
42
- if (action.equals(ACTION_SEND_R_APDU)) {
43
- String rapdu = AESGCMUtil.decryptData(encSecretKey, intent.getStringExtra(KEY_RAPDU));
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 = AESGCMUtil.encryptData(encSecretKey, BinaryUtils.ByteArrayToHexString(command));
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(receiver, filter, RECEIVER_EXPORTED);
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
- intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
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
- intent.putExtra(KEY_AUTH, AESGCMUtil.encryptData(encSecretKey, AESGCMUtil.randomString()));
105
- getApplicationContext().sendBroadcast(intent);
88
+ getApplicationContext().sendBroadcast(intent, PERMISSION_HCE_BROADCAST);
106
89
  }
107
90
  }
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- package com.itsecrnd.rtnhceandroid;
23
+ package com.itsecrnd.rtnhceandroid.util;
24
24
 
25
25
  public class BinaryUtils {
26
26
  /**
@@ -13,13 +13,8 @@ extension StringProtocol {
13
13
  }
14
14
 
15
15
  extension Data {
16
- struct HexEncodingOptions: OptionSet {
17
- let rawValue: Int
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@icedevml/react-native-host-card-emulation",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Host Card Emulation (HCE) React Native Turbo Module for iOS and Android platforms.",
5
5
  "react-native": "js/index",
6
6
  "source": "js/index",
@@ -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
- }