@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.
Files changed (110) hide show
  1. package/README.md +10 -5
  2. package/android/build.gradle +7 -1
  3. package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeCborModule.kt +69 -134
  4. package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +254 -183
  5. package/android/src/test/java/com/ioreactnativeiso18013/Base64UtilsTest.kt +66 -0
  6. package/android/src/test/java/com/ioreactnativeiso18013/IoReactNativeIso18013Test.kt +508 -0
  7. package/ios/Base64Utils.swift +10 -3
  8. package/ios/IoReactNativeCbor.mm +21 -21
  9. package/ios/IoReactNativeCbor.swift +116 -138
  10. package/ios/IoReactNativeIso18013.mm +10 -10
  11. package/ios/IoReactNativeIso18013.swift +172 -193
  12. package/lib/module/cbor/cbor/README.md +39 -59
  13. package/lib/module/cbor/cbor/decoder.js +13 -23
  14. package/lib/module/cbor/cbor/decoder.js.map +1 -1
  15. package/lib/module/cbor/cbor/error.js +15 -0
  16. package/lib/module/cbor/cbor/error.js.map +1 -0
  17. package/lib/module/cbor/cbor/index.js +1 -0
  18. package/lib/module/cbor/cbor/index.js.map +1 -1
  19. package/lib/module/cbor/cose/README.md +39 -9
  20. package/lib/module/cbor/cose/error.js +17 -0
  21. package/lib/module/cbor/cose/error.js.map +1 -0
  22. package/lib/module/cbor/cose/index.js +1 -0
  23. package/lib/module/cbor/cose/index.js.map +1 -1
  24. package/lib/module/cbor/cose/sign.js +4 -3
  25. package/lib/module/cbor/cose/sign.js.map +1 -1
  26. package/lib/module/iso18013/index.js +0 -1
  27. package/lib/module/iso18013/index.js.map +1 -1
  28. package/lib/module/iso18013/iso18013-5/README.md +120 -44
  29. package/lib/module/iso18013/iso18013-5/error.js +25 -0
  30. package/lib/module/iso18013/iso18013-5/error.js.map +1 -0
  31. package/lib/module/iso18013/iso18013-5/index.js +3 -2
  32. package/lib/module/iso18013/iso18013-5/index.js.map +1 -1
  33. package/lib/module/iso18013/iso18013-5/proximity.js +20 -15
  34. package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -1
  35. package/lib/module/iso18013/iso18013-5/{schema.js → request.js} +1 -37
  36. package/lib/module/iso18013/iso18013-5/request.js.map +1 -0
  37. package/lib/module/iso18013/iso18013-7/README.md +66 -41
  38. package/lib/module/iso18013/iso18013-7/error.js +15 -0
  39. package/lib/module/iso18013/iso18013-7/error.js.map +1 -0
  40. package/lib/module/iso18013/iso18013-7/index.js +1 -0
  41. package/lib/module/iso18013/iso18013-7/index.js.map +1 -1
  42. package/lib/module/iso18013/iso18013-7/remote.js +11 -24
  43. package/lib/module/iso18013/iso18013-7/remote.js.map +1 -1
  44. package/lib/module/schema.js +46 -0
  45. package/lib/module/schema.js.map +1 -0
  46. package/lib/typescript/src/cbor/cbor/decoder.d.ts +12 -22
  47. package/lib/typescript/src/cbor/cbor/decoder.d.ts.map +1 -1
  48. package/lib/typescript/src/cbor/cbor/error.d.ts +68 -0
  49. package/lib/typescript/src/cbor/cbor/error.d.ts.map +1 -0
  50. package/lib/typescript/src/cbor/cbor/index.d.ts +1 -1
  51. package/lib/typescript/src/cbor/cbor/index.d.ts.map +1 -1
  52. package/lib/typescript/src/cbor/cose/error.d.ts +68 -0
  53. package/lib/typescript/src/cbor/cose/error.d.ts.map +1 -0
  54. package/lib/typescript/src/cbor/cose/index.d.ts +1 -1
  55. package/lib/typescript/src/cbor/cose/index.d.ts.map +1 -1
  56. package/lib/typescript/src/cbor/cose/sign.d.ts +4 -3
  57. package/lib/typescript/src/cbor/cose/sign.d.ts.map +1 -1
  58. package/lib/typescript/src/iso18013/index.d.ts +0 -1
  59. package/lib/typescript/src/iso18013/index.d.ts.map +1 -1
  60. package/lib/typescript/src/iso18013/iso18013-5/error.d.ts +73 -0
  61. package/lib/typescript/src/iso18013/iso18013-5/error.d.ts.map +1 -0
  62. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +4 -3
  63. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -1
  64. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +21 -14
  65. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -1
  66. package/lib/typescript/src/iso18013/iso18013-5/{schema.d.ts → request.d.ts} +1 -39
  67. package/lib/typescript/src/iso18013/iso18013-5/request.d.ts.map +1 -0
  68. package/lib/typescript/src/iso18013/iso18013-7/error.d.ts +68 -0
  69. package/lib/typescript/src/iso18013/iso18013-7/error.d.ts.map +1 -0
  70. package/lib/typescript/src/iso18013/iso18013-7/index.d.ts +3 -2
  71. package/lib/typescript/src/iso18013/iso18013-7/index.d.ts.map +1 -1
  72. package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts +11 -25
  73. package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts.map +1 -1
  74. package/lib/typescript/src/iso18013/types.d.ts +25 -0
  75. package/lib/typescript/src/iso18013/types.d.ts.map +1 -1
  76. package/lib/typescript/src/schema.d.ts +76 -0
  77. package/lib/typescript/src/schema.d.ts.map +1 -0
  78. package/package.json +1 -1
  79. package/src/cbor/cbor/README.md +39 -59
  80. package/src/cbor/cbor/decoder.ts +13 -23
  81. package/src/cbor/cbor/error.ts +23 -0
  82. package/src/cbor/cbor/index.ts +5 -1
  83. package/src/cbor/cose/README.md +39 -9
  84. package/src/cbor/cose/error.ts +23 -0
  85. package/src/cbor/cose/index.ts +5 -1
  86. package/src/cbor/cose/sign.ts +5 -4
  87. package/src/iso18013/index.ts +0 -7
  88. package/src/iso18013/iso18013-5/README.md +120 -44
  89. package/src/iso18013/iso18013-5/error.ts +35 -0
  90. package/src/iso18013/iso18013-5/index.ts +9 -8
  91. package/src/iso18013/iso18013-5/proximity.ts +21 -17
  92. package/src/iso18013/iso18013-5/{schema.ts → request.ts} +0 -42
  93. package/src/iso18013/iso18013-7/README.md +66 -41
  94. package/src/iso18013/iso18013-7/error.ts +21 -0
  95. package/src/iso18013/iso18013-7/index.ts +6 -5
  96. package/src/iso18013/iso18013-7/remote.ts +12 -34
  97. package/src/iso18013/types.ts +24 -0
  98. package/src/schema.ts +55 -0
  99. package/lib/module/cbor/cbor/failure.js +0 -2
  100. package/lib/module/cbor/cbor/failure.js.map +0 -1
  101. package/lib/module/cbor/cose/failure.js +0 -2
  102. package/lib/module/cbor/cose/failure.js.map +0 -1
  103. package/lib/module/iso18013/iso18013-5/schema.js.map +0 -1
  104. package/lib/typescript/src/cbor/cbor/failure.d.ts +0 -15
  105. package/lib/typescript/src/cbor/cbor/failure.d.ts.map +0 -1
  106. package/lib/typescript/src/cbor/cose/failure.d.ts +0 -15
  107. package/lib/typescript/src/cbor/cose/failure.d.ts.map +0 -1
  108. package/lib/typescript/src/iso18013/iso18013-5/schema.d.ts.map +0 -1
  109. package/src/cbor/cbor/failure.ts +0 -18
  110. 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 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 - Two-dimensional array of base64 strings 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.
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
- print(e)
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 strings.
97
- * @param promise - The promise which will be resolved in case of success or rejected in case of failure.
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
- ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
81
+ promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED,NOT_INITIALIZED_ERROR_MESSAGE)
107
82
  }
