@pagopa/io-react-native-iso18013 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/android/build.gradle +7 -1
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeCborModule.kt +69 -134
- package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +254 -183
- package/android/src/test/java/com/ioreactnativeiso18013/Base64UtilsTest.kt +66 -0
- package/android/src/test/java/com/ioreactnativeiso18013/IoReactNativeIso18013Test.kt +508 -0
- package/ios/Base64Utils.swift +10 -3
- package/ios/IoReactNativeCbor.mm +21 -21
- package/ios/IoReactNativeCbor.swift +116 -138
- package/ios/IoReactNativeIso18013.mm +10 -10
- package/ios/IoReactNativeIso18013.swift +172 -193
- package/lib/module/cbor/cbor/README.md +39 -59
- package/lib/module/cbor/cbor/decoder.js +13 -23
- package/lib/module/cbor/cbor/decoder.js.map +1 -1
- package/lib/module/cbor/cbor/error.js +15 -0
- package/lib/module/cbor/cbor/error.js.map +1 -0
- package/lib/module/cbor/cbor/index.js +1 -0
- package/lib/module/cbor/cbor/index.js.map +1 -1
- package/lib/module/cbor/cose/README.md +39 -9
- package/lib/module/cbor/cose/error.js +17 -0
- package/lib/module/cbor/cose/error.js.map +1 -0
- package/lib/module/cbor/cose/index.js +1 -0
- package/lib/module/cbor/cose/index.js.map +1 -1
- package/lib/module/cbor/cose/sign.js +4 -3
- package/lib/module/cbor/cose/sign.js.map +1 -1
- package/lib/module/iso18013/index.js +0 -1
- package/lib/module/iso18013/index.js.map +1 -1
- package/lib/module/iso18013/iso18013-5/README.md +120 -44
- package/lib/module/iso18013/iso18013-5/error.js +25 -0
- package/lib/module/iso18013/iso18013-5/error.js.map +1 -0
- package/lib/module/iso18013/iso18013-5/index.js +3 -2
- package/lib/module/iso18013/iso18013-5/index.js.map +1 -1
- package/lib/module/iso18013/iso18013-5/proximity.js +20 -15
- package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -1
- package/lib/module/iso18013/iso18013-5/{schema.js → request.js} +1 -37
- package/lib/module/iso18013/iso18013-5/request.js.map +1 -0
- package/lib/module/iso18013/iso18013-7/README.md +66 -41
- package/lib/module/iso18013/iso18013-7/error.js +15 -0
- package/lib/module/iso18013/iso18013-7/error.js.map +1 -0
- package/lib/module/iso18013/iso18013-7/index.js +1 -0
- package/lib/module/iso18013/iso18013-7/index.js.map +1 -1
- package/lib/module/iso18013/iso18013-7/remote.js +11 -24
- package/lib/module/iso18013/iso18013-7/remote.js.map +1 -1
- package/lib/module/schema.js +46 -0
- package/lib/module/schema.js.map +1 -0
- package/lib/typescript/src/cbor/cbor/decoder.d.ts +12 -22
- package/lib/typescript/src/cbor/cbor/decoder.d.ts.map +1 -1
- package/lib/typescript/src/cbor/cbor/error.d.ts +68 -0
- package/lib/typescript/src/cbor/cbor/error.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cbor/index.d.ts +1 -1
- package/lib/typescript/src/cbor/cbor/index.d.ts.map +1 -1
- package/lib/typescript/src/cbor/cose/error.d.ts +68 -0
- package/lib/typescript/src/cbor/cose/error.d.ts.map +1 -0
- package/lib/typescript/src/cbor/cose/index.d.ts +1 -1
- package/lib/typescript/src/cbor/cose/index.d.ts.map +1 -1
- package/lib/typescript/src/cbor/cose/sign.d.ts +4 -3
- package/lib/typescript/src/cbor/cose/sign.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/index.d.ts +0 -1
- package/lib/typescript/src/iso18013/index.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/error.d.ts +73 -0
- package/lib/typescript/src/iso18013/iso18013-5/error.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +4 -3
- package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +21 -14
- package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-5/{schema.d.ts → request.d.ts} +1 -39
- package/lib/typescript/src/iso18013/iso18013-5/request.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-7/error.d.ts +68 -0
- package/lib/typescript/src/iso18013/iso18013-7/error.d.ts.map +1 -0
- package/lib/typescript/src/iso18013/iso18013-7/index.d.ts +3 -2
- package/lib/typescript/src/iso18013/iso18013-7/index.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts +11 -25
- package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts.map +1 -1
- package/lib/typescript/src/iso18013/types.d.ts +25 -0
- package/lib/typescript/src/iso18013/types.d.ts.map +1 -1
- package/lib/typescript/src/schema.d.ts +76 -0
- package/lib/typescript/src/schema.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cbor/cbor/README.md +39 -59
- package/src/cbor/cbor/decoder.ts +13 -23
- package/src/cbor/cbor/error.ts +23 -0
- package/src/cbor/cbor/index.ts +5 -1
- package/src/cbor/cose/README.md +39 -9
- package/src/cbor/cose/error.ts +23 -0
- package/src/cbor/cose/index.ts +5 -1
- package/src/cbor/cose/sign.ts +5 -4
- package/src/iso18013/index.ts +0 -7
- package/src/iso18013/iso18013-5/README.md +120 -44
- package/src/iso18013/iso18013-5/error.ts +35 -0
- package/src/iso18013/iso18013-5/index.ts +9 -8
- package/src/iso18013/iso18013-5/proximity.ts +21 -17
- package/src/iso18013/iso18013-5/{schema.ts → request.ts} +0 -42
- package/src/iso18013/iso18013-7/README.md +66 -41
- package/src/iso18013/iso18013-7/error.ts +21 -0
- package/src/iso18013/iso18013-7/index.ts +6 -5
- package/src/iso18013/iso18013-7/remote.ts +12 -34
- package/src/iso18013/types.ts +24 -0
- package/src/schema.ts +55 -0
- package/lib/module/cbor/cbor/failure.js +0 -2
- package/lib/module/cbor/cbor/failure.js.map +0 -1
- package/lib/module/cbor/cose/failure.js +0 -2
- package/lib/module/cbor/cose/failure.js.map +0 -1
- package/lib/module/iso18013/iso18013-5/schema.js.map +0 -1
- package/lib/typescript/src/cbor/cbor/failure.d.ts +0 -15
- package/lib/typescript/src/cbor/cbor/failure.d.ts.map +0 -1
- package/lib/typescript/src/cbor/cose/failure.d.ts +0 -15
- package/lib/typescript/src/cbor/cose/failure.d.ts.map +0 -1
- package/lib/typescript/src/iso18013/iso18013-5/schema.d.ts.map +0 -1
- package/src/cbor/cbor/failure.ts +0 -18
- package/src/cbor/cose/failure.ts +0 -20
|
@@ -9,8 +9,6 @@ import com.facebook.react.bridge.ReadableArray
|
|
|
9
9
|
import com.facebook.react.bridge.ReadableMap
|
|
10
10
|
import com.facebook.react.bridge.ReadableType
|
|
11
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
12
|
import it.pagopa.io.wallet.proximity.OpenID4VP
|
|
15
13
|
import it.pagopa.io.wallet.proximity.bluetooth.BleRetrievalMethod
|
|
16
14
|
import it.pagopa.io.wallet.proximity.qr_code.QrEngagement
|
|
@@ -32,12 +30,12 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
32
30
|
|
|
33
31
|
/**
|
|
34
32
|
* Starts the proximity flow by allocating the necessary resources and initializing the Bluetooth stack.
|
|
35
|
-
* Resolves to true or rejects
|
|
36
|
-
* @param peripheralMode
|
|
37
|
-
* @param centralClientMode
|
|
38
|
-
* @param clearBleCache
|
|
39
|
-
* @param certificates -
|
|
40
|
-
* @param promise
|
|
33
|
+
* Resolves to true or rejects with an error code defined in [ModuleErrorCodes].
|
|
34
|
+
* @param peripheralMode whether the device is in peripheral mode. Defaults to true
|
|
35
|
+
* @param centralClientMode whether the device is in central client mode. Defaults to false
|
|
36
|
+
* @param clearBleCache whether the BLE cache should be cleared. Defaults to true
|
|
37
|
+
* @param certificates two-dimensional array of base64 strings representing DER encoded X.509 certificate which are used to authenticate the verifier app
|
|
38
|
+
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
41
39
|
*/
|
|
42
40
|
@ReactMethod
|
|
43
41
|
fun start(
|
|
@@ -64,37 +62,14 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
64
62
|
setupProximityHandler()
|
|
65
63
|
promise.resolve(true)
|
|
66
64
|
} catch (e: Exception) {
|
|
67
|
-
|
|
68
|
-
ModuleException.START_ERROR.reject(
|
|
69
|
-
promise,
|
|
70
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
71
|
-
)
|
|
65
|
+
promise.reject(ModuleErrorCodes.START_ERROR, e.message, e)
|
|
72
66
|
}
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
/**
|
|
76
|
-
* Utility function to parse an array coming from the React Native Bridge into an ArrayList
|
|
77
|
-
* of ByteArray representing DER encoded X.509 certificates.
|
|
78
|
-
* @param certificates - Two-dimensional array of base64 strings representing DER encoded X.509 certificate
|
|
79
|
-
* @returns An ArrayList of ByteArray representing DER encoded X.509 certificates.
|
|
80
|
-
* @throws ClassCastException if the element in the array is not a string
|
|
81
|
-
*/
|
|
82
|
-
private fun parseCertificates(certificates: ReadableArray): List<List<ByteArray>> =
|
|
83
|
-
(0 until certificates.size())
|
|
84
|
-
.mapNotNull { chainIndex -> certificates.getArray(chainIndex) }
|
|
85
|
-
.map { chain ->
|
|
86
|
-
(0 until chain.size())
|
|
87
|
-
.mapNotNull { certIndex ->
|
|
88
|
-
chain.takeIf { it.getType(certIndex) == ReadableType.String }
|
|
89
|
-
?.getString(certIndex)
|
|
90
|
-
?.let { Base64Utils.decodeBase64(it) }
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
69
|
/**
|
|
95
70
|
* Creates a QR code to be scanned in order to initialize the presentation.
|
|
96
|
-
* Resolves with the QR code
|
|
97
|
-
* @param promise
|
|
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.
|
|
98
73
|
*/
|
|
99
74
|
@ReactMethod
|
|
100
75
|
fun getQrCodeString(promise: Promise) {
|
|
@@ -103,20 +78,19 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
103
78
|
val qrCodeString = it.getQrCodeString()
|
|
104
79
|
promise.resolve(qrCodeString)
|
|
105
80
|
} ?: run {
|
|
106
|
-
|
|
81
|
+
promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED,NOT_INITIALIZED_ERROR_MESSAGE)
|
|
107
82
|
}
|
|
108
83
|
} catch (e: Exception) {
|
|
109
|
-
|
|
110
|
-
promise,
|
|
111
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
112
|
-
)
|
|
84
|
+
promise.reject(ModuleErrorCodes.GET_QR_CODE_ERROR, e.message, e)
|
|
113
85
|
}
|
|
114
86
|
}
|
|
115
87
|
|
|
116
88
|
/**
|
|
117
89
|
* Closes the bluetooth connection and clears any resource.
|
|
118
|
-
*
|
|
119
|
-
*
|
|
90
|
+
* Resolves to true after closing the connection or rejects with an error code
|
|
91
|
+
* defined in [ModuleErrorCodes].
|
|
92
|
+
* @param promise the promise which will be resolved in case of success or rejected
|
|
93
|
+
* in case of failure.
|
|
120
94
|
*/
|
|
121
95
|
@ReactMethod
|
|
122
96
|
fun close(promise: Promise) {
|
|
@@ -125,21 +99,21 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
125
99
|
deviceRetrievalHelper?.disconnect()
|
|
126
100
|
promise.resolve(true)
|
|
127
101
|
} catch (e: Exception) {
|
|
128
|
-
|
|
129
|
-
promise,
|
|
130
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
131
|
-
)
|
|
102
|
+
promise.reject(ModuleErrorCodes.CLOSE_ERROR, message=e.message, e)
|
|
132
103
|
}
|
|
133
104
|
}
|
|
134
105
|
|
|
135
106
|
/**
|
|
136
|
-
* Sends an error response during the presentation according to the SessionData status codes
|
|
137
|
-
*
|
|
107
|
+
* Sends an error response during the presentation according to the SessionData status codes
|
|
108
|
+
* defined in table 20 of the ISO18013-5 standard.
|
|
109
|
+
* Resolves to true or rejects with an error code defined in [ModuleErrorCodes].
|
|
110
|
+
* @param code the status error to be sent is a long type but the bridge only maps
|
|
111
|
+
* double values. It is converted to a long.
|
|
138
112
|
* The accepted values are defined in ``SessionDataStatus`` as follows:
|
|
139
113
|
* 10 -> Error: session encryption
|
|
140
114
|
* 11 -> Error: CBOR decoding
|
|
141
115
|
* 20 -> Session termination
|
|
142
|
-
* @param promise
|
|
116
|
+
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
143
117
|
*/
|
|
144
118
|
@ReactMethod
|
|
145
119
|
fun sendErrorResponse(code: Double, promise: Promise) {
|
|
@@ -150,37 +124,46 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
150
124
|
it.sendErrorResponse(sessionDataStatus)
|
|
151
125
|
promise.resolve(true)
|
|
152
126
|
} else {
|
|
153
|
-
|
|
154
|
-
promise,
|
|
155
|
-
Pair(ERROR_KEY, "Invalid status code")
|
|
156
|
-
)
|
|
127
|
+
promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message="Invalid status code")
|
|
157
128
|
}
|
|
158
129
|
} ?: run {
|
|
159
|
-
|
|
130
|
+
promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
|
|
160
131
|
}
|
|
161
132
|
} catch (e: Exception) {
|
|
162
|
-
|
|
163
|
-
promise,
|
|
164
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
165
|
-
)
|
|
133
|
+
promise.reject(ModuleErrorCodes.SEND_ERROR_RESPONSE_ERROR, message=e.message, e)
|
|
166
134
|
}
|
|
167
135
|
}
|
|
168
136
|
|
|
169
137
|
/**
|
|
170
138
|
* Generates a response which can later be sent with {sendResponse} with the provided
|
|
171
139
|
* CBOR documents and the requested attributes.
|
|
172
|
-
*
|
|
140
|
+
* Resolves with a base64 encoded response or rejects with an error code defined in [ModuleErrorCodes].
|
|
141
|
+
* @param documents [ReadableArray] containing documents. Each document is defined as a map containing:
|
|
173
142
|
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
174
143
|
* - alias which is the alias of the key used to sign the credential;
|
|
175
144
|
* - docType which is the document type.
|
|
176
|
-
* @param
|
|
177
|
-
*
|
|
178
|
-
*
|
|
145
|
+
* @param acceptedFields A dictionary of elements, where each element must adhere to a Map<String, Map<String, Map<String,Boolean>>>.
|
|
146
|
+
* The outermost key represents the credential doctype. The inner dictionary contains namespaces, and for each namespace, there is another dictionary mapping requested claims to a boolean value, which indicates whether the user is willing to present the corresponding claim. Example:
|
|
147
|
+
* ```
|
|
148
|
+
* {
|
|
149
|
+
* "org.iso.18013.5.1.mDL": {
|
|
150
|
+
* "org.iso.18013.5.1": {
|
|
151
|
+
* "hair_colour": true,
|
|
152
|
+
* "given_name_national_character": true,
|
|
153
|
+
* "family_name_national_character": true,
|
|
154
|
+
* "given_name": true,
|
|
155
|
+
* },
|
|
156
|
+
* {...}
|
|
157
|
+
* },
|
|
158
|
+
* {...}
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
* @param promise The promise which will be resolved in case of success or rejected in case of failure.
|
|
179
162
|
*/
|
|
180
163
|
@ReactMethod
|
|
181
164
|
fun generateResponse(
|
|
182
165
|
documents: ReadableArray,
|
|
183
|
-
|
|
166
|
+
acceptedFields: ReadableMap,
|
|
184
167
|
promise: Promise
|
|
185
168
|
) {
|
|
186
169
|
try {
|
|
@@ -190,37 +173,32 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
190
173
|
|
|
191
174
|
val sessionTranscript = devHelper.sessionTranscript()
|
|
192
175
|
val responseGenerator = ResponseGenerator(sessionTranscript)
|
|
176
|
+
val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
|
|
193
177
|
responseGenerator.createResponse(docRequestedList,
|
|
194
|
-
|
|
178
|
+
parsedAcceptedFields,
|
|
195
179
|
object : ResponseGenerator.Response {
|
|
196
180
|
override fun onResponseGenerated(response: ByteArray) {
|
|
197
181
|
promise.resolve(Base64Utils.encodeBase64(response))
|
|
198
182
|
}
|
|
199
183
|
|
|
200
184
|
override fun onError(message: String) {
|
|
201
|
-
|
|
202
|
-
promise,
|
|
203
|
-
Pair(ERROR_KEY, message)
|
|
204
|
-
)
|
|
185
|
+
promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message)
|
|
205
186
|
}
|
|
206
187
|
})
|
|
207
188
|
} ?: run {
|
|
208
|
-
|
|
189
|
+
promise.reject(ModuleErrorCodes.DRH_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
|
|
209
190
|
}
|
|
210
191
|
} catch (e: Exception) {
|
|
211
|
-
|
|
212
|
-
promise,
|
|
213
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
214
|
-
)
|
|
192
|
+
promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message=e.message, e)
|
|
215
193
|
}
|
|
216
194
|
}
|
|
217
195
|
|
|
218
196
|
/**
|
|
219
197
|
* Sends a response containing the documents and the fields which the user decided to present generated by {generateResponse}.
|
|
220
|
-
* It resolves to true after sending the response, otherwise it rejects if an error occurs while decoding the response.
|
|
221
198
|
* Currently there's not evidence of the verifier app responding to this request, thus we don't handle the response.
|
|
222
|
-
*
|
|
223
|
-
* @param
|
|
199
|
+
* Resolves with a true boolean in case of success or rejects with an error code defined in [ModuleErrorCodes].
|
|
200
|
+
* @param response base64 encoded string containing the response generated by {generateResponse}
|
|
201
|
+
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
224
202
|
*/
|
|
225
203
|
@ReactMethod
|
|
226
204
|
fun sendResponse(response: String, promise: Promise) {
|
|
@@ -231,70 +209,77 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
231
209
|
promise.resolve(true)
|
|
232
210
|
}
|
|
233
211
|
?: run {
|
|
234
|
-
|
|
212
|
+
promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
|
|
235
213
|
}
|
|
236
214
|
} catch (e: Exception) {
|
|
237
|
-
|
|
238
|
-
promise,
|
|
239
|
-
Pair(ERROR_KEY, getExceptionMessageOrEmpty(e))
|
|
240
|
-
)
|
|
215
|
+
promise.reject(ModuleErrorCodes.SEND_RESPONSE_ERROR, e.message, e)
|
|
241
216
|
}
|
|
242
217
|
}
|
|
243
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Generates a CBOR encoded device response for ISO 18013-7 mDL remote presentation using OID4VP.
|
|
221
|
+
* Resolves with the base64 encoded device response or rejects with an error code
|
|
222
|
+
* defined in [ModuleErrorCodes].
|
|
223
|
+
* @param clientId the client id extracted from OID4VP session
|
|
224
|
+
* @param responseUri the response URI extracted from OID4VP session
|
|
225
|
+
* @param authorizationRequestNonce the authorization request nonce extracted from OID4VP session
|
|
226
|
+
* @param mdocGeneratedNonce the mDoc generated nonce to be generated
|
|
227
|
+
* @param documents [ReadableArray] containing documents. Each document is defined as a map containing:
|
|
228
|
+
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
229
|
+
* - alias which is the alias of the key used to sign the credential;
|
|
230
|
+
* - docType which is the document type.
|
|
231
|
+
* @param acceptedFields dictionary of elements, where each element must adhere to a Map<String, Map<String, Map<String,Boolean>>>.
|
|
232
|
+
* The outermost key represents the credential doctype. The inner dictionary contains namespaces, and for each namespace, there is another dictionary mapping requested claims to a boolean value, which indicates whether the user is willing to present the corresponding claim. Example:
|
|
233
|
+
* ```
|
|
234
|
+
* {
|
|
235
|
+
* "org.iso.18013.5.1.mDL": {
|
|
236
|
+
* "org.iso.18013.5.1": {
|
|
237
|
+
* "hair_colour": true,
|
|
238
|
+
* "given_name_national_character": true,
|
|
239
|
+
* "family_name_national_character": true,
|
|
240
|
+
* "given_name": true,
|
|
241
|
+
* }
|
|
242
|
+
* }
|
|
243
|
+
* }
|
|
244
|
+
* ```
|
|
245
|
+
* @param promise the promise which will be resolved in case of success or rejected in case of failure.
|
|
246
|
+
*/
|
|
244
247
|
@ReactMethod
|
|
245
248
|
fun generateOID4VPDeviceResponse(
|
|
246
249
|
clientId: String, responseUri: String, authorizationRequestNonce: String,
|
|
247
250
|
mdocGeneratedNonce: String, documents: ReadableArray,
|
|
248
|
-
|
|
251
|
+
acceptedFields: ReadableMap, promise: Promise
|
|
249
252
|
) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
ModuleException.UNABLE_TO_GENERATE_TRANSCRIPT.reject(
|
|
259
|
-
promise,
|
|
260
|
-
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
261
|
-
)
|
|
262
|
-
return
|
|
263
|
-
}
|
|
253
|
+
try {
|
|
254
|
+
val sessionTranscript =
|
|
255
|
+
OpenID4VP(
|
|
256
|
+
clientId,
|
|
257
|
+
responseUri,
|
|
258
|
+
authorizationRequestNonce,
|
|
259
|
+
mdocGeneratedNonce
|
|
260
|
+
).createSessionTranscript()
|
|
264
261
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
promise,
|
|
270
|
-
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
271
|
-
)
|
|
272
|
-
return
|
|
273
|
-
}
|
|
262
|
+
val documentsParsed =
|
|
263
|
+
parseDocRequested(documents)
|
|
264
|
+
|
|
265
|
+
val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
|
|
274
266
|
|
|
275
|
-
try {
|
|
276
267
|
val responseGenerator = ResponseGenerator(sessionTranscript)
|
|
277
268
|
responseGenerator.createResponse(
|
|
278
269
|
documentsParsed,
|
|
279
|
-
|
|
270
|
+
parsedAcceptedFields,
|
|
280
271
|
object : ResponseGenerator.Response {
|
|
281
272
|
override fun onResponseGenerated(response: ByteArray) {
|
|
282
273
|
promise.resolve(Base64Utils.encodeBase64(response))
|
|
283
274
|
}
|
|
284
275
|
|
|
285
276
|
override fun onError(message: String) {
|
|
286
|
-
|
|
287
|
-
promise,
|
|
288
|
-
Pair(ERROR_USER_INFO_KEY, message)
|
|
289
|
-
)
|
|
277
|
+
promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, message)
|
|
290
278
|
}
|
|
291
279
|
}
|
|
292
280
|
)
|
|
293
281
|
} catch (e: Exception) {
|
|
294
|
-
|
|
295
|
-
promise,
|
|
296
|
-
Pair(ERROR_USER_INFO_KEY, e.message.orEmpty())
|
|
297
|
-
)
|
|
282
|
+
promise.reject(ModuleErrorCodes.GENERATE_OID4VP_RESPONSE_ERROR, e.message, e)
|
|
298
283
|
}
|
|
299
284
|
}
|
|
300
285
|
|
|
@@ -339,41 +324,6 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
339
324
|
})
|
|
340
325
|
}
|
|
341
326
|
|
|
342
|
-
/**
|
|
343
|
-
* Utility function which extracts the document shape we expect to receive from the bridge
|
|
344
|
-
* in the one expected by {DocRequested}.
|
|
345
|
-
* @param documents - A {ReadableArray} containing documents. Each document is defined as a map containing:
|
|
346
|
-
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
347
|
-
* - alias which is the alias of the key used to sign the credential;
|
|
348
|
-
* - docType which is the document type.
|
|
349
|
-
* @returns An array containing a {DocRequested} object for each document in {documents}
|
|
350
|
-
* @throws IllegalArgumentException if the provided document doesn't adhere to the expected format
|
|
351
|
-
*/
|
|
352
|
-
private fun parseDocRequested(documents: ReadableArray): Array<DocRequested> {
|
|
353
|
-
return (0 until documents.size()).map { i ->
|
|
354
|
-
val entry = documents.getMap(i)
|
|
355
|
-
?: throw IllegalArgumentException("Entry in ReadableArray is null")
|
|
356
|
-
|
|
357
|
-
val alias = entry.getString("alias")
|
|
358
|
-
val issuerSignedContentStr = entry.getString("issuerSignedContent")
|
|
359
|
-
val docType = entry.getString("docType")
|
|
360
|
-
|
|
361
|
-
if (
|
|
362
|
-
alias == null || entry.getType("alias") != ReadableType.String ||
|
|
363
|
-
issuerSignedContentStr == null || entry.getType("issuerSignedContent") != ReadableType.String ||
|
|
364
|
-
docType == null || entry.getType("docType") != ReadableType.String
|
|
365
|
-
) throw IllegalArgumentException("Unable to decode the provided documents")
|
|
366
|
-
|
|
367
|
-
val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
|
|
368
|
-
|
|
369
|
-
DocRequested(
|
|
370
|
-
issuerSignedContent,
|
|
371
|
-
alias,
|
|
372
|
-
docType
|
|
373
|
-
)
|
|
374
|
-
}.toTypedArray()
|
|
375
|
-
}
|
|
376
|
-
|
|
377
327
|
/**
|
|
378
328
|
* Wrapper function to send an event via `RCTEventEmitter`
|
|
379
329
|
* @param eventName - The event name
|
|
@@ -398,44 +348,165 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
|
|
|
398
348
|
*/
|
|
399
349
|
}
|
|
400
350
|
|
|
401
|
-
|
|
402
|
-
val
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
fun reject(
|
|
421
|
-
promise: Promise, vararg args: Pair<String, String>
|
|
422
|
-
) {
|
|
423
|
-
exMap(*args).let {
|
|
424
|
-
promise.reject(it.first, ex.message, it.second)
|
|
425
|
-
}
|
|
351
|
+
companion object {
|
|
352
|
+
const val NAME = "IoReactNativeIso18013"
|
|
353
|
+
const val NOT_INITIALIZED_ERROR_MESSAGE = "Resources not initialized properly, call the start method before this one."
|
|
354
|
+
|
|
355
|
+
// Errors which this module uses to reject a promise
|
|
356
|
+
private object ModuleErrorCodes {
|
|
357
|
+
// ISO18013-5 related errors
|
|
358
|
+
const val DRH_NOT_DEFINED = "DRH_NOT_DEFINED"
|
|
359
|
+
const val QR_ENGAGEMENT_NOT_DEFINED = "QR_ENGAGEMENT_NOT_DEFINED"
|
|
360
|
+
const val START_ERROR = "START_ERROR"
|
|
361
|
+
const val GET_QR_CODE_ERROR = "GET_QR_CODE_ERROR"
|
|
362
|
+
const val GENERATE_RESPONSE_ERROR = "GENERATE_RESPONSE_ERROR"
|
|
363
|
+
const val SEND_RESPONSE_ERROR = "SEND_RESPONSE_ERROR"
|
|
364
|
+
const val SEND_ERROR_RESPONSE_ERROR = "SEND_ERROR_RESPONSE_ERROR"
|
|
365
|
+
const val CLOSE_ERROR = "CLOSE_ERROR"
|
|
366
|
+
|
|
367
|
+
// ISO18013-7 related errors
|
|
368
|
+
const val GENERATE_OID4VP_RESPONSE_ERROR = "GENERATE_OID4VP_RESPONSE_ERROR"
|
|
426
369
|
}
|
|
427
370
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
371
|
+
/**
|
|
372
|
+
* Utility function which checks if the input map is consistent with what we expects before parsing
|
|
373
|
+
* it to a string.
|
|
374
|
+
* It loops through each credential and each namespace, checking if the accepted fields contain
|
|
375
|
+
* a boolean value.
|
|
376
|
+
* @param acceptedFields - A map contained the accepted fields to be presented with the following shape:
|
|
377
|
+
* {
|
|
378
|
+
* "org.iso.18013.5.1.mDL": {
|
|
379
|
+
* "org.iso.18013.5.1": {
|
|
380
|
+
* "hair_colour": true,
|
|
381
|
+
* "given_name_national_character": true,
|
|
382
|
+
* "family_name_national_character": true,
|
|
383
|
+
* "given_name": true,
|
|
384
|
+
* },
|
|
385
|
+
* {...}
|
|
386
|
+
* },
|
|
387
|
+
* {...}
|
|
388
|
+
* }
|
|
389
|
+
* @throw IllegalArgumentException if the ReadableMap is not consistent or contains an invalid value
|
|
390
|
+
* @returns String representation of [acceptedFields]
|
|
391
|
+
*/
|
|
392
|
+
fun parseAcceptedFields(acceptedFields: ReadableMap): String {
|
|
393
|
+
try {
|
|
394
|
+
// Loop for each credential and throw if something different than map is found
|
|
395
|
+
acceptedFields.entryIterator.forEach { credentialEntry ->
|
|
396
|
+
val credentialName = credentialEntry.key
|
|
397
|
+
val credentialValue = credentialEntry.value
|
|
398
|
+
if (credentialValue !is ReadableMap) {
|
|
399
|
+
throw IllegalArgumentException("Credential '$credentialName' must be a map")
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If no namespace is found then throw
|
|
403
|
+
if(!credentialValue.entryIterator.hasNext()){
|
|
404
|
+
throw IllegalArgumentException("Credential '$credentialName' must define at least one namespace")
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Loop for each namespace in credential and throw if something different than map is found
|
|
408
|
+
credentialValue.entryIterator.forEach { namespaceEntry ->
|
|
409
|
+
val namespaceName = namespaceEntry.key
|
|
410
|
+
val namespaceValue = namespaceEntry.value
|
|
411
|
+
if (namespaceValue !is ReadableMap) {
|
|
412
|
+
throw IllegalArgumentException("Namespace '$namespaceName' in credential '$credentialName' must be a map")
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// If no field is found then throw
|
|
416
|
+
if(!namespaceValue.entryIterator.hasNext()){
|
|
417
|
+
throw IllegalArgumentException("Credential '$credentialName' with namespace `$namespaceName` must define at least one field")
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Loop for each field in namespace and throw if something different than boolean is found
|
|
421
|
+
namespaceValue.entryIterator.forEach { fieldEntry ->
|
|
422
|
+
val fieldName = fieldEntry.key
|
|
423
|
+
val fieldValue = fieldEntry.value
|
|
424
|
+
if (fieldValue !is Boolean) {
|
|
425
|
+
throw IllegalArgumentException("Field '$fieldName' in namespace '$namespaceName' of credential '$credentialName' must be a boolean")
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// If no exception is thrown then we can convert it to string
|
|
431
|
+
return acceptedFields.toString()
|
|
432
|
+
} catch (e: Exception) {
|
|
433
|
+
throw IllegalArgumentException("Failed to parse accepted fields: ${e.message}", e)
|
|
434
|
+
}
|
|
432
435
|
}
|
|
433
|
-
}
|
|
434
436
|
|
|
435
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Utility function which extracts the document shape we expect to receive from the bridge
|
|
439
|
+
* in the one expected by {DocRequested}.
|
|
440
|
+
* @param documents a {ReadableArray} containing documents. Each document is defined as a map containing:
|
|
441
|
+
* - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
|
|
442
|
+
* - alias which is the alias of the key used to sign the credential;
|
|
443
|
+
* - docType which is the document type.
|
|
444
|
+
* @returns an array containing a {DocRequested} object for each document in {documents}
|
|
445
|
+
* @throws IllegalArgumentException if the provided document doesn't adhere to the expected format
|
|
446
|
+
*/
|
|
447
|
+
fun parseDocRequested(documents: ReadableArray): Array<DocRequested> {
|
|
448
|
+
return try {
|
|
449
|
+
(0 until documents.size()).map { i ->
|
|
450
|
+
val entry = documents.getMap(i)
|
|
451
|
+
?: throw IllegalArgumentException("Entry at index $i in ReadableArray is null")
|
|
452
|
+
val alias = entry.getString("alias")
|
|
453
|
+
val issuerSignedContentStr = entry.getString("issuerSignedContent")
|
|
454
|
+
val docType = entry.getString("docType")
|
|
455
|
+
|
|
456
|
+
if (
|
|
457
|
+
alias == null || entry.getType("alias") != ReadableType.String ||
|
|
458
|
+
issuerSignedContentStr == null || entry.getType("issuerSignedContent") != ReadableType.String ||
|
|
459
|
+
docType == null || entry.getType("docType") != ReadableType.String
|
|
460
|
+
) throw IllegalArgumentException("Unable to decode the provided documents at index $i")
|
|
461
|
+
|
|
462
|
+
val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
|
|
463
|
+
|
|
464
|
+
DocRequested(
|
|
465
|
+
issuerSignedContent,
|
|
466
|
+
alias,
|
|
467
|
+
docType
|
|
468
|
+
)
|
|
469
|
+
}.toTypedArray()
|
|
470
|
+
} catch (e: Exception) {
|
|
471
|
+
throw IllegalArgumentException("Failed to parse documents: ${e.message}", e)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
436
474
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
475
|
+
/**
|
|
476
|
+
* Utility function to parse an array coming from the React Native Bridge into an ArrayList
|
|
477
|
+
* of ByteArray representing DER encoded X.509 certificates.
|
|
478
|
+
* @param certificates two-dimensional array of base64 strings representing DER encoded X.509 certificate
|
|
479
|
+
* @returns ArrayList of ByteArray representing DER encoded X.509 certificates.
|
|
480
|
+
* @throws IllegalArgumentException if an element in the array is not base64 encoded
|
|
481
|
+
*/
|
|
482
|
+
fun parseCertificates(certificates: ReadableArray): List<List<ByteArray>> =
|
|
483
|
+
/** Map the chain arrays and remove null entries. On each chain call the getArray method which
|
|
484
|
+
* can throw and if it does rethrow an exception with information on its position
|
|
485
|
+
*/
|
|
486
|
+
(0 until certificates.size()).mapNotNull { chainIndex ->
|
|
487
|
+
val chain = runCatching { certificates.getArray(chainIndex) }
|
|
488
|
+
.getOrElse { throw IllegalArgumentException("Certificate chain at $chainIndex is not an array", it) }
|
|
489
|
+
?: throw IllegalArgumentException("Certificate chain at index $chainIndex is null")
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Map each chain certificate and remove null entries. On each certificate call the getString
|
|
493
|
+
* method which can throw and if it does rethrow an exception with information on its position
|
|
494
|
+
*/
|
|
495
|
+
(0 until chain.size()).mapNotNull { certIndex ->
|
|
496
|
+
val base64 = runCatching { chain.getString(certIndex) }
|
|
497
|
+
.getOrElse { throw IllegalArgumentException("Failed to get certificate string at chain $chainIndex, cert $certIndex", it) }
|
|
498
|
+
?: throw java.lang.IllegalArgumentException("Certificate at index $certIndex is null")
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Decode the base64 string for each mapped certificate and if an error occurs rethrow
|
|
502
|
+
* an exception with information on its position
|
|
503
|
+
*/
|
|
504
|
+
runCatching {
|
|
505
|
+
Base64Utils.decodeBase64(base64)
|
|
506
|
+
}.getOrElse {
|
|
507
|
+
throw IllegalArgumentException("Certificate at index $certIndex in the chain at index $chainIndex is not a valid base64 string", it)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
440
511
|
}
|
|
441
512
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package com.ioreactnativeiso18013
|
|
2
|
+
import org.junit.Assert.assertThrows
|
|
3
|
+
import org.junit.Test
|
|
4
|
+
|
|
5
|
+
data class Base64Data(
|
|
6
|
+
val array: ByteArray,
|
|
7
|
+
val string: String
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
class Base64UtilsTest {
|
|
11
|
+
private val base64 = Base64Data(
|
|
12
|
+
array = byteArrayOf(116, 101, 115, 116),
|
|
13
|
+
string = "dGVzdA=="
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
private val base64url = Base64Data(
|
|
17
|
+
array = byteArrayOf(60, 60, 63, 63, 63, 62, 62),
|
|
18
|
+
string = "PDw/Pz8+Pg=="
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
private val invalidBase64 = "123~"
|
|
22
|
+
|
|
23
|
+
@Test
|
|
24
|
+
fun `should convert a base64url string into a byte array`() {
|
|
25
|
+
val decodedArray = Base64Utils.decodeBase64AndBase64Url(base64url.string)
|
|
26
|
+
assert(decodedArray.contentEquals(base64url.array))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun `should convert a base64 string into a byte array`() {
|
|
31
|
+
val decodedArray = Base64Utils.decodeBase64AndBase64Url(base64.string)
|
|
32
|
+
assert(decodedArray.contentEquals(base64.array))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun `should throw IllegalArgumentException with an invalid string while decoding base64 or base64url string`() {
|
|
37
|
+
assertThrows(
|
|
38
|
+
IllegalArgumentException::class.java
|
|
39
|
+
) {
|
|
40
|
+
Base64Utils.decodeBase64AndBase64Url(invalidBase64)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun `should encode a byte array into a base64 string`() {
|
|
46
|
+
val encodedArray = Base64Utils.encodeBase64(base64.array)
|
|
47
|
+
assert(encodedArray == base64.string)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun `should decode a base64 string into a byte array`() {
|
|
53
|
+
val encodedArray = Base64Utils.decodeBase64(base64.string)
|
|
54
|
+
assert(encodedArray.contentEquals(base64.array))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun `should throw IllegalArgumentException with an invalid string while decoding base64`() {
|
|
59
|
+
assertThrows(
|
|
60
|
+
IllegalArgumentException::class.java
|
|
61
|
+
) {
|
|
62
|
+
Base64Utils.decodeBase64(invalidBase64)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|