@trustchex/react-native-sdk 1.500.2 → 1.501.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/android/src/main/java/com/trustchex/reactnativesdk/GeolocationModule.kt +159 -0
  2. package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
  3. package/ios/Permission/CoarseLocationProvider.swift +111 -0
  4. package/ios/Permission/PermissionModule.m +4 -0
  5. package/ios/Permission/PermissionModule.swift +21 -1
  6. package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +230 -0
  7. package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +12 -0
  8. package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +33 -10
  9. package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +8 -10
  10. package/lib/module/Screens/Static/ResultScreen.js +74 -3
  11. package/lib/module/Shared/Components/EIDScanner.js +59 -3
  12. package/lib/module/Shared/Components/NavigationManager.js +6 -2
  13. package/lib/module/Shared/EIDReader/eidReader.js +52 -3
  14. package/lib/module/Shared/EIDReader/java/inputStream.js +6 -0
  15. package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +60 -20
  16. package/lib/module/Shared/EIDReader/passiveAuth/der.js +93 -0
  17. package/lib/module/Shared/EIDReader/passiveAuth/integrity.js +71 -0
  18. package/lib/module/Shared/EIDReader/passiveAuth/sodParser.js +123 -0
  19. package/lib/module/Shared/Libs/chipVizMatch.js +93 -0
  20. package/lib/module/Shared/Libs/country-allow.js +16 -0
  21. package/lib/module/Shared/Libs/documentDataNormalizer.js +21 -11
  22. package/lib/module/Shared/Libs/geolocation.utils.js +77 -0
  23. package/lib/module/Shared/Libs/promise.utils.js +6 -0
  24. package/lib/module/Shared/Services/DataUploadService.js +18 -1
  25. package/lib/module/Translation/Resources/en.js +16 -0
  26. package/lib/module/Translation/Resources/tr.js +16 -0
  27. package/lib/module/Trustchex.js +4 -0
  28. package/lib/module/version.js +1 -1
  29. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
  30. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
  31. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  32. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  37. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
  39. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  40. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
  41. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
  42. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
  43. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  44. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
  45. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
  46. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
  47. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
  48. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
  49. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
  50. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
  51. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
  52. package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
  53. package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
  54. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
  55. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
  56. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
  57. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  58. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
  60. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
  62. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
  64. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  65. package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
  66. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
  68. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  69. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  70. package/lib/typescript/src/version.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
  73. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
  74. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
  75. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
  76. package/src/Screens/Static/ResultScreen.tsx +101 -6
  77. package/src/Shared/Components/EIDScanner.tsx +69 -2
  78. package/src/Shared/Components/NavigationManager.tsx +6 -0
  79. package/src/Shared/EIDReader/eidReader.ts +75 -2
  80. package/src/Shared/EIDReader/java/inputStream.ts +6 -0
  81. package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
  82. package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
  83. package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
  84. package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
  85. package/src/Shared/Libs/chipVizMatch.ts +149 -0
  86. package/src/Shared/Libs/country-allow.ts +17 -0
  87. package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
  88. package/src/Shared/Libs/geolocation.utils.ts +87 -0
  89. package/src/Shared/Libs/promise.utils.ts +11 -0
  90. package/src/Shared/Services/DataUploadService.ts +23 -0
  91. package/src/Shared/Types/documentData.ts +8 -0
  92. package/src/Shared/Types/documentReadResult.ts +20 -0
  93. package/src/Shared/Types/identificationInfo.ts +97 -2
  94. package/src/Translation/Resources/en.ts +21 -0
  95. package/src/Translation/Resources/tr.ts +21 -0
  96. package/src/Trustchex.tsx +5 -0
  97. package/src/version.ts +1 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  import React, { useContext, useEffect, useState } from 'react';
4
4
  import { buildDocumentName } from "../../Shared/Libs/mrzTransliteration.js";
5
+ import { isCountryAllowed } from "../../Shared/Libs/country-allow.js";
5
6
  import { normalizeFromMRZFields } from "../../Shared/Libs/documentDataNormalizer.js";
6
7
  import { Alert, SafeAreaView, StyleSheet, View } from 'react-native';