108
83
  } catch (e: Exception) {
109
- ModuleException.GET_QR_CODE_ERROR.reject(
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
- * It resolves to true after closing the connection.
119
- * @param promise - The promise which will be resolved in case of success or rejected in case of failure.
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
- ModuleException.CLOSE_ERROR.reject(
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 defined in table 20 of the ISO18013-5 standard.
137
- * @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.
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 - The promise which will be resolved in case of success or rejected in case of failure.
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
- ModuleException.SEND_ERROR_RESPONSE_ERROR.reject(
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
- ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
130
+ promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
160
131
  }
161
132
  } catch (e: Exception) {
162
- ModuleException.SEND_ERROR_RESPONSE_ERROR.reject(
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
- * @param documents - A {ReadableArray} containing documents. Each document is defined as a map containing:
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 fieldRequestedAndAccepted - The string containing the requested attributes. This is based on the request
177
- * provided by the {onDocumentRequestReceived} callback.
178
- * @param promise - The promise which will be resolved in case of success or rejected in case of failure.
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
- fieldRequestedAndAccepted: ReadableMap,
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
- fieldRequestedAndAccepted.toString(),
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
- ModuleException.GENERATE_RESPONSE_ERROR.reject(
202
- promise,
203
- Pair(ERROR_KEY, message)
204
- )
185
+ promise.reject(ModuleErrorCodes.GENERATE_RESPONSE_ERROR, message)
205
186
  }
206
187
  })
207
188
  } ?: run {
208
- ModuleException.DRH_NOT_DEFINED.reject(promise)
189
+ promise.reject(ModuleErrorCodes.DRH_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
209
190
  }
210
191
  } catch (e: Exception) {
211
- ModuleException.GENERATE_RESPONSE_ERROR.reject(
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
- * @param response - A base64 encoded string containing the response generated by {generateResponse}
223
- * @param promise - The promise which will be resolved in case of success or rejected in case of failure.
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
- ModuleException.QR_ENGAGEMENT_NOT_DEFINED_ERROR.reject(promise)
212
+ promise.reject(ModuleErrorCodes.QR_ENGAGEMENT_NOT_DEFINED, message=NOT_INITIALIZED_ERROR_MESSAGE)
235
213
  }
236
214
  } catch (e: Exception) {
237
- ModuleException.SEND_RESPONSE_ERROR.reject(
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
- fieldRequestedAndAccepted: String, promise: Promise
251
+ acceptedFields: ReadableMap, promise: Promise
249
252
  ) {
250
- val sessionTranscript = try {
251
- OpenID4VP(
252
- clientId,
253
- responseUri,
254
- authorizationRequestNonce,
255
- mdocGeneratedNonce
256
- ).createSessionTranscript()
257
- } catch (e: Exception) {
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
- val documentsParsed = try {
266
- parseDocRequested(documents)
267
- } catch (e: Exception) {
268
- ModuleException.INVALID_DOC_REQUESTED.reject(
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
- fieldRequestedAndAccepted,
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
- ModuleException.UNABLE_TO_GENERATE_RESPONSE.reject(
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
- ModuleException.UNABLE_TO_GENERATE_RESPONSE.reject(
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
- private enum class ModuleException(
402
- val ex: Exception
403
- ) {
404
- /** ISO18013-5 related errors **/
405
- DRH_NOT_DEFINED(Exception("DRH_NOT_DEFINED")),
406
- QR_ENGAGEMENT_NOT_DEFINED_ERROR(Exception("QR_ENGAGEMENT_NOT_DEFINED_ERROR")),
407
- START_ERROR(Exception("START_ERROR")),
408
- GET_QR_CODE_ERROR(Exception("GET_QR_CODE_ERROR")),
409
- GENERATE_RESPONSE_ERROR(Exception("GENERATE_RESPONSE_ERROR")),
410
- SEND_RESPONSE_ERROR(Exception("SEND_RESPONSE_ERROR")),
411
- SEND_ERROR_RESPONSE_ERROR(Exception("SEND_ERROR_RESPONSE_ERROR")),
412
- CLOSE_ERROR(Exception("CLOSE_ERROR")),
413
-
414
- /** ISO18013-7 related errors **/
415
- UNABLE_TO_GENERATE_RESPONSE(Exception("UNABLE_TO_GENERATE_RESPONSE")),
416
- UNABLE_TO_GENERATE_TRANSCRIPT(Exception("UNABLE_TO_GENERATE_TRANSCRIPT")),
417
- INVALID_DOC_REQUESTED(Exception("INVALID_DOC_REQUESTED")),
418
- GENERATE_OID4VP_DEVICE_RESPONSE_FAILED(Exception("GENERATE_OID4VP_DEVICE_RESPONSE_FAILED"));
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
- private fun exMap(vararg args: Pair<String, String>): Pair<String, WritableMap> {
429
- val writableMap = WritableNativeMap()
430
- args.forEach { writableMap.putString(it.first, it.second) }
431
- return Pair(this.ex.message ?: "UNKNOWN", writableMap)
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
- private fun getExceptionMessageOrEmpty(e: Exception): String = e.message ?: ""
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
- companion object {
438
- const val NAME = "IoReactNativeIso18013"
439
- const val ERROR_KEY = "error"
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
+