@pagopa/io-react-native-iso18013 0.4.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 (54) 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 +90 -58
  4. package/android/src/main/java/com/ioreactnativeiso18013/IoReactNativeIso18013Module.kt +216 -100
  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/IoReactNativeCbor.mm +21 -21
  8. package/ios/IoReactNativeCbor.swift +70 -22
  9. package/ios/IoReactNativeIso18013.mm +10 -10
  10. package/ios/IoReactNativeIso18013.swift +89 -81
  11. package/lib/module/cbor/cbor/README.md +11 -63
  12. package/lib/module/cbor/cbor/decoder.js +13 -23
  13. package/lib/module/cbor/cbor/decoder.js.map +1 -1
  14. package/lib/module/cbor/cose/README.md +1 -1
  15. package/lib/module/cbor/cose/sign.js +4 -3
  16. package/lib/module/cbor/cose/sign.js.map +1 -1
  17. package/lib/module/iso18013/iso18013-5/README.md +66 -26
  18. package/lib/module/iso18013/iso18013-5/index.js.map +1 -1
  19. package/lib/module/iso18013/iso18013-5/proximity.js +19 -6
  20. package/lib/module/iso18013/iso18013-5/proximity.js.map +1 -1
  21. package/lib/module/iso18013/iso18013-5/request.js +0 -17
  22. package/lib/module/iso18013/iso18013-5/request.js.map +1 -1
  23. package/lib/module/iso18013/iso18013-7/README.md +32 -36
  24. package/lib/module/iso18013/iso18013-7/remote.js +11 -12
  25. package/lib/module/iso18013/iso18013-7/remote.js.map +1 -1
  26. package/lib/typescript/src/cbor/cbor/decoder.d.ts +12 -22
  27. package/lib/typescript/src/cbor/cbor/decoder.d.ts.map +1 -1
  28. package/lib/typescript/src/cbor/cose/sign.d.ts +4 -3
  29. package/lib/typescript/src/cbor/cose/sign.d.ts.map +1 -1
  30. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts +2 -2
  31. package/lib/typescript/src/iso18013/iso18013-5/index.d.ts.map +1 -1
  32. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts +20 -8
  33. package/lib/typescript/src/iso18013/iso18013-5/proximity.d.ts.map +1 -1
  34. package/lib/typescript/src/iso18013/iso18013-5/request.d.ts +0 -23
  35. package/lib/typescript/src/iso18013/iso18013-5/request.d.ts.map +1 -1
  36. package/lib/typescript/src/iso18013/iso18013-7/index.d.ts +1 -1
  37. package/lib/typescript/src/iso18013/iso18013-7/index.d.ts.map +1 -1
  38. package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts +11 -11
  39. package/lib/typescript/src/iso18013/iso18013-7/remote.d.ts.map +1 -1
  40. package/lib/typescript/src/iso18013/types.d.ts +25 -0
  41. package/lib/typescript/src/iso18013/types.d.ts.map +1 -1
  42. package/package.json +1 -1
  43. package/src/cbor/cbor/README.md +11 -63
  44. package/src/cbor/cbor/decoder.ts +13 -23
  45. package/src/cbor/cose/README.md +1 -1
  46. package/src/cbor/cose/sign.ts +5 -4
  47. package/src/iso18013/iso18013-5/README.md +66 -26
  48. package/src/iso18013/iso18013-5/index.ts +2 -6
  49. package/src/iso18013/iso18013-5/proximity.ts +20 -8
  50. package/src/iso18013/iso18013-5/request.ts +0 -22
  51. package/src/iso18013/iso18013-7/README.md +32 -36
  52. package/src/iso18013/iso18013-7/index.ts +1 -1
  53. package/src/iso18013/iso18013-7/remote.ts +12 -14
  54. package/src/iso18013/types.ts +24 -0
package/README.md CHANGED
@@ -10,12 +10,17 @@ npm install @pagopa/io-react-native-iso18013
10
10
 
11
11
  ## Usage
