@pagopa/io-react-native-iso18013 0.1.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/IoReactNativeIso18013.podspec +29 -0
- package/LICENSE +20 -0
- package/README.md +30 -0
- package/android/build.gradle +83 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/ioreactnativeiso18013/Base64Utils.kt +56 -0
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeCborModule.kt +209 -0
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +439 -0
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Package.kt +17 -0
- package/ios/Base64Utils.swift +47 -0
- package/ios/IoReactNativeCbor.mm +32 -0
- package/ios/IoReactNativeCbor.swift +188 -0
- package/ios/IoReactNativeIso18013-Bridging-Header.h +2 -0
- package/ios/IoReactNativeIso18013.mm +43 -0
- package/ios/IoReactNativeIso18013.swift +491 -0
- package/lib/module/cbor/cbor/README.md +95 -0
- package/lib/module/cbor/cbor/decoder.js +51 -0
- package/lib/module/cbor/cbor/decoder.js.map +1 -0
- package/lib/module/cbor/cbor/failure.js +2 -0
- package/lib/module/cbor/cbor/failure.js.map +1 -0
- package/lib/module/cbor/cbor/index.js +5 -0
- package/lib/module/cbor/cbor/index.js.map +1 -0
- package/lib/module/cbor/cbor/schema.js +117 -0
- package/lib/module/cbor/cbor/schema.js.map +1 -0
- package/lib/module/cbor/cbor/schema.utils.js +16 -0
- package/lib/module/cbor/cbor/schema.utils.js.map +1 -0
- package/lib/module/cbor/cose/README.md +53 -0
- package/lib/module/cbor/cose/failure.js +2 -0
- package/lib/module/cbor/cose/failure.js.map +1 -0
- package/lib/module/cbor/cose/index.js +4 -0
- package/lib/module/cbor/cose/index.js.map +1 -0
- package/lib/module/cbor/cose/sign.js +23 -0
- package/lib/module/cbor/cose/sign.js.map +1 -0
- package/lib/module/cbor/index.js +13 -0
- package/lib/module/cbor/index.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/iso18013/index.js +14 -0
- package/lib/module/iso18013/index.js.map +1 -0
- package/lib/module/iso18013/iso18013-5/README.md +234 -0
- package/lib/module/iso18013/iso18013-5/index.js +5 -0
- package/lib/module/iso18013/iso18013-5/index.js.map +1 -0
- package/lib/module/iso18013/iso18013-5/proximity.js +112 -0
- package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -0
- package/lib/module/iso18013/iso18013-5/schema.js +79 -0
- package/lib/module/iso18013/iso18013-5/schema.js.map +1 -0
- package/lib/module/iso18013/iso18013-7/README.md +59 -0
- package/lib/module/iso18013/iso18013-7/index.js +4 -0
- package/lib/module/iso18013/iso18013-7/index.js.map +1 -0
- package/lib/module/iso18013/iso18013-7/remote.js +31 -0
- package/lib/module/iso18013/iso18013-7/remote.js.map +1 -0
- package/lib/module/iso18013/types.js +2 -0
- package/lib/module/iso18013/types.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/cbor/cbor/decoder.d.ts +35 -0
- package/lib/typescript/src/cbor/cbor/decoder.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cbor/failure.d.ts +15 -0
- package/lib/typescript/src/cbor/cbor/failure.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cbor/index.d.ts +4 -0
- package/lib/typescript/src/cbor/cbor/index.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cbor/schema.d.ts +2420 -0
- package/lib/typescript/src/cbor/cbor/schema.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cbor/schema.utils.d.ts +8 -0
- package/lib/typescript/src/cbor/cbor/schema.utils.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cose/failure.d.ts +15 -0
- package/lib/typescript/src/cbor/cose/failure.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cose/index.d.ts +3 -0
- package/lib/typescript/src/cbor/cose/index.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cose/sign.d.ts +19 -0
- package/lib/typescript/src/cbor/cose/sign.d.ts.map +1 -0
- package/lib/typescript/src/cbor/index.d.ts +2 -0
- package/lib/typescript/src/cbor/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/index.d.ts +3 -0
- package/lib/typescript/src/iso18013/index.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +4 -0
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +88 -0
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-5/schema.d.ts +85 -0
- package/lib/typescript/src/iso18013/iso18013-5/schema.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-7/index.d.ts +3 -0
- package/lib/typescript/src/iso18013/iso18013-7/index.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts +29 -0
- package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/types.d.ts +16 -0
- package/lib/typescript/src/iso18013/types.d.ts.map +1 -0
- package/package.json +163 -0
- package/src/cbor/cbor/README.md +95 -0
- package/src/cbor/cbor/decoder.ts +56 -0
- package/src/cbor/cbor/failure.ts +18 -0
- package/src/cbor/cbor/index.ts +9 -0
- package/src/cbor/cbor/schema.ts +150 -0
- package/src/cbor/cbor/schema.utils.ts +13 -0
- package/src/cbor/cose/README.md +53 -0
- package/src/cbor/cose/failure.ts +20 -0
- package/src/cbor/cose/index.ts +2 -0
- package/src/cbor/cose/sign.ts +25 -0
- package/src/cbor/index.ts +18 -0
- package/src/index.tsx +5 -0
- package/src/iso18013/index.ts +25 -0
- package/src/iso18013/iso18013-5/README.md +234 -0
- package/src/iso18013/iso18013-5/index.ts +23 -0
- package/src/iso18013/iso18013-5/proximity.ts +134 -0
- package/src/iso18013/iso18013-5/schema.ts +87 -0
- package/src/iso18013/iso18013-7/README.md +59 -0
- package/src/iso18013/iso18013-7/index.ts +7 -0
- package/src/iso18013/iso18013-7/remote.ts +54 -0
- package/src/iso18013/types.ts +16 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
package com.ioreactnativeiso18013
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.Promise
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
7
|
+
import com.facebook.react.bridge.ReactMethod
|
|
8
|
+
import com.facebook.react.bridge.ReadableArray
|
|
9
|
+
import com.facebook.react.bridge.ReadableMap
|
|
10
|
+
import com.facebook.react.bridge.ReadableType
|
|
11
|
+
import com.facebook.react.bridge.WritableMap
|
|
12
|
+
import com.facebook.react.bridge.WritableNativeMap
|
|
13
|
+
import com.ioreactnativeiso18013.IoReactNativeCborModule.Companion.ERROR_USER_INFO_KEY
|
|
14
|
+
import it.pagopa.io.wallet.proximity.OpenID4VP
|
|
15
|
+
import it.pagopa.io.wallet.proximity.bluetooth.BleRetrievalMethod
|
|
16
|
+
import it.pagopa.io.wallet.proximity.qr_code.QrEngagement
|
|
17
|
+
import it.pagopa.io.wallet.proximity.qr_code.QrEngagementListener
|
|
18
|
+
import it.pagopa.io.wallet.proximity.request.DocRequested
|
|
19
|
+
import it.pagopa.io.wallet.proximity.response.ResponseGenerator
|
|
20
|
+
import it.pagopa.io.wallet.proximity.session_data.SessionDataStatus
|
|
21
|
+
import it.pagopa.io.wallet.proximity.wrapper.DeviceRetrievalHelperWrapper
|
|
22
|
+
|
|
23
|
+
class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
24
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
25
|
+
|
|
26
|
+
override fun getName(): String {
|
|
27
|
+
return NAME
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private var qrEngagement: QrEngagement? = null
|
|
31
|
+
private var deviceRetrievalHelper: DeviceRetrievalHelperWrapper? = null
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Starts the proximity flow by allocating the necessary resources and initializing the Bluetooth stack.
|
|
35
|
+
* Resolves to true or rejects if an error occurs.
|
|
36
|
+
* @param peripheralMode - Whether the device is in peripheral mode. Defaults to true
|
|
37
|
+
* @param centralClientMode - Whether the device is in central client mode. Defaults to false
|
|
38
|
+
* @param clearBleCache - Whether the BLE cache should be cleared. Defaults to true
|
|
39
|
+
* @param certificates - Array of base64 representing DER encoded X.509 certificate which are used to authenticate the verifier app
|
|
40
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
41
|
+
*/
|
|
42
|
+
@ReactMethod
|
|
43
|
+
fun start(
|
|
44
|
+
peripheralMode: Boolean,
|
|
45
|
+
centralClientMode: Boolean,
|
|
46
|
+
clearBleCache: Boolean,
|
|
47
|
+
certificates: ReadableArray,
|
|
48
|
+
promise: Promise
|
|
49
|
+
) {
|
|
50
|
+
try {
|
|
51
|
+
val retrievalMethod = BleRetrievalMethod(
|
|
52
|
+
peripheralServerMode = peripheralMode,
|
|
53
|
+
centralClientMode = centralClientMode,
|
|
54
|
+
clearBleCache = clearBleCache
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
val certificatesList = parseCertificates(certificates)
|
|
58
|
+
qrEngagement = QrEngagement.build(reactApplicationContext, listOf(retrievalMethod)).apply {
|
|
59
|
+
if (certificatesList.isNotEmpty()) {
|
|
60
|
+
withReaderTrustStore(certificatesList)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
qrEngagement?.configure()
|
|
64
|
+
setupProximityHandler()
|
|
65
|
+
promise.resolve(true)
|
|
66
|
+
} catch (e: Exception) {
|
|
67
|
+
ModuleException.START_ERROR.reject(
|
|
68
|
+
promise,
|
|
69
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Utility function to parse an array coming from the React Native Bridge into an ArrayList
|
|
76
|
+
* of ByteArray representing DER encoded X.509 certificates.
|
|
77
|
+
* @param certificates - Array of base64 strings representing DER encoded X.509 certificate
|
|
78
|
+
* @returns An ArrayList of ByteArray representing DER encoded X.509 certificates.
|
|
79
|
+
* @throws ClassCastException if the element in the array is not a string
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
private fun parseCertificates(certificates: ReadableArray): ArrayList<ByteArray> {
|
|
83
|
+
return ArrayList(
|
|
84
|
+
(0 until certificates.size()).mapNotNull { i ->
|
|
85
|
+
certificates.getString(i)?.let { cert ->
|
|
86
|
+
Base64Utils.decodeBase64(cert)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Creates a QR code to be scanned in order to initialize the presentation.
|
|
94
|
+
* Resolves with the QR code strings.
|
|
95
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
96
|
+
*/
|
|
97
|
+
@ReactMethod
|
|
98
|
+
fun getQrCodeString(promise: Promise) {
|
|
99
|
+
try {
|
|
100
|
+
qrEngagement?.let {
|
|
101
|
+
val qrCodeString = it.getQrCodeString()
|
|
102
|
+
promise.resolve(qrCodeString)
|
|
103
|
+
} ?: run {
|
|
104
|
+
ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
|
|
105
|
+
}
|
|
106
|
+
} catch (e: Exception) {
|
|
107
|
+
ModuleException.GET_QR_CODE_ERROR.reject(
|
|
108
|
+
promise,
|
|
109
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Closes the bluetooth connection and clears any resource.
|
|
116
|
+
* It resolves to true after closing the connection.
|
|
117
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
118
|
+
*/
|
|
119
|
+
@ReactMethod
|
|
120
|
+
fun close(promise: Promise) {
|
|
121
|
+
try {
|
|
122
|
+
qrEngagement?.close()
|
|
123
|
+
deviceRetrievalHelper?.disconnect()
|
|
124
|
+
promise.resolve(true)
|
|
125
|
+
} catch (e: Exception) {
|
|
126
|
+
ModuleException.CLOSE_ERROR.reject(
|
|
127
|
+
promise,
|
|
128
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sends an error response during the presentation according to the SessionData status codes defined in table 20 of the ISO18013-5 standard.
|
|
135
|
+
* @param code - The status error to be sent is a long type but the bridge only maps double values. It is converted to a long.
|
|
136
|
+
* The accepted values are defined in ``SessionDataStatus`` as follows:
|
|
137
|
+
* 10 -> Error: session encryption
|
|
138
|
+
* 11 -> Error: CBOR decoding
|
|
139
|
+
* 20 -> Session termination
|
|
140
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
141
|
+
*/
|
|
142
|
+
@ReactMethod
|
|
143
|
+
fun sendErrorResponse(code: Double, promise: Promise) {
|
|
144
|
+
try {
|
|
145
|
+
qrEngagement?.let { it ->
|
|
146
|
+
val sessionDataStatus = SessionDataStatus.entries.find { it.value == code.toLong() }
|
|
147
|
+
if (sessionDataStatus != null) {
|
|
148
|
+
it.sendErrorResponse(sessionDataStatus)
|
|
149
|
+
promise.resolve(true)
|
|
150
|
+
} else {
|
|
151
|
+
ModuleException.SEND_ERROR_RESPONSE_ERROR.reject(
|
|
152
|
+
promise,
|
|
153
|
+
Pair(ERROR_KEY, "Invalid status code")
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
} ?: run {
|
|
157
|
+
ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
|
|
158
|
+
}
|
|
159
|
+
} catch (e: Exception) {
|
|
160
|
+
ModuleException.SEND_ERROR_RESPONSE_ERROR.reject(
|
|
161
|
+
promise,
|
|
162
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generates a response which can later be sent with {sendResponse} with the provided
|
|
169
|
+
* CBOR documents and the requested attributes.
|
|
170
|
+
* @param documents - A {ReadableArray} containing documents. Each document is defined as a map containing:
|
|
171
|
+
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
172
|
+
* - alias which is the alias of the key used to sign the credential;
|
|
173
|
+
* - docType which is the document type.
|
|
174
|
+
* @param fieldRequestedAndAccepted - The string containing the requested attributes. This is based on the request
|
|
175
|
+
* provided by the {onDocumentRequestReceived} callback.
|
|
176
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
177
|
+
*/
|
|
178
|
+
@ReactMethod
|
|
179
|
+
fun generateResponse(
|
|
180
|
+
documents: ReadableArray,
|
|
181
|
+
fieldRequestedAndAccepted: ReadableMap,
|
|
182
|
+
promise: Promise
|
|
183
|
+
) {
|
|
184
|
+
try {
|
|
185
|
+
deviceRetrievalHelper?.let { devHelper ->
|
|
186
|
+
// Get the DocRequested list and if it's empty then reject the promise and return
|
|
187
|
+
val docRequestedList = parseDocRequested(documents)
|
|
188
|
+
|
|
189
|
+
val sessionTranscript = devHelper.sessionTranscript()
|
|
190
|
+
val responseGenerator = ResponseGenerator(sessionTranscript)
|
|
191
|
+
responseGenerator.createResponse(docRequestedList,
|
|
192
|
+
fieldRequestedAndAccepted.toString(),
|
|
193
|
+
object : ResponseGenerator.Response {
|
|
194
|
+
override fun onResponseGenerated(response: ByteArray) {
|
|
195
|
+
promise.resolve(Base64Utils.encodeBase64(response))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
override fun onError(message: String) {
|
|
199
|
+
ModuleException.GENERATE_RESPONSE_ERROR.reject(
|
|
200
|
+
promise,
|
|
201
|
+
Pair(ERROR_KEY, message)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
} ?: run {
|
|
206
|
+
ModuleException.DRH_NOT_DEFINED.reject(promise)
|
|
207
|
+
}
|
|
208
|
+
} catch (e: Exception) {
|
|
209
|
+
ModuleException.GENERATE_RESPONSE_ERROR.reject(
|
|
210
|
+
promise,
|
|
211
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Sends a response containing the documents and the fields which the user decided to present generated by {generateResponse}.
|
|
218
|
+
* It resolves to true after sending the response, otherwise it rejects if an error occurs while decoding the response.
|
|
219
|
+
* Currently there's not evidence of the verifier app responding to this request, thus we don't handle the response.
|
|
220
|
+
* @param response - A base64 encoded string containing the response generated by {generateResponse}
|
|
221
|
+
* @param promise - The promise which will be resolved in case of success or rejected in case of failure.
|
|
222
|
+
*/
|
|
223
|
+
@ReactMethod
|
|
224
|
+
fun sendResponse(response: String, promise: Promise) {
|
|
225
|
+
try {
|
|
226
|
+
qrEngagement?.let { qrEng ->
|
|
227
|
+
val responseBytes = Base64Utils.decodeBase64(response)
|
|
228
|
+
qrEng.sendResponse(responseBytes)
|
|
229
|
+
promise.resolve(true)
|
|
230
|
+
}
|
|
231
|
+
?: run {
|
|
232
|
+
ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
|
|
233
|
+
}
|
|
234
|
+
} catch (e: Exception) {
|
|
235
|
+
ModuleException.SEND_RESPONSE_ERROR.reject(
|
|
236
|
+
promise,
|
|
237
|
+
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
@ReactMethod
|
|
243
|
+
fun generateOID4VPDeviceResponse(
|
|
244
|
+
clientId: String, responseUri: String, authorizationRequestNonce: String,
|
|
245
|
+
mdocGeneratedNonce: String, documents: ReadableArray,
|
|
246
|
+
fieldRequestedAndAccepted: String, promise: Promise
|
|
247
|
+
) {
|
|
248
|
+
val sessionTranscript = try {
|
|
249
|
+
OpenID4VP(
|
|
250
|
+
clientId,
|
|
251
|
+
responseUri,
|
|
252
|
+
authorizationRequestNonce,
|
|
253
|
+
mdocGeneratedNonce
|
|
254
|
+
).createSessionTranscript()
|
|
255
|
+
} catch (e: Exception) {
|
|
256
|
+
ModuleException.UNABLE_TO_GENERATE_TRANSCRIPT.reject(
|
|
257
|
+
promise,
|
|
258
|
+
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
259
|
+
)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
val documentsParsed = try {
|
|
264
|
+
parseDocRequested(documents)
|
|
265
|
+
} catch (e: Exception) {
|
|
266
|
+
ModuleException.INVALID_DOC_REQUESTED.reject(
|
|
267
|
+
promise,
|
|
268
|
+
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
val responseGenerator = ResponseGenerator(sessionTranscript)
|
|
275
|
+
responseGenerator.createResponse(
|
|
276
|
+
documentsParsed,
|
|
277
|
+
fieldRequestedAndAccepted,
|
|
278
|
+
object : ResponseGenerator.Response {
|
|
279
|
+
override fun onResponseGenerated(response: ByteArray) {
|
|
280
|
+
promise.resolve(Base64Utils.encodeBase64(response))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override fun onError(message: String) {
|
|
284
|
+
ModuleException.UNABLE_TO_GENERATE_RESPONSE.reject(
|
|
285
|
+
promise,
|
|
286
|
+
Pair(ERROR_USER_INFO_KEY, message)
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
} catch (e: Exception) {
|
|
292
|
+
ModuleException.UNABLE_TO_GENERATE_RESPONSE.reject(
|
|
293
|
+
promise,
|
|
294
|
+
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Sets the proximity handler along with the possible dispatched events and their callbacks.
|
|
301
|
+
* The events are then sent to React Native via `RCTEventEmitter`.
|
|
302
|
+
* onDeviceConnecting: Emitted when the device is connecting to the verifier app.
|
|
303
|
+
* onDeviceConnected: Emitted when the device is connected to the verifier app.
|
|
304
|
+
* onDocumentRequestReceived: Emitted when a document request is received from the verifier app. Carries a payload containing the request data.
|
|
305
|
+
* onDeviceDisconnected: Emitted when the device is disconnected from the verifier app.
|
|
306
|
+
* onError: Emitted when an error occurs. Carries a payload containing the error data.
|
|
307
|
+
*/
|
|
308
|
+
private fun setupProximityHandler() {
|
|
309
|
+
qrEngagement?.withListener(object : QrEngagementListener {
|
|
310
|
+
/**
|
|
311
|
+
* This event currently doesn't get called due to an issue with the underlying native library.
|
|
312
|
+
*/
|
|
313
|
+
override fun onDeviceConnecting() {
|
|
314
|
+
sendEvent("onDeviceConnecting", "")
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
override fun onDeviceConnected(deviceRetrievalHelper: DeviceRetrievalHelperWrapper) {
|
|
318
|
+
this@IoReactNativeIso18013Module.deviceRetrievalHelper = deviceRetrievalHelper
|
|
319
|
+
sendEvent("onDeviceConnected", "")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
override fun onError(error: Throwable) {
|
|
323
|
+
val data: WritableMap = Arguments.createMap()
|
|
324
|
+
data.putString("error", error.message ?: "")
|
|
325
|
+
sendEvent("onError", data)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
override fun onDocumentRequestReceived(request: String?, sessionsTranscript: ByteArray) {
|
|
329
|
+
val data: WritableMap = Arguments.createMap()
|
|
330
|
+
data.putString("data", request)
|
|
331
|
+
sendEvent("onDocumentRequestReceived", data)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
override fun onDeviceDisconnected(transportSpecificTermination: Boolean) {
|
|
335
|
+
sendEvent("onDeviceDisconnected", transportSpecificTermination.toString())
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Utility function which extracts the document shape we expect to receive from the bridge
|
|
342
|
+
* in the one expected by {DocRequested}.
|
|
343
|
+
* @param documents - A {ReadableArray} containing documents. Each document is defined as a map containing:
|
|
344
|
+
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
345
|
+
* - alias which is the alias of the key used to sign the credential;
|
|
346
|
+
* - docType which is the document type.
|
|
347
|
+
* @returns An array containing a {DocRequested} object for each document in {documents}
|
|
348
|
+
* @throws IllegalArgumentException if the provided document doesn't adhere to the expected format
|
|
349
|
+
*/
|
|
350
|
+
private fun parseDocRequested(documents: ReadableArray): Array<DocRequested> {
|
|
351
|
+
return (0 until documents.size()).map { i ->
|
|
352
|
+
val entry = documents.getMap(i)
|
|
353
|
+
?: throw IllegalArgumentException("Entry in ReadableArray is null")
|
|
354
|
+
|
|
355
|
+
val alias = entry.getString("alias")
|
|
356
|
+
val issuerSignedContentStr = entry.getString("issuerSignedContent")
|
|
357
|
+
val docType = entry.getString("docType")
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
alias == null || entry.getType("alias") != ReadableType.String ||
|
|
361
|
+
issuerSignedContentStr == null || entry.getType("issuerSignedContent") != ReadableType.String ||
|
|
362
|
+
docType == null || entry.getType("docType") != ReadableType.String
|
|
363
|
+
) throw IllegalArgumentException("Unable to decode the provided documents")
|
|
364
|
+
|
|
365
|
+
val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
|
|
366
|
+
|
|
367
|
+
DocRequested(
|
|
368
|
+
issuerSignedContent,
|
|
369
|
+
alias,
|
|
370
|
+
docType
|
|
371
|
+
)
|
|
372
|
+
}.toTypedArray()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Wrapper function to send an event via `RCTEventEmitter`
|
|
377
|
+
* @param eventName - The event name
|
|
378
|
+
* @param data - The data attached to eventName
|
|
379
|
+
*/
|
|
380
|
+
private fun sendEvent(eventName: String, data: Any?) {
|
|
381
|
+
reactApplicationContext.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
382
|
+
.emit(eventName, data)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
@ReactMethod
|
|
386
|
+
fun addListener(eventName: String?) {
|
|
387
|
+
/* Keep: Required for RN built in Event Emitter Calls.
|
|
388
|
+
This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
389
|
+
*/
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@ReactMethod
|
|
393
|
+
fun removeListeners(count: Int?) {
|
|
394
|
+
/*Keep: Required for RN built in Event Emitter Calls.
|
|
395
|
+
This fixes the warning: `new NativeEventEmitter()` was called with a non-null argument without the required `removeListeners` method
|
|
396
|
+
*/
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private enum class ModuleException(
|
|
400
|
+
val ex: Exception
|
|
401
|
+
) {
|
|
402
|
+
/** ISO18013-5 related errors **/
|
|
403
|
+
DRH_NOT_DEFINED(Exception("DRH_NOT_DEFINED")),
|
|
404
|
+
QR_ENGAGEMENT_NOT_DEFINED_ERROR(Exception("QR_ENGAGEMENT_NOT_DEFINED_ERROR")),
|
|
405
|
+
START_ERROR(Exception("START_ERROR")),
|
|
406
|
+
GET_QR_CODE_ERROR(Exception("GET_QR_CODE_ERROR")),
|
|
407
|
+
GENERATE_RESPONSE_ERROR(Exception("GENERATE_RESPONSE_ERROR")),
|
|
408
|
+
SEND_RESPONSE_ERROR(Exception("SEND_RESPONSE_ERROR")),
|
|
409
|
+
SEND_ERROR_RESPONSE_ERROR(Exception("SEND_ERROR_RESPONSE_ERROR")),
|
|
410
|
+
CLOSE_ERROR(Exception("CLOSE_ERROR")),
|
|
411
|
+
|
|
412
|
+
/** ISO18013-7 related errors **/
|
|
413
|
+
UNABLE_TO_GENERATE_RESPONSE(Exception("UNABLE_TO_GENERATE_RESPONSE")),
|
|
414
|
+
UNABLE_TO_GENERATE_TRANSCRIPT(Exception("UNABLE_TO_GENERATE_TRANSCRIPT")),
|
|
415
|
+
INVALID_DOC_REQUESTED(Exception("INVALID_DOC_REQUESTED")),
|
|
416
|
+
GENERATE_OID4VP_DEVICE_RESPONSE_FAILED(Exception("GENERATE_OID4VP_DEVICE_RESPONSE_FAILED"));
|
|
417
|
+
|
|
418
|
+
fun reject(
|
|
419
|
+
promise: Promise, vararg args: Pair<String, String>
|
|
420
|
+
) {
|
|
421
|
+
exMap(*args).let {
|
|
422
|
+
promise.reject(it.first, ex.message, it.second)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private fun exMap(vararg args: Pair<String, String>): Pair<String, WritableMap> {
|
|
427
|
+
val writableMap = WritableNativeMap()
|
|
428
|
+
args.forEach { writableMap.putString(it.first, it.second) }
|
|
429
|
+
return Pair(this.ex.message ?: "UNKNOWN", writableMap)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private fun getExceptionMessageOrEmpty(e: Exception): String = e.message ?: ""
|
|
434
|
+
|
|
435
|
+
companion object {
|
|
436
|
+
const val NAME = "IoReactNativeIso18013"
|
|
437
|
+
const val ERROR_KEY = "error"
|
|
438
|
+
}
|
|
439
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
package com.ioreactnativeiso18013
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class IoReactNativeIso18013Package : ReactPackage {
|
|
10
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
+
return listOf(IoReactNativeCborModule(reactContext), IoReactNativeIso18013Module(reactContext))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
15
|
+
return emptyList()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This class contains a set of utilities for encoding and decoding base64 and base64url strings.
|
|
5
|
+
*/
|
|
6
|
+
class Base64Utils {
|
|
7
|
+
enum Base64DecodingError: Error {
|
|
8
|
+
case invalidBase64(String)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
Parses a base64 or base64url `String` into `Data` buffer.
|
|
13
|
+
|
|
14
|
+
- Parameters:
|
|
15
|
+
- documents: An array of any elements. In order to be added to the result array each element must be a dictionary with `issuerSignedContent`, `alias` and `docType` as keys and strings as values.
|
|
16
|
+
|
|
17
|
+
- Throws: `Base64DecodingError.invalidBase64` if `base` is not a valid base64 or base64url
|
|
18
|
+
|
|
19
|
+
- Returns: A `Data` buffer representing `base`.
|
|
20
|
+
*/
|
|
21
|
+
static func decodeBase64OrBase64URL(base: String) throws -> Data {
|
|
22
|
+
if let data = dataFromBase64Url(base64url: base) {
|
|
23
|
+
return data
|
|
24
|
+
} else if let data = Data(base64Encoded: base) {
|
|
25
|
+
return data
|
|
26
|
+
} else {
|
|
27
|
+
throw Base64DecodingError.invalidBase64("Input is not valid Base64 or Base64URL")
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
Parses a base64url `String` into `Data` buffer by converting it to base64 before feeding it into `Data`.
|
|
33
|
+
- Parameters:
|
|
34
|
+
- base64url: the base64url encoded `String` to be converted
|
|
35
|
+
|
|
36
|
+
- Returns: A `Data` buffer representing `base64url`
|
|
37
|
+
*/
|
|
38
|
+
static private func dataFromBase64Url(base64url: String) -> Data? {
|
|
39
|
+
var base64 = base64url
|
|
40
|
+
.replacingOccurrences(of: "-", with: "+")
|
|
41
|
+
.replacingOccurrences(of: "_", with: "/")
|
|
42
|
+
if base64.count % 4 != 0 {
|
|
43
|
+
base64.append(String(repeating: "=", count: 4 - base64.count % 4))
|
|
44
|
+
}
|
|
45
|
+
return Data(base64Encoded: base64)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
@interface RCT_EXTERN_MODULE(IoReactNativeCbor, NSObject)
|
|
4
|
+
|
|
5
|
+
RCT_EXTERN_METHOD(decode: (NSString)cbor
|
|
6
|
+
resolver: (RCTPromiseResolveBlock)resolve
|
|
7
|
+
rejecter: (RCTPromiseRejectBlock)reject)
|
|
8
|
+
|
|
9
|
+
RCT_EXTERN_METHOD(decodeDocuments: (NSString)mdoc
|
|
10
|
+
resolver: (RCTPromiseResolveBlock)resolve
|
|
11
|
+
rejecter: (RCTPromiseRejectBlock)reject)
|
|
12
|
+
|
|
13
|
+
RCT_EXTERN_METHOD(decodeIssuerSigned: (NSString)issuerSigned
|
|
14
|
+
resolver: (RCTPromiseResolveBlock)resolve
|
|
15
|
+
rejecter: (RCTPromiseRejectBlock)reject)
|
|
16
|
+
|
|
17
|
+
RCT_EXTERN_METHOD(sign: (NSString)payload
|
|
18
|
+
keyTag: (NSString*)keyTag
|
|
19
|
+
resolver: (RCTPromiseResolveBlock)resolve
|
|
20
|
+
rejecter: (RCTPromiseRejectBlock)reject)
|
|
21
|
+
|
|
22
|
+
RCT_EXTERN_METHOD(verify: (NSString)sign1Data
|
|
23
|
+
jwk: (NSDictionary*)jwk
|
|
24
|
+
resolver: (RCTPromiseResolveBlock)resolve
|
|
25
|
+
rejecter: (RCTPromiseRejectBlock)reject)
|
|
26
|
+
|
|
27
|
+
+ (BOOL)requiresMainQueueSetup
|
|
28
|
+
{
|
|
29
|
+
return NO;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@end
|