@pagopa/io-react-native-iso18013 0.7.0 → 0.8.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/IoReactNativeIso18013.podspec +1 -1
- package/README.md +1 -1
- package/android/build.gradle +3 -2
- package/android/gradle.properties +3 -3
- package/android/src/main/AndroidManifest.xml +17 -0
- package/android/src/main/AndroidManifestNew.xml +17 -0
- package/android/src/main/java/com/ioreactnativeiso18013/Enums.kt +42 -0
- package/android/src/main/java/com/ioreactnativeiso18013/IoNfcEngagementService.kt +17 -0
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +326 -132
- package/android/src/main/res/values/strings.xml +5 -0
- package/android/src/main/res/xml/nfc_engagement_apdu_service.xml +9 -0
- package/ios/IoReactNativeIso18013.mm +8 -9
- package/ios/IoReactNativeIso18013.swift +264 -191
- package/lib/module/cbor/cbor/README.md +3 -2
- package/lib/module/cbor/cose/README.md +7 -5
- package/lib/module/iso18013/iso18013-5/README.md +162 -42
- package/lib/module/iso18013/iso18013-5/error.js +1 -3
- package/lib/module/iso18013/iso18013-5/error.js.map +1 -1
- package/lib/module/iso18013/iso18013-5/index.js +1 -1
- package/lib/module/iso18013/iso18013-5/index.js.map +1 -1
- package/lib/module/iso18013/iso18013-5/proximity.js +29 -19
- package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -1
- package/lib/module/iso18013/iso18013-7/README.md +2 -2
- package/lib/module/schema.js +1 -1
- package/lib/module/schema.js.map +1 -1
- package/lib/typescript/src/cbor/cbor/error.d.ts +5 -5
- package/lib/typescript/src/cbor/cose/error.d.ts +5 -5
- package/lib/typescript/src/iso18013/iso18013-5/error.d.ts +9 -9
- package/lib/typescript/src/iso18013/iso18013-5/error.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +32 -13
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-7/error.d.ts +5 -5
- package/lib/typescript/src/schema.d.ts +7 -17
- package/lib/typescript/src/schema.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/cbor/cbor/README.md +3 -2
- package/src/cbor/cose/README.md +7 -5
- package/src/iso18013/iso18013-5/README.md +162 -42
- package/src/iso18013/iso18013-5/error.ts +0 -2
- package/src/iso18013/iso18013-5/index.ts +3 -2
- package/src/iso18013/iso18013-5/proximity.ts +52 -23
- package/src/iso18013/iso18013-7/README.md +2 -2
- package/src/schema.ts +1 -1
|
@@ -10,56 +10,132 @@ import com.facebook.react.bridge.ReadableMap
|
|
|
10
10
|
import com.facebook.react.bridge.ReadableType
|
|
11
11
|
import com.facebook.react.bridge.WritableMap
|
|
12
12
|
import it.pagopa.io.wallet.proximity.OpenID4VP
|
|
13
|
+
import it.pagopa.io.wallet.proximity.ProximityLogger
|
|
13
14
|
import it.pagopa.io.wallet.proximity.bluetooth.BleRetrievalMethod
|
|
15
|
+
import it.pagopa.io.wallet.proximity.engagement.EngagementListener
|
|
16
|
+
import it.pagopa.io.wallet.proximity.nfc.NfcEngagementEvent
|
|
17
|
+
import it.pagopa.io.wallet.proximity.nfc.NfcEngagementEventBus
|
|
18
|
+
import it.pagopa.io.wallet.proximity.nfc.NfcEngagementService
|
|
19
|
+
import it.pagopa.io.wallet.proximity.nfc.NfcRetrievalMethod
|
|
14
20
|
import it.pagopa.io.wallet.proximity.qr_code.QrEngagement
|
|
15
|
-
import it.pagopa.io.wallet.proximity.qr_code.QrEngagementListener
|
|
16
21
|
import it.pagopa.io.wallet.proximity.request.DocRequested
|
|
17
22
|
import it.pagopa.io.wallet.proximity.response.ResponseGenerator
|
|
23
|
+
import it.pagopa.io.wallet.proximity.retrieval.sendErrorResponse
|
|
18
24
|
import it.pagopa.io.wallet.proximity.session_data.SessionDataStatus
|
|
19
25
|
import it.pagopa.io.wallet.proximity.wrapper.DeviceRetrievalHelperWrapper
|
|
26
|
+
import kotlinx.coroutines.CoroutineScope
|
|
27
|
+
import kotlinx.coroutines.Dispatchers
|
|
28
|
+
import kotlinx.coroutines.Job
|
|
29
|
+
import kotlinx.coroutines.SupervisorJob
|
|
30
|
+
import kotlinx.coroutines.launch
|
|
20
31
|
|
|
21
32
|
class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
22
33
|
ReactContextBaseJavaModule(reactContext) {
|
|
23
34
|
|
|
35
|
+
private var qrEngagement: QrEngagement? = null
|
|
36
|
+
private var deviceRetrievalHelper: DeviceRetrievalHelperWrapper? = null
|
|
37
|
+
|
|
38
|
+
private var nfcEventJob: Job? = null
|
|
39
|
+
private val nfcScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
|
40
|
+
|
|
41
|
+
private var sessionRetrievalMethod: RetrievalMethod? = null
|
|
42
|
+
|
|
43
|
+
// Session transcript received by the NFC engagement, stored for the current session
|
|
44
|
+
private var sessionTranscript: ByteArray? = null
|
|
45
|
+
|
|
46
|
+
init {
|
|
47
|
+
ProximityLogger.enabled = BuildConfig.DEBUG
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
override fun getName(): String {
|
|
25
51
|
return NAME
|
|
26
52
|
}
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
override fun invalidate() {
|
|
55
|
+
super.invalidate()
|
|
56
|
+
nfcEventJob?.cancel()
|
|
57
|
+
nfcEventJob = null
|
|
58
|
+
nfcScope.coroutineContext[Job]?.cancel()
|
|
59
|
+
reactApplicationContext.currentActivity?.let { NfcEngagementService.disable(it) }
|
|
60
|
+
}
|
|
30
61
|
|
|
31
62
|
/**
|
|
32
|
-
* Starts the proximity flow by allocating the necessary resources
|
|
63
|
+
* Starts the NFC proximity flow by allocating the necessary resources, enabling NFC
|
|
64
|
+
* engagement/HCE and initializing the Bluetooth stack (if required).
|
|
33
65
|
* Resolves to true or rejects with an error code defined in [ModuleErrorCodes].
|
|
34
66
|
* @param peripheralMode whether the device is in peripheral mode. Defaults to true
|
|
35
67
|
* @param centralClientMode whether the device is in central client mode. Defaults to false
|
|
36
68
|
* @param clearBleCache whether the BLE cache should be cleared. Defaults to true
|
|
37
69
|
* @param certificates two-dimensional array of base64 strings representing DER encoded X.509 certificate which are used to authenticate the verifier app
|
|
70
|
+
* @param enagementModes array of strings representing the engagement modes (e.g. QR_CODE, BLE) to be used.
|
|
71
|
+
* @param retrievalMethods array of strings representing the retrieval methods (e.g. NFC, BLE) to be used.
|
|
38
72
|
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
39
73
|
*/
|
|
40
74
|
@ReactMethod
|
|
41
|
-
fun
|
|
75
|
+
fun startEngagement(
|
|
42
76
|
peripheralMode: Boolean,
|
|
43
77
|
centralClientMode: Boolean,
|
|
44
78
|
clearBleCache: Boolean,
|
|
45
79
|
certificates: ReadableArray,
|
|
80
|
+
enagementModes: ReadableArray,
|
|
81
|
+
retrievalMethods: ReadableArray,
|
|
46
82
|
promise: Promise
|
|
47
83
|
) {
|
|
48
84
|
try {
|
|
49
|
-
val
|
|
50
|
-
peripheralServerMode = peripheralMode,
|
|
51
|
-
centralClientMode = centralClientMode,
|
|
52
|
-
clearBleCache = clearBleCache
|
|
53
|
-
)
|
|
85
|
+
val activity = checkNotNull(reactApplicationContext.currentActivity) { "No activity available" }
|
|
54
86
|
|
|
55
87
|
val certificatesList = parseCertificates(certificates)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
val parsedEngagementModes = parseEngagementModes(enagementModes)
|
|
89
|
+
val parsedRetrievalMethods = parseRetrievalMethods(retrievalMethods)
|
|
90
|
+
|
|
91
|
+
val methods = buildList {
|
|
92
|
+
if (parsedRetrievalMethods.any { it == RetrievalMethod.BLE }) {
|
|
93
|
+
add(
|
|
94
|
+
BleRetrievalMethod(
|
|
95
|
+
peripheralServerMode = peripheralMode,
|
|
96
|
+
centralClientMode = centralClientMode,
|
|
97
|
+
clearBleCache = clearBleCache
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
if (parsedRetrievalMethods.any { it == RetrievalMethod.NFC }) {
|
|
102
|
+
add(NfcRetrievalMethod())
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// NFC engagement
|
|
107
|
+
|
|
108
|
+
if (parsedEngagementModes.any { it == EngagementMode.NFC }) {
|
|
109
|
+
val readerTrustStore = certificatesList.takeIf { it.isNotEmpty() }
|
|
110
|
+
?.map { chain -> chain.map<ByteArray, Any> { it } }
|
|
111
|
+
|
|
112
|
+
check(
|
|
113
|
+
NfcEngagementEventBus.setupNfcService(
|
|
114
|
+
retrievalMethods = methods, readerTrustStore = readerTrustStore
|
|
115
|
+
)
|
|
116
|
+
) { "Unable to setup NFC engagement service" }
|
|
117
|
+
|
|
118
|
+
val status = NfcEngagementService.enable(
|
|
119
|
+
activity = activity, preferredNfcEngSerCls = IoNfcEngagementService::class.java
|
|
120
|
+
)
|
|
121
|
+
check(status.canWork()) { "HCE not available: $status" }
|
|
122
|
+
setupNfcHandler()
|
|
123
|
+
sendEvent("onNfcStarted", null)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// QR Code engagement
|
|
127
|
+
|
|
128
|
+
if (parsedEngagementModes.any { it == EngagementMode.QR_CODE }) {
|
|
129
|
+
qrEngagement = QrEngagement.build(reactApplicationContext, methods).apply {
|
|
130
|
+
if (certificatesList.isNotEmpty()) {
|
|
131
|
+
withReaderTrustStore(certificatesList)
|
|
132
|
+
}
|
|
59
133
|
}
|
|
134
|
+
qrEngagement?.configure()
|
|
135
|
+
setupProximityHandler()
|
|
136
|
+
emitQrCodeString()
|
|
60
137
|
}
|
|
61
|
-
|
|
62
|
-
setupProximityHandler()
|
|
138
|
+
|
|
63
139
|
promise.resolve(true)
|
|
64
140
|
} catch (e: Exception) {
|
|
65
141
|
promise.reject(ModuleErrorCodes.START_ERROR, e.message, e)
|
|
@@ -67,22 +143,15 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
67
143
|
}
|
|
68
144
|
|
|
69
145
|
/**
|
|
70
|
-
*
|
|
71
|
-
* Resolves with a string containing the QR code or rejects with an error code defined in [ModuleErrorCodes].
|
|
72
|
-
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
146
|
+
* Generates and send QR Code string payload to React Native via `RCTEventEmitter`.
|
|
73
147
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED,NOT_INITIALIZED_ERROR_MESSAGE)
|
|
82
|
-
}
|
|
83
|
-
} catch (e: Exception) {
|
|
84
|
-
promise.reject(ModuleErrorCodes.GET_QR_CODE_ERROR, e.message, e)
|
|
85
|
-
}
|
|
148
|
+
private fun emitQrCodeString() {
|
|
149
|
+
val qrCodeString =
|
|
150
|
+
checkNotNull(qrEngagement?.getQrCodeString()) { NOT_INITIALIZED_ERROR_MESSAGE }
|
|
151
|
+
|
|
152
|
+
val data: WritableMap = Arguments.createMap()
|
|
153
|
+
data.putString("data", qrCodeString)
|
|
154
|
+
sendEvent("onQrCodeString", data)
|
|
86
155
|
}
|
|
87
156
|
|
|
88
157
|
/**
|
|
@@ -96,13 +165,21 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
96
165
|
fun close(promise: Promise) {
|
|
97
166
|
try {
|
|
98
167
|
qrEngagement?.close()
|
|
168
|
+
qrEngagement = null
|
|
99
169
|
deviceRetrievalHelper?.disconnect()
|
|
170
|
+
deviceRetrievalHelper = null
|
|
171
|
+
sessionRetrievalMethod = null
|
|
172
|
+
sessionTranscript = null
|
|
173
|
+
nfcEventJob?.cancel()
|
|
174
|
+
nfcEventJob = null
|
|
175
|
+
reactApplicationContext.currentActivity?.let { NfcEngagementService.disable(it) }
|
|
100
176
|
promise.resolve(true)
|
|
101
177
|
} catch (e: Exception) {
|
|
102
|
-
promise.reject(ModuleErrorCodes.CLOSE_ERROR, message=e.message, e)
|
|
178
|
+
promise.reject(ModuleErrorCodes.CLOSE_ERROR, message = e.message, e)
|
|
103
179
|
}
|
|
104
180
|
}
|
|
105
181
|
|
|
182
|
+
|
|
106
183
|
/**
|
|
107
184
|
* Sends an error response during the presentation according to the SessionData status codes
|
|
108
185
|
* defined in table 20 of the ISO18013-5 standard.
|
|
@@ -118,19 +195,20 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
118
195
|
@ReactMethod
|
|
119
196
|
fun sendErrorResponse(code: Double, promise: Promise) {
|
|
120
197
|
try {
|
|
121
|
-
|
|
122
|
-
val sessionDataStatus =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
198
|
+
deviceRetrievalHelper?.let { drh ->
|
|
199
|
+
val sessionDataStatus =
|
|
200
|
+
checkNotNull(SessionDataStatus.entries.find { it.value == code.toLong() }) {
|
|
201
|
+
"Invalid status code"
|
|
202
|
+
}
|
|
203
|
+
drh.sendErrorResponse(sessionDataStatus)
|
|
204
|
+
promise.resolve(true)
|
|
129
205
|
} ?: run {
|
|
130
|
-
promise.reject(
|
|
206
|
+
promise.reject(
|
|
207
|
+
ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
|
|
208
|
+
)
|
|
131
209
|
}
|
|
132
210
|
} catch (e: Exception) {
|
|
133
|
-
promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message=e.message, e)
|
|
211
|
+
promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message = e.message, e)
|
|
134
212
|
}
|
|
135
213
|
}
|
|
136
214
|
|
|
@@ -162,34 +240,48 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
162
240
|
*/
|
|
163
241
|
@ReactMethod
|
|
164
242
|
fun generateResponse(
|
|
165
|
-
documents: ReadableArray,
|
|
166
|
-
acceptedFields: ReadableMap,
|
|
167
|
-
promise: Promise
|
|
243
|
+
documents: ReadableArray, acceptedFields: ReadableMap, promise: Promise
|
|
168
244
|
) {
|
|
169
245
|
try {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
246
|
+
when (sessionRetrievalMethod) {
|
|
247
|
+
|
|
248
|
+
// For NFC retrievals we use the session transcript received during the device connection event
|
|
249
|
+
RetrievalMethod.NFC -> {
|
|
250
|
+
val sessionTranscript = checkNotNull(sessionTranscript) { "Session transcript is null" }
|
|
251
|
+
createAndResolveResponse(
|
|
252
|
+
sessionTranscript = sessionTranscript,
|
|
253
|
+
documents = documents,
|
|
254
|
+
acceptedFields = acceptedFields,
|
|
255
|
+
errorCode = ModuleErrorCodes.GENERATE_RESPONSE_ERROR,
|
|
256
|
+
promise = promise
|
|
257
|
+
)
|
|
258
|
+
}
|
|
183
259
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
260
|
+
// For BLE retrievals we use the session transcript obtained from the DeviceRetrievalHelper
|
|
261
|
+
RetrievalMethod.BLE -> {
|
|
262
|
+
deviceRetrievalHelper?.let { drh ->
|
|
263
|
+
createAndResolveResponse(
|
|
264
|
+
sessionTranscript = drh.sessionTranscript(),
|
|
265
|
+
documents = documents,
|
|
266
|
+
acceptedFields = acceptedFields,
|
|
267
|
+
errorCode = ModuleErrorCodes.GENERATE_RESPONSE_ERROR,
|
|
268
|
+
promise = promise
|
|
269
|
+
)
|
|
270
|
+
} ?: run {
|
|
271
|
+
promise.reject(
|
|
272
|
+
ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
else -> {
|
|
278
|
+
promise.reject(
|
|
279
|
+
ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message = "Invalid retrieval method"
|
|
280
|
+
)
|
|
281
|
+
}
|
|
190
282
|
}
|
|
191
283
|
} catch (e: Exception) {
|
|
192
|
-
promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message=e.message, e)
|
|
284
|
+
promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message = e.message, e)
|
|
193
285
|
}
|
|
194
286
|
}
|
|
195
287
|
|
|
@@ -203,14 +295,37 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
203
295
|
@ReactMethod
|
|
204
296
|
fun sendResponse(response: String, promise: Promise) {
|
|
205
297
|
try {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
298
|
+
val responseBytes = Base64Utils.decodeBase64(response)
|
|
299
|
+
|
|
300
|
+
when (sessionRetrievalMethod) {
|
|
301
|
+
|
|
302
|
+
// For NFC retrievals we use NfcEngagementEventBus to send the response over NFC
|
|
303
|
+
RetrievalMethod.NFC -> {
|
|
304
|
+
check(
|
|
305
|
+
NfcEngagementEventBus.sendDocumentResponse(response = responseBytes)
|
|
306
|
+
) { "Unable to send response over NFC" }
|
|
307
|
+
promise.resolve(true)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// For BLE retrievals we use the DeviceRetrievalHelper to send the response over BLE
|
|
311
|
+
RetrievalMethod.BLE -> {
|
|
312
|
+
deviceRetrievalHelper?.let { drh ->
|
|
313
|
+
drh.sendResponse(responseBytes, 0L)
|
|
314
|
+
promise.resolve(true)
|
|
315
|
+
} ?: run {
|
|
316
|
+
promise.reject(
|
|
317
|
+
ModuleErrorCodes.DRH_NOT_DEFINED, message = NOT_INITIALIZED_ERROR_MESSAGE
|
|
318
|
+
)
|
|
319
|
+
}
|
|
213
320
|
}
|
|
321
|
+
|
|
322
|
+
else -> {
|
|
323
|
+
promise.reject(
|
|
324
|
+
ModuleErrorCodes.SEND_RESPONSE_ERROR, message = "Invalid retrieval method"
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
214
329
|
} catch (e: Exception) {
|
|
215
330
|
promise.reject(ModuleErrorCodes.SEND_RESPONSE_ERROR, e.message, e)
|
|
216
331
|
}
|
|
@@ -246,37 +361,25 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
246
361
|
*/
|
|
247
362
|
@ReactMethod
|
|
248
363
|
fun generateOID4VPDeviceResponse(
|
|
249
|
-
clientId: String,
|
|
250
|
-
|
|
251
|
-
|
|
364
|
+
clientId: String,
|
|
365
|
+
responseUri: String,
|
|
366
|
+
authorizationRequestNonce: String,
|
|
367
|
+
mdocGeneratedNonce: String,
|
|
368
|
+
documents: ReadableArray,
|
|
369
|
+
acceptedFields: ReadableMap,
|
|
370
|
+
promise: Promise
|
|
252
371
|
) {
|
|
253
372
|
try {
|
|
254
|
-
val sessionTranscript =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
|
|
266
|
-
|
|
267
|
-
val responseGenerator = ResponseGenerator(sessionTranscript)
|
|
268
|
-
responseGenerator.createResponse(
|
|
269
|
-
documentsParsed,
|
|
270
|
-
parsedAcceptedFields,
|
|
271
|
-
object : ResponseGenerator.Response {
|
|
272
|
-
override fun onResponseGenerated(response: ByteArray) {
|
|
273
|
-
promise.resolve(Base64Utils.encodeBase64(response))
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
override fun onError(message: String) {
|
|
277
|
-
promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, message)
|
|
278
|
-
}
|
|
279
|
-
}
|
|
373
|
+
val sessionTranscript = OpenID4VP(
|
|
374
|
+
clientId, responseUri, authorizationRequestNonce, mdocGeneratedNonce
|
|
375
|
+
).createSessionTranscript()
|
|
376
|
+
|
|
377
|
+
createAndResolveResponse(
|
|
378
|
+
sessionTranscript = sessionTranscript,
|
|
379
|
+
documents = documents,
|
|
380
|
+
acceptedFields = acceptedFields,
|
|
381
|
+
errorCode = ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR,
|
|
382
|
+
promise = promise
|
|
280
383
|
)
|
|
281
384
|
} catch (e: Exception) {
|
|
282
385
|
promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, e.message, e)
|
|
@@ -293,12 +396,12 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
293
396
|
* onError: Emitted when an error occurs. Carries a payload containing the error data.
|
|
294
397
|
*/
|
|
295
398
|
private fun setupProximityHandler() {
|
|
296
|
-
qrEngagement?.withListener(object :
|
|
399
|
+
qrEngagement?.withListener(object : EngagementListener {
|
|
297
400
|
/**
|
|
298
401
|
* This event currently doesn't get called due to an issue with the underlying native library.
|
|
299
402
|
*/
|
|
300
403
|
override fun onDeviceConnecting() {
|
|
301
|
-
sendEvent("onDeviceConnecting",
|
|
404
|
+
sendEvent("onDeviceConnecting", null)
|
|
302
405
|
}
|
|
303
406
|
|
|
304
407
|
override fun onDeviceConnected(deviceRetrievalHelper: DeviceRetrievalHelperWrapper) {
|
|
@@ -313,8 +416,11 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
313
416
|
}
|
|
314
417
|
|
|
315
418
|
override fun onDocumentRequestReceived(request: String?, sessionsTranscript: ByteArray) {
|
|
419
|
+
sessionRetrievalMethod = RetrievalMethod.BLE
|
|
420
|
+
|
|
316
421
|
val data: WritableMap = Arguments.createMap()
|
|
317
|
-
data.putString("data", request)
|
|
422
|
+
data.putString("data", request ?: "")
|
|
423
|
+
data.putString("retrievalMethod", sessionRetrievalMethod?.bridgeValue)
|
|
318
424
|
sendEvent("onDocumentRequestReceived", data)
|
|
319
425
|
}
|
|
320
426
|
|
|
@@ -324,6 +430,90 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
324
430
|
})
|
|
325
431
|
}
|
|
326
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Sets the NFC handler along with the possible dispatched events and their callbacks.
|
|
435
|
+
* The events are then sent to React Native via `RCTEventEmitter`.
|
|
436
|
+
* onDeviceConnecting: Emitted when the device is connecting to the verifier app.
|
|
437
|
+
* onDeviceConnected: Emitted when the device is connected to the verifier app.
|
|
438
|
+
* onDocumentRequestReceived: Emitted when a document request is received from the verifier app. Carries a payload containing the request data.
|
|
439
|
+
* onDeviceDisconnected: Emitted when the device is disconnected from the verifier app.
|
|
440
|
+
* onError: Emitted when an error occurs. Carries a payload containing the error data.
|
|
441
|
+
* onNfcStopped: Emitted when the NFC handler is stopped.
|
|
442
|
+
*/
|
|
443
|
+
private fun setupNfcHandler() {
|
|
444
|
+
nfcEventJob?.cancel()
|
|
445
|
+
nfcEventJob = nfcScope.launch {
|
|
446
|
+
NfcEngagementEventBus.events.collect { event ->
|
|
447
|
+
when (event) {
|
|
448
|
+
is NfcEngagementEvent.Connecting -> {
|
|
449
|
+
sendEvent("onDeviceConnecting", null)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
is NfcEngagementEvent.Connected -> {
|
|
453
|
+
deviceRetrievalHelper = event.device
|
|
454
|
+
sendEvent("onDeviceConnected", null)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
is NfcEngagementEvent.Error -> {
|
|
458
|
+
val data: WritableMap = Arguments.createMap()
|
|
459
|
+
data.putString("error", event.error.message ?: "")
|
|
460
|
+
sendEvent("onError", data)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
is NfcEngagementEvent.Disconnected -> sendEvent(
|
|
464
|
+
"onDeviceDisconnected", null
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
is NfcEngagementEvent.DocumentRequestReceived -> {
|
|
468
|
+
sessionRetrievalMethod = if (event.onlyNfc) RetrievalMethod.NFC else RetrievalMethod.BLE
|
|
469
|
+
sessionTranscript = event.sessionTranscript
|
|
470
|
+
|
|
471
|
+
val data: WritableMap = Arguments.createMap()
|
|
472
|
+
data.putString("data", event.request)
|
|
473
|
+
data.putString("retrievalMethod", sessionRetrievalMethod?.bridgeValue)
|
|
474
|
+
sendEvent("onDocumentRequestReceived", data)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
is NfcEngagementEvent.NotSupported -> sendEvent("onNfcStopped", null)
|
|
478
|
+
|
|
479
|
+
is NfcEngagementEvent.NfcOnlyEventListener -> { /* NOT HANDLED */
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
is NfcEngagementEvent.DocumentSent -> sendEvent("onDeviceDisconnected", null)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Utility function that generates the response using the ResponseGeneration from a session
|
|
490
|
+
* transcript
|
|
491
|
+
* @param sessionTranscript the session transcript to be used to generate the response
|
|
492
|
+
* @param documents the documents to be included in the response
|
|
493
|
+
* @param acceptedFields the accepted fields to be presented
|
|
494
|
+
* @param errorCode the error code to be used in case of error
|
|
495
|
+
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
496
|
+
*/
|
|
497
|
+
private fun createAndResolveResponse(
|
|
498
|
+
sessionTranscript: ByteArray,
|
|
499
|
+
documents: ReadableArray,
|
|
500
|
+
acceptedFields: ReadableMap,
|
|
501
|
+
errorCode: String,
|
|
502
|
+
promise: Promise
|
|
503
|
+
) {
|
|
504
|
+
val docRequestedList = parseDocRequested(documents)
|
|
505
|
+
val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
|
|
506
|
+
ResponseGenerator(sessionTranscript).createResponse(
|
|
507
|
+
docRequestedList, parsedAcceptedFields, object : ResponseGenerator.Response {
|
|
508
|
+
override fun onResponseGenerated(response: ByteArray) =
|
|
509
|
+
promise.resolve(Base64Utils.encodeBase64(response))
|
|
510
|
+
|
|
511
|
+
override fun onError(message: String) {
|
|
512
|
+
promise.reject(errorCode, message)
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
327
517
|
/**
|
|
328
518
|
* Wrapper function to send an event via `RCTEventEmitter`
|
|
329
519
|
* @param eventName - The event name
|
|
@@ -334,31 +524,30 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
334
524
|
.emit(eventName, data)
|
|
335
525
|
}
|
|
336
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Keep: Required for RN built in Event Emitter Calls.
|
|
529
|
+
* This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
530
|
+
*/
|
|
337
531
|
@ReactMethod
|
|
338
|
-
fun addListener(eventName: String?)
|
|
339
|
-
/* Keep: Required for RN built in Event Emitter Calls.
|
|
340
|
-
This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
341
|
-
*/
|
|
342
|
-
}
|
|
532
|
+
fun addListener(eventName: String?) = Unit
|
|
343
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Keep: Required for RN built in Event Emitter Calls.
|
|
536
|
+
* This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
537
|
+
*/
|
|
344
538
|
@ReactMethod
|
|
345
|
-
fun removeListeners(count: Int?)
|
|
346
|
-
/*Keep: Required for RN built in Event Emitter Calls.
|
|
347
|
-
This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
348
|
-
*/
|
|
349
|
-
}
|
|
539
|
+
fun removeListeners(count: Int?) = Unit
|
|
350
540
|
|
|
351
541
|
companion object {
|
|
352
542
|
const val NAME = "IoReactNativeIso18013"
|
|
353
|
-
const val NOT_INITIALIZED_ERROR_MESSAGE =
|
|
543
|
+
const val NOT_INITIALIZED_ERROR_MESSAGE =
|
|
544
|
+
"Resources not initialized properly, call the start method before this one."
|
|
354
545
|
|
|
355
546
|
// Errors which this module uses to reject a promise
|
|
356
547
|
private object ModuleErrorCodes {
|
|
357
548
|
// ISO18013-5 related errors
|
|
358
549
|
const val DRH_NOT_DEFINED = "DRH_NOT_DEFINED"
|
|
359
|
-
const val QR_ENGAGEMENT_NOT_DEFINED = "QR_ENGAGEMENT_NOT_DEFINED"
|
|
360
550
|
const val START_ERROR = "START_ERROR"
|
|
361
|
-
const val GET_QR_CODE_ERROR = "GET_QR_CODE_ERROR"
|
|
362
551
|
const val GENERATE_RESPONSE_ERROR = "GENERATE_RESPONSE_ERROR"
|
|
363
552
|
const val SEND_RESPONSE_ERROR = "SEND_RESPONSE_ERROR"
|
|
364
553
|
const val SEND_ERROR_RESPONSE_ERROR = "SEND_ERROR_RESPONSE_ERROR"
|
|
@@ -390,7 +579,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
390
579
|
* at least a credential type. If a namespace doesn't contain at least one value.
|
|
391
580
|
* @returns String representation of [acceptedFields]
|
|
392
581
|
*/
|
|
393
|
-
fun parseAcceptedFields(acceptedFields: ReadableMap): String {
|
|
582
|
+
internal fun parseAcceptedFields(acceptedFields: ReadableMap): String {
|
|
394
583
|
try {
|
|
395
584
|
// Loop for each credential and throw if something different than map is found
|
|
396
585
|
acceptedFields.entryIterator.forEach { credentialEntry ->
|
|
@@ -411,7 +600,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
411
600
|
}
|
|
412
601
|
|
|
413
602
|
// If no field is found then throw
|
|
414
|
-
if(!namespaceValue.entryIterator.hasNext()){
|
|
603
|
+
if (!namespaceValue.entryIterator.hasNext()) {
|
|
415
604
|
throw IllegalArgumentException("Credential '$credentialName' with namespace `$namespaceName` must define at least one field")
|
|
416
605
|
}
|
|
417
606
|
|
|
@@ -432,6 +621,7 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
432
621
|
}
|
|
433
622
|
}
|
|
434
623
|
|
|
624
|
+
|
|
435
625
|
/**
|
|
436
626
|
* Utility function which extracts the document shape we expect to receive from the bridge
|
|
437
627
|
* in the one expected by {DocRequested}.
|
|
@@ -451,18 +641,15 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
451
641
|
val issuerSignedContentStr = entry.getString("issuerSignedContent")
|
|
452
642
|
val docType = entry.getString("docType")
|
|
453
643
|
|
|
454
|
-
if (
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
docType == null || entry.getType("docType") != ReadableType.String
|
|
644
|
+
if (alias == null || entry.getType("alias") != ReadableType.String || issuerSignedContentStr == null || entry.getType(
|
|
645
|
+
"issuerSignedContent"
|
|
646
|
+
) != ReadableType.String || docType == null || entry.getType("docType") != ReadableType.String
|
|
458
647
|
) throw IllegalArgumentException("Unable to decode the provided documents at index $i")
|
|
459
648
|
|
|
460
649
|
val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
|
|
461
650
|
|
|
462
651
|
DocRequested(
|
|
463
|
-
issuerSignedContent,
|
|
464
|
-
alias,
|
|
465
|
-
docType
|
|
652
|
+
issuerSignedContent, alias, docType
|
|
466
653
|
)
|
|
467
654
|
}.toTypedArray()
|
|
468
655
|
} catch (e: Exception) {
|
|
@@ -482,18 +669,22 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
482
669
|
* can throw and if it does rethrow an exception with information on its position
|
|
483
670
|
*/
|
|
484
671
|
(0 until certificates.size()).mapNotNull { chainIndex ->
|
|
485
|
-
val chain = runCatching { certificates.getArray(chainIndex) }
|
|
486
|
-
|
|
487
|
-
|
|
672
|
+
val chain = runCatching { certificates.getArray(chainIndex) }.getOrElse {
|
|
673
|
+
throw IllegalArgumentException(
|
|
674
|
+
"Certificate chain at $chainIndex is not an array", it
|
|
675
|
+
)
|
|
676
|
+
} ?: throw IllegalArgumentException("Certificate chain at index $chainIndex is null")
|
|
488
677
|
|
|
489
678
|
/**
|
|
490
679
|
* Map each chain certificate and remove null entries. On each certificate call the getString
|
|
491
680
|
* method which can throw and if it does rethrow an exception with information on its position
|
|
492
681
|
*/
|
|
493
682
|
(0 until chain.size()).mapNotNull { certIndex ->
|
|
494
|
-
val base64 = runCatching { chain.getString(certIndex) }
|
|
495
|
-
|
|
496
|
-
|
|
683
|
+
val base64 = runCatching { chain.getString(certIndex) }.getOrElse {
|
|
684
|
+
throw IllegalArgumentException(
|
|
685
|
+
"Failed to get certificate string at chain $chainIndex, cert $certIndex", it
|
|
686
|
+
)
|
|
687
|
+
} ?: throw java.lang.IllegalArgumentException("Certificate at index $certIndex is null")
|
|
497
688
|
|
|
498
689
|
/**
|
|
499
690
|
* Decode the base64 string for each mapped certificate and if an error occurs rethrow
|
|
@@ -502,7 +693,10 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
502
693
|
runCatching {
|
|
503
694
|
Base64Utils.decodeBase64(base64)
|
|
504
695
|
}.getOrElse {
|
|
505
|
-
throw IllegalArgumentException(
|
|
696
|
+
throw IllegalArgumentException(
|
|
697
|
+
"Certificate at index $certIndex in the chain at index $chainIndex is not a valid base64 string",
|
|
698
|
+
it
|
|
699
|
+
)
|
|
506
700
|
}
|
|
507
701
|
}
|
|
508
702
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
android:description="@string/nfc_engagement_service_desc"
|
|
4
|
+
android:requireDeviceUnlock="true">
|
|
5
|
+
<aid-group android:description="@string/nfc_engagement_aid_group_desc" android:category="other">
|
|
6
|
+
<aid-filter android:name="D2760000850101" />
|
|
7
|
+
<aid-filter android:name="A0000002480400" />
|
|
8
|
+
</aid-group>
|
|
9
|
+
</host-apdu-service>
|