7
8
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -64,16 +65,13 @@ const IdentityDocumentScanningScreen = () => {
64
65
  appContext.identificationInfo.scannedDocument.hologramImage = idFrontSideData?.hologramImage || passportData?.hologramImage;
65
66
  appContext.identificationInfo.scannedDocument.barcodeValue = idBackSideData?.barcodeValue;
66
67
  appContext.identificationInfo.scannedDocument.documentType = mrzFields.documentCode === 'I' ? 'ID' : mrzFields.documentCode === 'P' ? 'PASSPORT' : 'UNKNOWN';
67
- if (allowedCountries && allowedCountries.length > 0) {
68
- const countryCode = mrzFields.issuingState;
69
- if (!allowedCountries.includes(countryCode)) {
70
- // Workflow validation - user's document country not in allowed list
71
- // Expected behavior, not a bug
72
- Alert.alert(t('general.error'), t('general.countryNotAllowed'));
73
- appContext.onError?.(t('general.countryNotAllowed'));
74
- navigationManagerRef.current?.reset();
75
- return;
76
- }
68
+ if (!isCountryAllowed(mrzFields.issuingState, allowedCountries)) {
69
+ // Workflow validation - user's document country not in allowed list
70
+ // Expected behavior, not a bug
71
+ Alert.alert(t('general.error'), t('general.countryNotAllowed'));
72
+ appContext.onError?.(t('general.countryNotAllowed'));
73
+ navigationManagerRef.current?.reset();
74
+ return;
77
75
  }
78
76
  if (allowedDocumentTypes && allowedDocumentTypes.length > 0) {
79
77
  const documentCode = mrzFields.documentCode;
@@ -67,7 +67,9 @@ const ResultScreen = () => {
67
67
  consent: false,
68
68
  document: false,
69
69
  media: false,
70
- verbalConsentVideos: false
70
+ verbalConsentVideos: false,
71
+ address: false,
72
+ passiveAuth: false
71
73
  });
72
74
  const [shouldShowDemoData, setShouldShowDemoData] = useState(false);
73
75
  const [deviceIdentifier, setDeviceIdentifier] = useState('');
@@ -183,11 +185,55 @@ const ResultScreen = () => {
183
185
  encryptedData,
184
186
  nonce
185
187
  } = encryptWithAes(JSON.stringify(appContext.identificationInfo.consent), sessionKey);
186
- await httpClient.post(`${apiUrl}/identifications/${identificationId}/consents`, {
188
+ const response = await httpClient.post(`${apiUrl}/identifications/${identificationId}/consents`, {
187
189
  encryptedData,
188
190
  nonce
189
191
  });
192
+ // MASAK 4/C(1)(e): the server risk-evaluates the technical data and may
193
+ // instruct us to terminate the process. Honor it by aborting the flow.
194
+ if (response?.terminate) {
195
+ throw Object.assign(new Error('session terminated'), {
196
+ sessionTerminated: true,
197
+ terminateReason: response.reason ?? undefined
198
+ });
199
+ }
190
200
  }, [apiUrl, appContext.identificationInfo.consent]);
201
+
202
+ // MASAK 4/C(1)(d): submit the captured address. The proof document image is
203
+ // uploaded separately as ADDRESS_PROOF_DOCUMENT media.
204
+ const submitIdentificationAddress = useCallback(async (identificationId, sessionKey) => {
205
+ const address = appContext.identificationInfo.address;
206
+ if (!address?.addressText) return;
207
+ const {
208
+ encryptedData,
209
+ nonce
210
+ } = encryptWithAes(JSON.stringify({
211
+ addressText: address.addressText,
212
+ fromChip: address.fromChip
213
+ }), sessionKey);
214
+ await httpClient.post(`${apiUrl}/identifications/${identificationId}/address`, {
215
+ encryptedData,
216
+ nonce
217
+ });
218
+ }, [apiUrl, appContext.identificationInfo.address]);
219
+
220
+ // MASAK 4/C(1)(b),(c): submit EF.SOD for server-side Passive Authentication
221
+ // (signature + CSCA chaining). Only when the eID step ran with passiveAuth.
222
+ const submitPassiveAuth = useCallback(async (identificationId, sessionKey) => {
223
+ const sd = appContext.identificationInfo.scannedDocument;
224
+ if (!sd?.paSodBase64) return;
225
+ const {
226
+ encryptedData,
227
+ nonce
228
+ } = encryptWithAes(JSON.stringify({
229
+ sodBase64: sd.paSodBase64,
230
+ integrityStatus: sd.paIntegrityStatus ?? null
231
+ }), sessionKey);
232
+ await httpClient.post(`${apiUrl}/identifications/${identificationId}/passive-auth`, {
233
+ encryptedData,
234
+ nonce
235
+ });
236
+ }, [apiUrl, appContext.identificationInfo.scannedDocument]);
191
237
  const submitIdentificationDocument = useCallback(async (identificationId, scannedDocument, sessionKey) => {
192
238
  if (!scannedDocument.mrzFields) {
193
239
  throw new Error('MRZ fields not found');
@@ -204,6 +250,8 @@ const ResultScreen = () => {
204
250
  gender: getGenderEnumType(rawMrzFields.sex),
205
251
  number: rawMrzFields.documentNumber,
206
252
  country: rawMrzFields.issuingState,
253
+ // Raw ICAO nationality (for the MASAK 4/C(3) risky-country block).
254
+ nationality: rawMrzFields.nationality,
207
255
  barcodeValue: scannedDocument.barcodeValue,
208
256
  personalNumber: rawMrzFields.personalNumber || rawMrzFields.optional1,
209
257
  birthDate: mrzUtils.convertMRZDateToISODate(rawMrzFields.birthDate),
@@ -280,6 +328,12 @@ const ResultScreen = () => {
280
328
  if (hologramImage && hologramImage !== '') {
281
329
  await writeTempImage('DOCUMENT_HOLOGRAM_IMAGE.jpg', hologramImage);
282
330
  }
331
+
332
+ // MASAK 4/C(1)(d): the address proof document, when captured.
333
+ const addressProof = appContext.identificationInfo.address?.proofImage;
334
+ if (addressProof && addressProof !== '') {
335
+ await writeTempImage('ADDRESS_PROOF_DOCUMENT.jpg', addressProof);
336
+ }
283
337
  if (livenessDetection) {
284
338
  for (const instruction of livenessDetection.instructions) {
285
339
  if (instruction.photo !== '') {
@@ -419,12 +473,20 @@ const ResultScreen = () => {
419
473
  completedSteps.current.consent = true;
420
474
  }
421
475
  setProgress(30);
476
+ if (!completedSteps.current.address && appContext.identificationInfo.address?.addressText) {
477
+ await runWithRetry(() => submitIdentificationAddress(identificationId, sessionKey));
478
+ completedSteps.current.address = true;
479
+ }
422
480
  if (!alreadyUploaded) {
423
481
  const scannedIdentityDocument = identificationInfo.scannedDocument;
424
482
  if (!completedSteps.current.document && scannedIdentityDocument && scannedIdentityDocument.documentType !== 'UNKNOWN') {
425
483
  await runWithRetry(() => submitIdentificationDocument(identificationId, scannedIdentityDocument, sessionKey));
426
484
  completedSteps.current.document = true;
427
485
  }
486
+ if (!completedSteps.current.passiveAuth && scannedIdentityDocument?.paSodBase64) {
487
+ await runWithRetry(() => submitPassiveAuth(identificationId, sessionKey));
488
+ completedSteps.current.passiveAuth = true;
489
+ }
428
490
  setProgress(40);
429
491
  if (!completedSteps.current.media) {
430
492
  const livenessDetection = identificationInfo.livenessDetection;
@@ -452,6 +514,15 @@ const ResultScreen = () => {
452
514
  setIsSubmitting(false);
453
515
  setProgress(0);
454
516
 
517
+ // MASAK 4/C(1)(e): the server terminated the session on a risk signal.
518
+ // This is terminal, not recoverable — surface it and stop, with no
519
+ // retry affordance (re-submitting can't change the decision).
520
+ if (error && typeof error === 'object' && error.sessionTerminated) {
521
+ appContext.onError?.(t('resultScreen.sessionTerminated'));
522
+ setSubmitFailed(true);
523
+ return;
524
+ }
525
+
455
526
  // Track verification failure
456
527
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
457
528
  trackError('VERIFICATION_SUBMISSION_FAILED', errorMessage, 'result_screen', 'critical', {
@@ -471,7 +542,7 @@ const ResultScreen = () => {
471
542
  appContext.onError?.(t('resultScreen.submissionFailed'));
472
543
  setSubmitFailed(true);
473
544
  }
474
- }, [createIdentification, apiUrl, uploadIdentificationMedia, uploadVerbalConsentVideos, finishIdentification, submitIdentificationDocument, submitIdentificationConsent, t, appContext]);
545
+ }, [createIdentification, apiUrl, uploadIdentificationMedia, uploadVerbalConsentVideos, finishIdentification, submitIdentificationDocument, submitIdentificationConsent, submitIdentificationAddress, submitPassiveAuth, t, appContext]);
475
546
  useEffect(() => {
476
547
  if (appContext.identificationInfo && !appContext.isDemoSession && !appContext.isTestVideoSession && !hasSubmittedRef.current) {
477
548
  hasSubmittedRef.current = true;
@@ -11,7 +11,8 @@ import { debugError } from "../Libs/debug.utils.js";
11
11
  import { eidReader } from "../EIDReader/eidReader.js";
12
12
  import NativeProgressBar from "./NativeProgressBar.js";
13
13
  import { buildDocumentName } from "../Libs/mrzTransliteration.js";
14
- import { normalizeFromMRZInfo } from "../Libs/documentDataNormalizer.js";
14
+ import { normalizeFromMRZInfo, normalizeFromMRZFields } from "../Libs/documentDataNormalizer.js";
15
+ import { compareChipToViz } from "../Libs/chipVizMatch.js";
15
16
  import { useTranslation } from 'react-i18next';
16
17
  import AppContext from "../Contexts/AppContext.js";
17
18
  import StyledButton from "./StyledButton.js";
@@ -34,6 +35,7 @@ const EIDScanner = ({
34
35
  dateOfBirth,
35
36
  dateOfExpiry,
36
37
  documentType,
38
+ vizMrzFields,
37
39
  onScanSuccess,
38
40
  isNFCSupported
39
41
  }) => {
@@ -216,6 +218,8 @@ const EIDScanner = ({
216
218
  } else {
217
219
  setProgressStage(t('eidScannerScreen.completing'));
218
220
  }
221
+ }, {
222
+ passiveAuth: !!appContext.currentWorkflowStep?.data?.passiveAuth
219
223
  });
220
224
  let watchdog;
221
225
  const watchdogPromise = new Promise((_, reject) => {
@@ -243,20 +247,62 @@ const EIDScanner = ({
243
247
  const scanDuration = Date.now() - startTime;
244
248
  const documentData = normalizeFromMRZInfo(passportData.mrz);
245
249
  const builtName = buildDocumentName(documentData.firstName, documentData.lastName, documentData.issuingCountry, passportData.dg11FirstName, passportData.dg11LastName);
250
+ // MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
251
+ // match the data printed on the passport. The printed/VIZ MRZ is the
252
+ // camera scan the EID step itself runs to obtain the BAC/PACE key,
253
+ // passed in as vizMrzFields (scannedDocument.mrzFields is only
254
+ // persisted AFTER the chip read, so it can't be used here). Fall back
255
+ // to a prior standalone OCR step's result (lastDocumentRead) if no
256
+ // vizMrzFields was supplied. Compare on raw MRZ ASCII names (builtName
257
+ // carries them before the display overwrite below) to avoid false
258
+ // mismatches from transliteration. The verdict is advisory on the
259
+ // client; the server re-derives the authoritative one.
260
+ const priorViz = appContext.lastDocumentRead;
261
+ const vizDocument = vizMrzFields ? normalizeFromMRZFields(vizMrzFields) : priorViz?.document;
262
+ const vizName = vizMrzFields ? {
263
+ first: vizDocument.firstName,
264
+ last: vizDocument.lastName
265
+ } : priorViz ? {
266
+ first: priorViz.name.rawFirst,
267
+ last: priorViz.name.rawLast
268
+ } : undefined;
269
+ const chipVizMatch = compareChipToViz(documentData, vizDocument, {
270
+ chipName: {
271
+ first: builtName.rawFirst,
272
+ last: builtName.rawLast
273
+ },
274
+ vizName
275
+ });
246
276
  const documentReadResult = {
247
277
  document: {
248
278
  ...documentData,
249
279
  firstName: builtName.displayFirst,
250
- lastName: builtName.displayLast
280
+ lastName: builtName.displayLast,
281
+ // Address from DG11 where the chip provides it (e.g. Turkish eID);
282
+ // null for most passports. Pre-fills the address step (4/C(1)(d)).
283
+ address: passportData.dg11Address ?? null
251
284
  },
252
285
  name: builtName,
253
286
  face: {
254
287
  data: passportData.faceImage,
255
288
  mimeType: passportData.mimeType
256
- }
289
+ },
290
+ chipVizMatch,
291
+ passiveAuth: passportData.passiveAuth ? {
292
+ integrityStatus: passportData.passiveAuth.integrity?.status ?? null,
293
+ sodBase64: passportData.passiveAuth.sodBase64,
294
+ error: passportData.passiveAuth.error
295
+ } : undefined
257
296
  };
258
297
  appContext.setLastDocumentRead?.(documentReadResult);
259
298
  appContext.onDocumentRead?.(documentReadResult);
299
+
300
+ // MASAK 4/C(1)(b): the chip↔VIZ verdict is computed and sent to the
301
+ // backend (via the document upload), which is the authoritative gate —
302
+ // the verification engine rejects on a non-MATCH when the step enforces
303
+ // it. The client neither blocks nor shows anything to the user; the
304
+ // check is invisible on-device by design.
305
+
260
306
  if (appContext.skipNfcResultScreen && onScanSuccess) {
261
307
  onScanSuccess(getFieldsFromMRZ(passportData.mrz, builtName), passportData.faceImage, passportData.mimeType);
262
308
  } else {
@@ -549,6 +595,16 @@ const EIDScanner = ({
549
595
  }, {
550
596
  label: t('eidScannerScreen.personalNumber'),
551
597
  value: personalNumber
598
+ },
599
+ // MASAK 4/C(1)(d): address from the chip's DG11, when present.
600
+ {
601
+ label: t('eidScannerScreen.address'),
602
+ value: appContext.lastDocumentRead?.document.address ?? ''
603
+ },
604
+ // MASAK 4/C(1)(e): coarse device geolocation, when collected.
605
+ {
606
+ label: t('eidScannerScreen.location'),
607
+ value: appContext.identificationInfo?.consent?.geolocation ?? ''
552
608
  }];
553
609
  return fields.filter(f => f.value && f.value.trim().length > 0).map(f => /*#__PURE__*/_jsx(IDCardField, {
554
610
  label: f.label,
@@ -38,7 +38,8 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
38
38
  IDENTITY_DOCUMENT_EID_SCAN: 'IdentityDocumentEIDScanningScreen',
39
39
  LIVENESS_CHECK: 'LivenessDetectionScreen',
40
40
  VERBAL_CONSENT: 'VerbalConsentScreen',
41
- VIDEO_CALL: 'VideoCallScreen'
41
+ VIDEO_CALL: 'VideoCallScreen',
42
+ ADDRESS_CAPTURE: 'AddressCaptureScreen'
42
43
  },
43
44
  RESULT: 'ResultScreen'
44
45
  };
@@ -104,8 +105,11 @@ const NavigationManager = /*#__PURE__*/forwardRef(({
104
105
  if (nextStep.type === 'VERBAL_CONSENT') {
105
106
  return routes.DYNAMIC_ROUTES.VERBAL_CONSENT;
106
107
  }
108
+ if (nextStep.type === 'ADDRESS_CAPTURE') {
109
+ return routes.DYNAMIC_ROUTES.ADDRESS_CAPTURE;
110
+ }
107
111
  return routes.VERIFICATION_SESSION_CHECK;
108
- }, [appContext, routes.VERIFICATION_SESSION_CHECK, routes.RESULT, routes.DYNAMIC_ROUTES.CONTRACT_ACCEPTANCE, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_SCAN, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_EID_SCAN, routes.DYNAMIC_ROUTES.LIVENESS_CHECK, routes.DYNAMIC_ROUTES.VIDEO_CALL, routes.DYNAMIC_ROUTES.VERBAL_CONSENT]);
112
+ }, [appContext, routes.VERIFICATION_SESSION_CHECK, routes.RESULT, routes.DYNAMIC_ROUTES.CONTRACT_ACCEPTANCE, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_SCAN, routes.DYNAMIC_ROUTES.IDENTITY_DOCUMENT_EID_SCAN, routes.DYNAMIC_ROUTES.LIVENESS_CHECK, routes.DYNAMIC_ROUTES.VIDEO_CALL, routes.DYNAMIC_ROUTES.VERBAL_CONSENT, routes.DYNAMIC_ROUTES.ADDRESS_CAPTURE]);
109
113
  const goToNextRoute = useCallback((outcome = 'success') => {
110
114
  const currentTime = Date.now();
111
115
  const minTimeBetweenNavigation = 1000;
@@ -8,6 +8,8 @@ import { isPACEUnsupportedError } from "./protocol/paceErrors.js";
8
8
  import { DG1File } from "./lds/icao/dg1File.js";
9
9
  import { DG2File } from "./lds/icao/dg2File.js";
10
10
  import { DG11File } from "./lds/icao/dg11File.js";
11
+ import { parseSod } from "./passiveAuth/sodParser.js";
12
+ import { checkIntegrity } from "./passiveAuth/integrity.js";
11
13
  import { Buffer } from 'buffer';
12
14
  import { InputStream } from "./java/inputStream.js";
13
15
  import { debugLog } from "../Libs/debug.utils.js";
@@ -376,7 +378,7 @@ function decodeOID(bytes) {
376
378
  }
377
379
  return components.join('.');
378
380
  }
379
- const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCallback) => {
381
+ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCallback, options) => {
380
382
  let progress = 0;
381
383
  const nfcManagerCardService = new NFCManagerCardService();
382
384
  const passportService = new EIDService(nfcManagerCardService, EIDService.NORMAL_MAX_TRANSCEIVE_LENGTH, EIDService.DEFAULT_MAX_BLOCKSIZE, false, true);
@@ -607,9 +609,10 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
607
609
  progressCallback(progress);
608
610
  }
609
611
 
610
- // Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
612
+ // Read DG11 (Additional Personal Data — printed name + address in UTF-8, optional)
611
613
  let dg11FirstName = null;
612
614
  let dg11LastName = null;
615
+ let dg11Address = null;
613
616
  try {
614
617
  const dg11InputStream = passportService.getInputStream(EIDService.EF_DG11);
615
618
  await dg11InputStream.init();
@@ -617,6 +620,7 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
617
620
  await dg11File.read();
618
621
  dg11FirstName = dg11File.getFirstName();
619
622
  dg11LastName = dg11File.getLastName();
623
+ dg11Address = dg11File.getAddress();
620
624
  } catch {
621
625
  // DG11 is optional — not all chips have it, and access may be denied
622
626
  }
@@ -670,6 +674,49 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
670
674
  imageAsBase64 = converted.base64;
671
675
  mimeType = converted.mimeType;
672
676
  }
677
+
678
+ // Passive Authentication (ICAO 9303-11 §5.1) — opt-in. Read EF.SOD and the
679
+ // exact wire bytes of DG1/DG2 (via fresh streams so the parse flow above is
680
+ // untouched), recompute hashes on-device, and capture the raw SOD for
681
+ // server-side signature + CSCA chaining. Fully wrapped: any failure here
682
+ // leaves the read result intact (PA is reported as unavailable, not fatal).
683
+ let passiveAuth;
684
+ if (options?.passiveAuth) {
685
+ // The PA SOD/DG re-reads must NOT drive the visible progress bar — clear
686
+ // the DG2 byte-progress listener (else progress jumps backward to ~30–60
687
+ // during the small re-reads), and hold the bar at 95 for the PA pass.
688
+ InputStream.clearListeners();
689
+ progress = 95;
690
+ if (progressCallback) {
691
+ progressCallback(progress);
692
+ }
693
+ try {
694
+ const sodStream = passportService.getInputStream(EIDService.EF_SOD);
695
+ await sodStream.init();
696
+ const sodBytes = await sodStream.readAllBytes();
697
+ const rawDg = {};
698
+ for (const [dgNum, fid] of [[1, EIDService.EF_DG1], [2, EIDService.EF_DG2]]) {
699
+ try {
700
+ const s = passportService.getInputStream(fid);
701
+ await s.init();
702
+ rawDg[dgNum] = await s.readAllBytes();
703
+ } catch {
704
+ // DG re-read failed — it simply won't be integrity-checked.
705
+ }
706
+ }
707
+ const sod = parseSod(sodBytes);
708
+ passiveAuth = {
709
+ integrity: checkIntegrity(sod, rawDg),
710
+ sodBase64: Buffer.from(sodBytes).toString('base64')
711
+ };
712
+ } catch (paError) {
713
+ passiveAuth = {
714
+ integrity: null,
715
+ sodBase64: null,
716
+ error: paError instanceof Error ? paError.message : 'passive auth failed'
717
+ };
718
+ }
719
+ }
673
720
  progress = 100;
674
721
  if (progressCallback) {
675
722
  progressCallback(progress);
@@ -680,7 +727,9 @@ const eidReader = async (documentNumber, dateOfBirth, dateOfExpiry, progressCall
680
727
  faceImage: imageAsBase64,
681
728
  mimeType: mimeType,
682
729
  dg11FirstName,
683
- dg11LastName
730
+ dg11LastName,
731
+ dg11Address,
732
+ passiveAuth
684
733
  };
685
734
  } catch (error) {
686
735
  const msg = error instanceof Error ? error.message : String(error);
@@ -136,4 +136,10 @@ export class InputStream {
136
136
  static onRead(listener) {
137
137
  InputStream.listeners.push(listener);
138
138
  }
139
+
140
+ /** Remove all progress listeners (e.g. before post-read passes that must not
141
+ * drive the visible progress bar, like the Passive Auth SOD/DG re-reads). */
142
+ static clearListeners() {
143
+ InputStream.listeners = [];
144
+ }
139
145
  }
@@ -7,21 +7,26 @@ import { DataGroup } from "../dataGroup.js";
7
7
  /**
8
8
  * DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
9
9
  *
10
- * We only extract tag 0x5F0E (Name of Holder), which contains the full name
11
- * exactly as printed on the document, in UTF-8, using the same `<<` primary/
12
- * secondary delimiter as the MRZ (but with native Unicode characters).
10
+ * We extract:
11
+ * - tag 0x5F0E (Name of Holder) — full name as printed, UTF-8, `<<` primary/
12
+ * secondary delimiter like the MRZ but with native Unicode.
13
+ * Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
14
+ * - tag 0x5F42 (Permanent Address) — the holder's address as printed/stored,
15
+ * free-form, components delimited by `<` (used for MASAK 4/C(1)(d) address
16
+ * pre-fill where the chip provides it; e.g. Turkish eID).
13
17
  *
14
- * Example: "ÖZTÜRK<<GÖKHAN MEHMET" lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
15
- *
16
- * The file is optional on the chip — if absent or unreadable we return null
17
- * so the caller can fall back to reverse transliteration.
18
+ * Every field is optional on the chip — absent fields stay null so the caller
19
+ * can fall back (reverse transliteration for the name, manual entry for the
20
+ * address).
18
21
  */
19
22
  export class DG11File extends DataGroup {
20
23
  static DG11_TAG = 0x6b;
21
24
  static TAG_LIST_TAG = 0x5c;
22
25
  static NAME_OF_HOLDER_TAG = 0x5f0e;
26
+ static PERMANENT_ADDRESS_TAG = 0x5f42;
23
27
  firstName = null;
24
28
  lastName = null;
29
+ address = null;
25
30
  constructor(is) {
26
31
  super(DG11File.DG11_TAG, is);
27
32
  }
@@ -35,26 +40,61 @@ export class DG11File extends DataGroup {
35
40
  getLastName() {
36
41
  return this.lastName;
37
42
  }
43
+
44
+ /** Returns the permanent address from DG11, or null if not present. */
45
+ getAddress() {
46
+ return this.address;
47
+ }
38
48
  async read() {
39
49
  await super.readObject();
40
50
  }
41
51
  async readContent(inputStream) {
42
52
  const tlvIn = inputStream instanceof TLVInputStream ? inputStream : new TLVInputStream(inputStream);
43
53
 
44
- // Skip the tag list (0x5C) we only care about 0x5F0E
45
- // The tag list tells us which optional tags are present; we probe directly.
46
- try {
47
- await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
48
- } catch {
49
- // Tag not present in this chip's DG11
50
- return;
54
+ // Iterate the DG11 TLV structure once, capturing the tags we care about and
55
+ // skipping the rest (incl. the 0x5C tag list). A single forward scan handles
56
+ // any field ordering and lets us read name AND address in one pass.
57
+ while (true) {
58
+ let tag;
59
+ try {
60
+ tag = await tlvIn.readTag();
61
+ } catch {
62
+ break; // end of data
63
+ }
64
+ if (tag < 0) break;
65
+ let length;
66
+ try {
67
+ length = await tlvIn.readLength();
68
+ } catch {
69
+ break;
70
+ }
71
+ if ((tag === DG11File.NAME_OF_HOLDER_TAG || tag === DG11File.PERMANENT_ADDRESS_TAG) && length > 0) {
72
+ // readValue keeps the TLV stream's value-byte accounting in sync so the
73
+ // next readTag starts cleanly — important now that we read multiple tags.
74
+ const value = await tlvIn.readValue();
75
+ const text = Buffer.from(Uint8Array.from(value)).toString('utf8');
76
+ if (tag === DG11File.NAME_OF_HOLDER_TAG) {
77
+ this.parseName(text.trim());
78
+ } else {
79
+ this.address = this.parseAddress(text);
80
+ }
81
+ } else if (length > 0) {
82
+ // Tag we don't use (incl. the 0x5C tag list) — skip its value.
83
+ await tlvIn.skip(length);
84
+ }
51
85
  }
52
- const length = await tlvIn.readLength();
53
- if (length <= 0) return;
54
- const bytes = new Uint8Array(length);
55
- await inputStream.readBytesWithOffset(bytes, 0, length);
56
- const nameString = Buffer.from(bytes).toString('utf8').trim();
57
- this.parseName(nameString);
86
+ }
87
+
88
+ /**
89
+ * Normalize a DG11 permanent-address value. Components are delimited by `<`
90
+ * (single = field separator, double = stronger break); convert to a single
91
+ * comma-separated line, dropping filler.
92
+ */
93
+ parseAddress(raw) {
94
+ const cleaned = raw.replace(/<{2,}/g, '<') // collapse multi-filler to one separator
95
+ .split('<').map(part => part.trim()).filter(part => part.length > 0);
96
+ if (cleaned.length === 0) return null;
97
+ return cleaned.join(', ');
58
98
  }
59
99
  writeContent() {
60
100
  // Write not needed for reading
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Minimal DER (ASN.1) reader for Passive Authentication parsing (ICAO 9303-11
5
+ * §5.1). Purpose-built and dependency-free so the EF.SOD / LDSSecurityObject
6
+ * parse is fully unit-testable offline. Only the constructs PA needs are
7
+ * supported: SEQUENCE, SET, INTEGER, OCTET STRING, OID, and tagged wrappers.
8
+ */
9
+
10
+ /** Read the TLV node starting at `offset` in `buf`. */
11
+ export function readNode(buf, offset) {
12
+ const start = offset;
13
+ let i = offset;
14
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (tag)');
15
+ const tag = buf[i++];
16
+ // High-tag-number form (tag & 0x1f === 0x1f) — consume continuation bytes.
17
+ let tagNumber = tag & 0x1f;
18
+ if (tagNumber === 0x1f) {
19
+ tagNumber = 0;
20
+ let b;
21
+ do {
22
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (tag#)');
23
+ b = buf[i++];
24
+ tagNumber = tagNumber << 7 | b & 0x7f;
25
+ } while ((b & 0x80) === 0x80);
26
+ }
27
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (length)');
28
+ let length = buf[i++];
29
+ if (length & 0x80) {
30
+ const numBytes = length & 0x7f;
31
+ if (numBytes === 0) throw new Error('DER: indefinite length not supported');
32
+ length = 0;
33
+ for (let n = 0; n < numBytes; n++) {
34
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (len bytes)');
35
+ length = length << 8 | buf[i++];
36
+ }
37
+ }
38
+ const contentStart = i;
39
+ const end = contentStart + length;
40
+ if (end > buf.length) throw new Error('DER: content exceeds buffer');
41
+ return {
42
+ tag,
43
+ tagNumber,
44
+ constructed: (tag & 0x20) === 0x20,
45
+ content: buf.subarray(contentStart, end),
46
+ start,
47
+ end
48
+ };
49
+ }
50
+
51
+ /** Read all immediate child TLVs of a constructed node's content. */
52
+ export function readChildren(content) {
53
+ const out = [];
54
+ let offset = 0;
55
+ while (offset < content.length) {
56
+ const node = readNode(content, offset);
57
+ out.push(node);
58
+ offset = node.end;
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** Decode an OID node's content to dotted-decimal string. */
64
+ export function decodeOid(content) {
65
+ if (content.length === 0) return '';
66
+ const parts = [];
67
+ const first = content[0];
68
+ parts.push(Math.floor(first / 40), first % 40);
69
+ let value = 0;
70
+ for (let i = 1; i < content.length; i++) {
71
+ const b = content[i];
72
+ value = value << 7 | b & 0x7f;
73
+ if ((b & 0x80) === 0) {
74
+ parts.push(value);
75
+ value = 0;
76
+ }
77
+ }
78
+ return parts.join('.');
79
+ }
80
+
81
+ /** Decode an INTEGER node's content to a JS number (small values only). */
82
+ export function decodeInteger(content) {
83
+ let value = 0;
84
+ for (const b of content) value = value * 256 + b;
85
+ return value;
86
+ }
87
+
88
+ /** Lowercase hex string for a byte array. */
89
+ export function toHex(bytes) {
90
+ let s = '';
91
+ for (const b of bytes) s += b.toString(16).padStart(2, '0');
92
+ return s;
93
+ }