12
12
 
13
- Each module is documented in its own README file:
13
+ This library includes two native modules for both iOS and Android:
14
14
 
15
- - [ISO18013-5](src/iso18013/iso18013-5/README.md): Proximity presentation according to the ISO 18013-5 standard;
16
- - [ISO18013-7](src/iso18013/iso18013-7/README.md): Remote presentation according to the ISO 18013-7 standard;
17
- - [CBOR](src/cbor/cbor/README.md): Decoding of CBOR data into readable objects;
18
- - [COSE](src/cbor/cose/README.md): Management of COSE verify and sign operations.
15
+ - [IoReactNativeIso18013](src/iso18013/iso18013-5/proximity.ts) for ISO 18013-5 and ISO 18013-7 implementations;
16
+ - [IoReactNativeCbor](src/cbor/cbor.ts) for CBOR and COSE implementations.
17
+
18
+ On the javascript side, the library exposes the following modules:
19
+
20
+ - [ISO18013-5](src/iso18013/iso18013-5/README.md): Proximity presentation according to the ISO 18013-5 standard which is part of the `IoReactNativeIso18013` module;
21
+ - [ISO18013-7](src/iso18013/iso18013-7/README.md): Remote presentation according to the ISO 18013-7 standard which is part of the `IoReactNativeIso18013` module;
22
+ - [CBOR](src/cbor/cbor/README.md): Decoding of CBOR data into readable objects which is part of the `IoReactNativeCbor` module;
23
+ - [COSE](src/cbor/cose/README.md): Management of COSE verify and sign operations which is part of the `IoReactNativeCbor` module.
19
24
 
20
25
  ## Contributing
21
26
 
@@ -65,6 +65,12 @@ android {
65
65
  sourceCompatibility JavaVersion.VERSION_1_8
66
66
  targetCompatibility JavaVersion.VERSION_1_8
67
67
  }
68
+
69
+ testOptions {
70
+ unitTests.all {
71
+ useJUnit()
72
+ }
73
+ }
68
74
  }
69
75
 
70
76
  repositories {
@@ -79,5 +85,5 @@ dependencies {
79
85
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
80
86
  implementation "it.pagopa.io.wallet.proximity:proximity:2.1.0"
81
87
  implementation "it.pagopa.io.wallet.cbor:cbor:1.3.0"
88
+ testImplementation('junit:junit:4.13.2')
82
89
  }
83
-
@@ -16,6 +16,13 @@ class IoReactNativeCborModule(reactContext: ReactApplicationContext) :
16
16
  return NAME
17
17
  }
18
18
 
19
+ /**
20
+ * Decode base64 or base64url encoded CBOR data to JSON object.
21
+ * Resolves with a string containing the parsed data or rejects with an error code
22
+ * defined in [ModuleErrorCodes].
23
+ * This method does not handle nested CBOR data, which will need additional parsing.
24
+ * @param data the base64 or base64url encoded CBOR string
25
+ */
19
26
  @ReactMethod
20
27
  fun decode(data: String, promise: Promise) {
21
28
  try {
@@ -27,75 +34,100 @@ class IoReactNativeCborModule(reactContext: ReactApplicationContext) :
27
34
  }
28
35
  }
29
36
 
30
-
31
- @ReactMethod
32
- fun decodeDocuments(data: String, promise: Promise) {
33
- try {
34
- val buffer = Base64Utils.decodeBase64AndBase64Url(data)
35
- CBorParser(buffer).documentsCborToJson(separateElementIdentifier = true, onComplete = {
36
- promise.resolve(it)
37
- }, onError = { e ->
38
- promise.reject(ModuleErrorCodes.DECODE_DOCUMENTS_ERROR, e.message, e)
39
- })
40
- } catch (e: Exception) {
37
+ /**
38
+ * Decode base64 or base64url encoded mDOC-CBOR data to a JSON object.
39
+ * Resolves with a string containing the parsed data or rejects with an error code
40
+ * @param data the base64 or base64url encoded mDOC-CBOR string
41
+ */
42
+ @ReactMethod
43
+ fun decodeDocuments(data: String, promise: Promise) {
44
+ try {
45
+ val buffer = Base64Utils.decodeBase64AndBase64Url(data)
46
+ CBorParser(buffer).documentsCborToJson(separateElementIdentifier = true, onComplete = {
47
+ promise.resolve(it)
48
+ }, onError = { e ->
41
49
  promise.reject(ModuleErrorCodes.DECODE_DOCUMENTS_ERROR, e.message, e)
42
- }
50
+ })
51
+ } catch (e: Exception) {
52
+ promise.reject(ModuleErrorCodes.DECODE_DOCUMENTS_ERROR, e.message, e)
43
53
  }
54
+ }
44
55
 
45
- @ReactMethod
46
- fun decodeIssuerSigned(issuerSigned: String, promise: Promise) {
47
- try {
48
- val buffer =
49
- Base64Utils.decodeBase64AndBase64Url(issuerSigned)
50
- val result =
51
- CBorParser(buffer).issuerSignedCborToJson(separateElementIdentifier = true) ?: run {
52
- // We don't have the exact error here for some reason
53
- promise.reject(
54
- ModuleErrorCodes.DECODE_ISSUER_SIGNED_ERROR,
55
- "An error occurred while decoding the issuer signed content"
56
- )
57
- }
58
- promise.resolve(result)
59
- } catch (e: Exception) {
60
- promise.reject(ModuleErrorCodes.DECODE_ISSUER_SIGNED_ERROR, e.message, e)
61
- }
56
+ /**
57
+ * Decode base64 or base64url encoded issuerSigned attribute part of an mDOC-CBOR.
58
+ * @param data the base64 or base64url encoded mDOC-CBOR containing the issuerSigned data string
59
+ * Resolves with a string containing the parsed data or rejects with an error code
60
+ * defined in [ModuleErrorCodes].
61
+ */
62
+ @ReactMethod
63
+ fun decodeIssuerSigned(data: String, promise: Promise) {
64
+ try {
65
+ val buffer =
66
+ Base64Utils.decodeBase64AndBase64Url(data)
67
+ val result =
68
+ CBorParser(buffer).issuerSignedCborToJson(separateElementIdentifier = true) ?: run {
69
+ // We don't have the exact error here for some reason
70
+ promise.reject(
71
+ ModuleErrorCodes.DECODE_ISSUER_SIGNED_ERROR,
72
+ "An error occurred while decoding the issuer signed content"
73
+ )
74
+ }
75
+ promise.resolve(result)
76
+ } catch (e: Exception) {
77
+ promise.reject(ModuleErrorCodes.DECODE_ISSUER_SIGNED_ERROR, e.message, e)
62
78
  }
79
+ }
63
80
 
64
- @ReactMethod
65
- fun sign(payload: String, keyTag: String, promise: Promise){
81
+ /**
82
+ * Sign base64 encoded data with COSE and return the COSE-Sign1 object in base64 encoding.
83
+ * Resolves with a string containing the COSE-Sign1 object in base64 encoding or rejects with an
84
+ * error code defined in [ModuleErrorCodes].
85
+ * @param data the base64 or base64url encoded payload to sign
86
+ * @param keyTag the alias of the key to use for signing.
87
+ */
88
+ @ReactMethod
89
+ fun sign(data: String, keyTag: String, promise: Promise) {
66
90
  try {
67
- val data = Base64Utils.decodeBase64AndBase64Url(payload)
68
- val result = COSEManager().signWithCOSE(
69
- data = data,
70
- alias = keyTag
71
- )
72
- when (result) {
73
- is SignWithCOSEResult.Failure -> {
74
- // We don't have a throwable to pass here from the onError callback
75
- promise.reject(ModuleErrorCodes.SIGN_ERROR, result.reason.msg)
76
- }
77
- is SignWithCOSEResult.Success -> {
78
- promise.resolve(Base64Utils.encodeBase64(result.signature))
79
- }
91
+ val buffer = Base64Utils.decodeBase64AndBase64Url(data)
92
+ val result = COSEManager().signWithCOSE(
93
+ data = buffer,
94
+ alias = keyTag
95
+ )
96
+ when (result) {
97
+ is SignWithCOSEResult.Failure -> {
98
+ // We don't have a throwable to pass here from the onError callback
99
+ promise.reject(ModuleErrorCodes.SIGN_ERROR, result.reason.msg)
100
+ }
101
+
102
+ is SignWithCOSEResult.Success -> {
103
+ promise.resolve(Base64Utils.encodeBase64(result.signature))
80
104
  }
81
- } catch (e: Exception) {
82
- promise.reject(ModuleErrorCodes.SIGN_ERROR, e.message, e)
83
105
  }
106
+ } catch (e: Exception) {
107
+ promise.reject(ModuleErrorCodes.SIGN_ERROR, e.message, e)
84
108
  }
109
+ }
85
110
 
86
- @ReactMethod
87
- fun verify(sign1Data: String, publicKey: ReadableMap, promise: Promise) {
88
- try {
89
- val data = Base64Utils.decodeBase64AndBase64Url(sign1Data)
90
- val result = COSEManager().verifySign1FromJWK(
91
- dataSigned = data,
92
- jwk = publicKey.toString()
93
- )
94
- promise.resolve(result)
95
- } catch (e: Exception) {
96
- promise.reject(ModuleErrorCodes.VERIFY_ERROR, e.message, e)
97
- }
111
+ /**
112
+ * Verifies a COSE-Sign1 object with the provided public key.
113
+ * Resolves with a boolean indicating whether or not the verification succeeded or not or rejects
114
+ * with an error code defined in [ModuleErrorCodes].
115
+ * @param data the COSE-Sign1 object in base64 or base64url encoding
116
+ * @param publicKey the public key in JWK format
117
+ */
118
+ @ReactMethod
119
+ fun verify(data: String, publicKey: ReadableMap, promise: Promise) {
120
+ try {
121
+ val buffer = Base64Utils.decodeBase64AndBase64Url(data)
122
+ val result = COSEManager().verifySign1FromJWK(
123
+ dataSigned = buffer,
124
+ jwk = publicKey.toString()
125
+ )
126
+ promise.resolve(result)
127
+ } catch (e: Exception) {
128
+ promise.reject(ModuleErrorCodes.VERIFY_ERROR, e.message, e)
98
129
  }
130
+ }
99
131
 
100
132
  companion object {
101
133
  const val NAME = "IoReactNativeCbor"
@@ -30,12 +30,12 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
30
30
 
31
31
  /**
32
32
  * Starts the proximity flow by allocating the necessary resources and initializing the Bluetooth stack.
33
- * Resolves to true or rejects if an error occurs.
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.
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.
39
39
  */
40
40
  @ReactMethod
41
41
  fun start(
@@ -66,48 +66,10 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
66
66
  }
67
67
  }
68
68
 
69
- /**
70
- * Utility function to parse an array coming from the React Native Bridge into an ArrayList
71
- * of ByteArray representing DER encoded X.509 certificates.
72
- * @param certificates - Two-dimensional array of base64 strings representing DER encoded X.509 certificate
73
- * @returns An ArrayList of ByteArray representing DER encoded X.509 certificates.
74
- * @throws IllegalArgumentException if an element in the array is not base64 encoded
75
- */
76
- private fun parseCertificates(certificates: ReadableArray): List<List<ByteArray>> =
77
- /** Map the chain arrays and remove null entries. On each chain call the getArray method which
78
- * can throw and if it does rethrow an exception with information on its position
79
- */
80
- (0 until certificates.size()).mapNotNull { chainIndex ->
81
- val chain = runCatching { certificates.getArray(chainIndex) }
82
- .getOrElse { throw IllegalArgumentException("Certificate chain at $chainIndex is not an array", it) }
83
- ?: throw IllegalArgumentException("Certificate chain at index $chainIndex is null")
84
-
85
- /**
86
- * Map each chain certificate and remove null entries. On each certificate call the getString
87
- * method which can throw and if it does rethrow an exception with information on its position
88
- */
89
- (0 until chain.size()).mapNotNull { certIndex ->
90
- val base64 = runCatching { chain.getString(certIndex) }
91
- .getOrElse { throw IllegalArgumentException("Failed to get certificate string at chain $chainIndex, cert $certIndex", it) }
92
- ?: throw java.lang.IllegalArgumentException("Certificate at index $certIndex is null")
93
-
94
- /**
95
- * Decode the base64 string for each mapped certificate and if an error occurs rethrow
96
- * an exception with information on its position
97
- */
98
- runCatching {
99
- Base64Utils.decodeBase64(base64)
100
- }.getOrElse {
101
- throw IllegalArgumentException("Certificate at index $certIndex in the chain at index $chainIndex is not a valid base64 string", it)
102
- }
103
- }
104
- }
105
-
106
-
107
69
  /**
108
70
  * Creates a QR code to be scanned in order to initialize the presentation.
109
- * Resolves with the QR code strings.
110
- * @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.
111
73
  */
112
74
  @ReactMethod
113
75
  fun getQrCodeString(promise: Promise) {
@@ -125,8 +87,10 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
125
87
 
126
88
  /**
127
89
  * Closes the bluetooth connection and clears any resource.
128
- * It resolves to true after closing the connection.
129
- * @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.
130
94
  */
131
95
  @ReactMethod
132
96
  fun close(promise: Promise) {
@@ -140,13 +104,16 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
140
104
  }
141
105
 
142
106
  /**
143
- * Sends an error response during the presentation according to the SessionData status codes defined in table 20 of the ISO18013-5 standard.
144
- * @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.
145
112
  * The accepted values are defined in ``SessionDataStatus`` as follows:
146
113
  * 10 -> Error: session encryption
147
114
  * 11 -> Error: CBOR decoding
148
115
  * 20 -> Session termination
149
- * @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.
150
117
  */
151
118
  @ReactMethod
152
119
  fun sendErrorResponse(code: Double, promise: Promise) {
@@ -170,18 +137,33 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
170
137
  /**
171
138
  * Generates a response which can later be sent with {sendResponse} with the provided
172
139
  * CBOR documents and the requested attributes.
173
- * @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:
174
142
  * - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
175
143
  * - alias which is the alias of the key used to sign the credential;
176
144
  * - docType which is the document type.
177
- * @param fieldRequestedAndAccepted - The string containing the requested attributes. This is based on the request
178
- * provided by the {onDocumentRequestReceived} callback.
179
- * @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.
180
162
  */
181
163
  @ReactMethod
182
164
  fun generateResponse(
183
165
  documents: ReadableArray,
184
- fieldRequestedAndAccepted: ReadableMap,
166
+ acceptedFields: ReadableMap,
185
167
  promise: Promise
186
168
  ) {
187
169
  try {
@@ -191,8 +173,9 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
191
173
 
192
174
  val sessionTranscript = devHelper.sessionTranscript()
193
175
  val responseGenerator = ResponseGenerator(sessionTranscript)
176
+ val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
194
177
  responseGenerator.createResponse(docRequestedList,
195
- fieldRequestedAndAccepted.toString(),
178
+ parsedAcceptedFields,
196
179
  object : ResponseGenerator.Response {
197
180
  override fun onResponseGenerated(response: ByteArray) {
198
181
  promise.resolve(Base64Utils.encodeBase64(response))
@@ -212,10 +195,10 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
212
195
 
213
196
  /**
214
197
  * Sends a response containing the documents and the fields which the user decided to present generated by {generateResponse}.
215
- * It resolves to true after sending the response, otherwise it rejects if an error occurs while decoding the response.
216
198
  * Currently there's not evidence of the verifier app responding to this request, thus we don't handle the response.
217
- * @param response - A base64 encoded string containing the response generated by {generateResponse}
218
- * @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.
219
202
  */
220
203
  @ReactMethod
221
204
  fun sendResponse(response: String, promise: Promise) {
@@ -233,11 +216,39 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
233
216
  }
234
217
  }
235
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
+ */
236
247
  @ReactMethod
237
248
  fun generateOID4VPDeviceResponse(
238
249
  clientId: String, responseUri: String, authorizationRequestNonce: String,
239
250
  mdocGeneratedNonce: String, documents: ReadableArray,
240
- fieldRequestedAndAccepted: String, promise: Promise
251
+ acceptedFields: ReadableMap, promise: Promise
241
252
  ) {
242
253
  try {
243
254
  val sessionTranscript =
@@ -251,10 +262,12 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
251
262
  val documentsParsed =
252
263
  parseDocRequested(documents)
253
264
 
265
+ val parsedAcceptedFields = parseAcceptedFields(acceptedFields)
266
+
254
267
  val responseGenerator = ResponseGenerator(sessionTranscript)
255
268
  responseGenerator.createResponse(
256
269
  documentsParsed,
257
- fieldRequestedAndAccepted,
270
+ parsedAcceptedFields,
258
271
  object : ResponseGenerator.Response {
259
272
  override fun onResponseGenerated(response: ByteArray) {
260
273
  promise.resolve(Base64Utils.encodeBase64(response))
@@ -311,44 +324,6 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
311
324
  })
312
325
  }
313
326
 
314
- /**
315
- * Utility function which extracts the document shape we expect to receive from the bridge
316
- * in the one expected by {DocRequested}.
317
- * @param documents - A {ReadableArray} containing documents. Each document is defined as a map containing:
318
- * - issuerSignedContent which is a base64 or base64url encoded string representing the credential;
319
- * - alias which is the alias of the key used to sign the credential;
320
- * - docType which is the document type.
321
- * @returns An array containing a {DocRequested} object for each document in {documents}
322
- * @throws IllegalArgumentException if the provided document doesn't adhere to the expected format
323
- */
324
- private fun parseDocRequested(documents: ReadableArray): Array<DocRequested> {
325
- return try {
326
- (0 until documents.size()).map { i ->
327
- val entry = documents.getMap(i)
328
- ?: throw IllegalArgumentException("Entry at index $i in ReadableArray is null")
329
- val alias = entry.getString("alias")
330
- val issuerSignedContentStr = entry.getString("issuerSignedContent")
331
- val docType = entry.getString("docType")
332
-
333
- if (
334
- alias == null || entry.getType("alias") != ReadableType.String ||
335
- issuerSignedContentStr == null || entry.getType("issuerSignedContent") != ReadableType.String ||
336
- docType == null || entry.getType("docType") != ReadableType.String
337
- ) throw IllegalArgumentException("Unable to decode the provided documents at index $i")
338
-
339
- val issuerSignedContent = Base64Utils.decodeBase64AndBase64Url(issuerSignedContentStr)
340
-
341
- DocRequested(
342
- issuerSignedContent,
343
- alias,
344
- docType
345
- )
346
- }.toTypedArray()
347
- } catch (e: Exception) {
348
- throw IllegalArgumentException("Failed to parse documents: ${e.message}", e)
349
- }
350
- }
351
-
352
327
  /**
353
328
  * Wrapper function to send an event via `RCTEventEmitter`
354
329
  * @param eventName - The event name
@@ -392,5 +367,146 @@ class IoReactNativeIso18013Module(reactContext: ReactApplicationContext) :
392
367
  // ISO18013-7 related errors
393
368
  const val GENERATE_OID4VP_RESPONSE_ERROR = "GENERATE_OID4VP_RESPONSE_ERROR"
394
369
  }
370
+
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
+ }
435
+ }
436
+
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
+ }
474
+
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
+ }
395
511
  }
396
512
  }