@trustchex/react-native-sdk 1.500.1 → 1.501.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 (100) 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/mrz.utils.js +24 -1
  24. package/lib/module/Shared/Libs/promise.utils.js +6 -0
  25. package/lib/module/Shared/Services/DataUploadService.js +18 -1
  26. package/lib/module/Translation/Resources/en.js +16 -0
  27. package/lib/module/Translation/Resources/tr.js +16 -0
  28. package/lib/module/Trustchex.js +4 -0
  29. package/lib/module/version.js +1 -1
  30. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
  31. package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
  32. package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
  34. package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
  36. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
  37. package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
  38. package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
  39. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
  40. package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
  41. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
  42. package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
  43. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
  44. package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
  45. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
  46. package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
  47. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
  48. package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
  49. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
  50. package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
  51. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
  52. package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
  53. package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
  54. package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
  55. package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
  56. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
  57. package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
  58. package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
  59. package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
  60. package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
  61. package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
  62. package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
  63. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
  64. package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
  65. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
  66. package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
  67. package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
  68. package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
  69. package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
  70. package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
  71. package/lib/typescript/src/Trustchex.d.ts.map +1 -1
  72. package/lib/typescript/src/version.d.ts +1 -1
  73. package/package.json +1 -1
  74. package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
  75. package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
  76. package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
  77. package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
  78. package/src/Screens/Static/ResultScreen.tsx +101 -6
  79. package/src/Shared/Components/EIDScanner.tsx +69 -2
  80. package/src/Shared/Components/NavigationManager.tsx +6 -0
  81. package/src/Shared/EIDReader/eidReader.ts +75 -2
  82. package/src/Shared/EIDReader/java/inputStream.ts +6 -0
  83. package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
  84. package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
  85. package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
  86. package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
  87. package/src/Shared/Libs/chipVizMatch.ts +149 -0
  88. package/src/Shared/Libs/country-allow.ts +17 -0
  89. package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
  90. package/src/Shared/Libs/geolocation.utils.ts +87 -0
  91. package/src/Shared/Libs/mrz.utils.ts +27 -1
  92. package/src/Shared/Libs/promise.utils.ts +11 -0
  93. package/src/Shared/Services/DataUploadService.ts +23 -0
  94. package/src/Shared/Types/documentData.ts +8 -0
  95. package/src/Shared/Types/documentReadResult.ts +20 -0
  96. package/src/Shared/Types/identificationInfo.ts +97 -2
  97. package/src/Translation/Resources/en.ts +21 -0
  98. package/src/Translation/Resources/tr.ts +21 -0
  99. package/src/Trustchex.tsx +5 -0
  100. package/src/version.ts +1 -1
@@ -20,7 +20,11 @@ import NativeProgressBar from './NativeProgressBar';
20
20
  import type { MRZFields } from '../Types/mrzFields';
21
21
  import { buildDocumentName } from '../Libs/mrzTransliteration';
22
22
  import type { DocumentName } from '../Types/documentReadResult';
23
- import { normalizeFromMRZInfo } from '../Libs/documentDataNormalizer';
23
+ import {
24
+ normalizeFromMRZInfo,
25
+ normalizeFromMRZFields,
26
+ } from '../Libs/documentDataNormalizer';
27
+ import { compareChipToViz } from '../Libs/chipVizMatch';
24
28
  import { useTranslation } from 'react-i18next';
25
29
  import AppContext from '../Contexts/AppContext';
26
30
  import StyledButton from './StyledButton';
@@ -47,6 +51,12 @@ interface eIDScannerProps {
47
51
  dateOfBirth: string;
48
52
  dateOfExpiry: string;
49
53
  documentType?: string;
54
+ /**
55
+ * The printed/VIZ MRZ scanned by the EID step's own camera, used to verify
56
+ * the chip data matches the printed page (MASAK 4/C(1)(b)). Supplied directly
57
+ * because scannedDocument.mrzFields is only persisted after the chip read.
58
+ */
59
+ vizMrzFields?: MRZFields;
50
60
  onScanSuccess?: (
51
61
  mrzInfo: MRZFields,
52
62
  faceImage: string,
@@ -60,6 +70,7 @@ const EIDScanner = ({
60
70
  dateOfBirth,
61
71
  dateOfExpiry,
62
72
  documentType,
73
+ vizMrzFields,
63
74
  onScanSuccess,
64
75
  isNFCSupported,
65
76
  }: eIDScannerProps) => {
@@ -296,7 +307,8 @@ const EIDScanner = ({
296
307
  } else {
297
308
  setProgressStage(t('eidScannerScreen.completing'));
298
309
  }
299
- }
310
+ },
311
+ { passiveAuth: !!appContext.currentWorkflowStep?.data?.passiveAuth }
300
312
  );
301
313
  let watchdog: ReturnType<typeof setTimeout> | undefined;
302
314
  const watchdogPromise = new Promise<never>((_, reject) => {
@@ -332,21 +344,63 @@ const EIDScanner = ({
332
344
  passportData.dg11FirstName,
333
345
  passportData.dg11LastName
334
346
  );
347
+ // MASAK Tebliğ No. 32 Article 4/C(1)(b): the chip identity data must
348
+ // match the data printed on the passport. The printed/VIZ MRZ is the
349
+ // camera scan the EID step itself runs to obtain the BAC/PACE key,
350
+ // passed in as vizMrzFields (scannedDocument.mrzFields is only
351
+ // persisted AFTER the chip read, so it can't be used here). Fall back
352
+ // to a prior standalone OCR step's result (lastDocumentRead) if no
353
+ // vizMrzFields was supplied. Compare on raw MRZ ASCII names (builtName
354
+ // carries them before the display overwrite below) to avoid false
355
+ // mismatches from transliteration. The verdict is advisory on the
356
+ // client; the server re-derives the authoritative one.
357
+ const priorViz = appContext.lastDocumentRead;
358
+ const vizDocument = vizMrzFields
359
+ ? normalizeFromMRZFields(vizMrzFields)
360
+ : priorViz?.document;
361
+ const vizName = vizMrzFields
362
+ ? { first: vizDocument!.firstName, last: vizDocument!.lastName }
363
+ : priorViz
364
+ ? { first: priorViz.name.rawFirst, last: priorViz.name.rawLast }
365
+ : undefined;
366
+ const chipVizMatch = compareChipToViz(documentData, vizDocument, {
367
+ chipName: { first: builtName.rawFirst, last: builtName.rawLast },
368
+ vizName,
369
+ });
370
+
335
371
  const documentReadResult = {
336
372
  document: {
337
373
  ...documentData,
338
374
  firstName: builtName.displayFirst,
339
375
  lastName: builtName.displayLast,
376
+ // Address from DG11 where the chip provides it (e.g. Turkish eID);
377
+ // null for most passports. Pre-fills the address step (4/C(1)(d)).
378
+ address: passportData.dg11Address ?? null,
340
379
  },
341
380
  name: builtName,
342
381
  face: {
343
382
  data: passportData.faceImage,
344
383
  mimeType: passportData.mimeType,
345
384
  },
385
+ chipVizMatch,
386
+ passiveAuth: passportData.passiveAuth
387
+ ? {
388
+ integrityStatus:
389
+ passportData.passiveAuth.integrity?.status ?? null,
390
+ sodBase64: passportData.passiveAuth.sodBase64,
391
+ error: passportData.passiveAuth.error,
392
+ }
393
+ : undefined,
346
394
  };
347
395
  appContext.setLastDocumentRead?.(documentReadResult);
348
396
  appContext.onDocumentRead?.(documentReadResult);
349
397
 
398
+ // MASAK 4/C(1)(b): the chip↔VIZ verdict is computed and sent to the
399
+ // backend (via the document upload), which is the authoritative gate —
400
+ // the verification engine rejects on a non-MATCH when the step enforces
401
+ // it. The client neither blocks nor shows anything to the user; the
402
+ // check is invisible on-device by design.
403
+
350
404
  if (appContext.skipNfcResultScreen && onScanSuccess) {
351
405
  onScanSuccess(
352
406
  getFieldsFromMRZ(passportData.mrz, builtName),
@@ -775,6 +829,19 @@ const EIDScanner = ({
775
829
  label: t('eidScannerScreen.personalNumber'),
776
830
  value: personalNumber,
777
831
  },
832
+ // MASAK 4/C(1)(d): address from the chip's DG11, when present.
833
+ {
834
+ label: t('eidScannerScreen.address'),
835
+ value:
836
+ appContext.lastDocumentRead?.document.address ?? '',
837
+ },
838
+ // MASAK 4/C(1)(e): coarse device geolocation, when collected.
839
+ {
840
+ label: t('eidScannerScreen.location'),
841
+ value:
842
+ appContext.identificationInfo?.consent
843
+ ?.geolocation ?? '',
844
+ },
778
845
  ];
779
846
 
780
847
  return fields
@@ -53,6 +53,7 @@ const NavigationManager = forwardRef(
53
53
  LIVENESS_CHECK: 'LivenessDetectionScreen',
54
54
  VERBAL_CONSENT: 'VerbalConsentScreen',
55
55
  VIDEO_CALL: 'VideoCallScreen',
56
+ ADDRESS_CAPTURE: 'AddressCaptureScreen',
56
57
  },
57
58
  RESULT: 'ResultScreen',
58
59
  };
@@ -146,6 +147,10 @@ const NavigationManager = forwardRef(
146
147
  return routes.DYNAMIC_ROUTES.VERBAL_CONSENT;
147
148
  }
148
149
 
150
+ if (nextStep.type === 'ADDRESS_CAPTURE') {
151
+ return routes.DYNAMIC_ROUTES.ADDRESS_CAPTURE;
152
+ }
153
+
149
154
  return routes.VERIFICATION_SESSION_CHECK;
150
155
  },
151
156
  [
@@ -158,6 +163,7 @@ const NavigationManager = forwardRef(
158
163
  routes.DYNAMIC_ROUTES.LIVENESS_CHECK,
159
164
  routes.DYNAMIC_ROUTES.VIDEO_CALL,
160
165
  routes.DYNAMIC_ROUTES.VERBAL_CONSENT,
166
+ routes.DYNAMIC_ROUTES.ADDRESS_CAPTURE,
161
167
  ]
162
168
  );
163
169
 
@@ -6,6 +6,8 @@ import { isPACEUnsupportedError } from './protocol/paceErrors';
6
6
  import { DG1File } from './lds/icao/dg1File';
7
7
  import { DG2File } from './lds/icao/dg2File';
8
8
  import { DG11File } from './lds/icao/dg11File';
9
+ import { parseSod } from './passiveAuth/sodParser';
10
+ import { checkIntegrity } from './passiveAuth/integrity';
9
11
  import { FaceImageInfo } from './lds/iso19794/faceImageInfo';
10
12
  import { Buffer } from 'buffer';
11
13
  import { MRZInfo } from './lds/icao/mrzInfo';
@@ -407,7 +409,8 @@ const eidReader = async (
407
409
  documentNumber: string,
408
410
  dateOfBirth: string,
409
411
  dateOfExpiry: string,
410
- progressCallback?: (progress: number) => void
412
+ progressCallback?: (progress: number) => void,
413
+ options?: { passiveAuth?: boolean }
411
414
  ): Promise<
412
415
  | {
413
416
  mrz?: MRZInfo;
@@ -415,6 +418,17 @@ const eidReader = async (
415
418
  mimeType: string;
416
419
  dg11FirstName: string | null;
417
420
  dg11LastName: string | null;
421
+ dg11Address: string | null;
422
+ /**
423
+ * Passive Authentication result (ICAO 9303-11 §5.1), present only when
424
+ * options.passiveAuth was set. `integrity` is the on-device DG-hash check;
425
+ * `sodBase64` is the raw EF.SOD for server-side signature + CSCA chaining.
426
+ */
427
+ passiveAuth?: {
428
+ integrity: import('./passiveAuth/integrity').IntegrityReport | null;
429
+ sodBase64: string | null;
430
+ error?: string;
431
+ };
418
432
  }
419
433
  | undefined
420
434
  > => {
@@ -704,9 +718,10 @@ const eidReader = async (
704
718
  progressCallback(progress);
705
719
  }
706
720
 
707
- // Read DG11 (Additional Personal Data — printed name in UTF-8, optional)
721
+ // Read DG11 (Additional Personal Data — printed name + address in UTF-8, optional)
708
722
  let dg11FirstName: string | null = null;
709
723
  let dg11LastName: string | null = null;
724
+ let dg11Address: string | null = null;
710
725
  try {
711
726
  const dg11InputStream = passportService.getInputStream(
712
727
  EIDService.EF_DG11
@@ -716,6 +731,7 @@ const eidReader = async (
716
731
  await dg11File.read();
717
732
  dg11FirstName = dg11File.getFirstName();
718
733
  dg11LastName = dg11File.getLastName();
734
+ dg11Address = dg11File.getAddress();
719
735
  } catch {
720
736
  // DG11 is optional — not all chips have it, and access may be denied
721
737
  }
@@ -771,6 +787,61 @@ const eidReader = async (
771
787
  mimeType = converted.mimeType;
772
788
  }
773
789
 
790
+ // Passive Authentication (ICAO 9303-11 §5.1) — opt-in. Read EF.SOD and the
791
+ // exact wire bytes of DG1/DG2 (via fresh streams so the parse flow above is
792
+ // untouched), recompute hashes on-device, and capture the raw SOD for
793
+ // server-side signature + CSCA chaining. Fully wrapped: any failure here
794
+ // leaves the read result intact (PA is reported as unavailable, not fatal).
795
+ let passiveAuth:
796
+ | {
797
+ integrity: ReturnType<typeof checkIntegrity> | null;
798
+ sodBase64: string | null;
799
+ error?: string;
800
+ }
801
+ | undefined;
802
+ if (options?.passiveAuth) {
803
+ // The PA SOD/DG re-reads must NOT drive the visible progress bar — clear
804
+ // the DG2 byte-progress listener (else progress jumps backward to ~30–60
805
+ // during the small re-reads), and hold the bar at 95 for the PA pass.
806
+ InputStream.clearListeners();
807
+ progress = 95;
808
+ if (progressCallback) {
809
+ progressCallback(progress);
810
+ }
811
+ try {
812
+ const sodStream = passportService.getInputStream(EIDService.EF_SOD);
813
+ await sodStream.init();
814
+ const sodBytes = await sodStream.readAllBytes();
815
+
816
+ const rawDg: Record<number, Uint8Array> = {};
817
+ for (const [dgNum, fid] of [
818
+ [1, EIDService.EF_DG1],
819
+ [2, EIDService.EF_DG2],
820
+ ] as const) {
821
+ try {
822
+ const s = passportService.getInputStream(fid);
823
+ await s.init();
824
+ rawDg[dgNum] = await s.readAllBytes();
825
+ } catch {
826
+ // DG re-read failed — it simply won't be integrity-checked.
827
+ }
828
+ }
829
+
830
+ const sod = parseSod(sodBytes);
831
+ passiveAuth = {
832
+ integrity: checkIntegrity(sod, rawDg),
833
+ sodBase64: Buffer.from(sodBytes).toString('base64'),
834
+ };
835
+ } catch (paError) {
836
+ passiveAuth = {
837
+ integrity: null,
838
+ sodBase64: null,
839
+ error:
840
+ paError instanceof Error ? paError.message : 'passive auth failed',
841
+ };
842
+ }
843
+ }
844
+
774
845
  progress = 100;
775
846
  if (progressCallback) {
776
847
  progressCallback(progress);
@@ -783,6 +854,8 @@ const eidReader = async (
783
854
  mimeType: mimeType,
784
855
  dg11FirstName,
785
856
  dg11LastName,
857
+ dg11Address,
858
+ passiveAuth,
786
859
  };
787
860
  } catch (error) {
788
861
  const msg = error instanceof Error ? error.message : String(error);
@@ -183,4 +183,10 @@ export abstract class InputStream {
183
183
  ) {
184
184
  InputStream.listeners.push(listener);
185
185
  }
186
+
187
+ /** Remove all progress listeners (e.g. before post-read passes that must not
188
+ * drive the visible progress bar, like the Passive Auth SOD/DG re-reads). */
189
+ static clearListeners() {
190
+ InputStream.listeners = [];
191
+ }
186
192
  }
@@ -6,22 +6,27 @@ import { DataGroup } from '../dataGroup';
6
6
  /**
7
7
  * DG11 — Additional Personal Data (ICAO 9303-10 §4.6.6).
8
8
  *
9
- * We only extract tag 0x5F0E (Name of Holder), which contains the full name
10
- * exactly as printed on the document, in UTF-8, using the same `<<` primary/
11
- * secondary delimiter as the MRZ (but with native Unicode characters).
9
+ * We extract:
10
+ * - tag 0x5F0E (Name of Holder) — full name as printed, UTF-8, `<<` primary/
11
+ * secondary delimiter like the MRZ but with native Unicode.
12
+ * Example: "ÖZTÜRK<<GÖKHAN MEHMET" → lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
13
+ * - tag 0x5F42 (Permanent Address) — the holder's address as printed/stored,
14
+ * free-form, components delimited by `<` (used for MASAK 4/C(1)(d) address
15
+ * pre-fill where the chip provides it; e.g. Turkish eID).
12
16
  *
13
- * Example: "ÖZTÜRK<<GÖKHAN MEHMET" lastName="ÖZTÜRK", firstName="GÖKHAN MEHMET"
14
- *
15
- * The file is optional on the chip — if absent or unreadable we return null
16
- * so the caller can fall back to reverse transliteration.
17
+ * Every field is optional on the chip — absent fields stay null so the caller
18
+ * can fall back (reverse transliteration for the name, manual entry for the
19
+ * address).
17
20
  */
18
21
  export class DG11File extends DataGroup {
19
22
  private static readonly DG11_TAG = 0x6b;
20
23
  private static readonly TAG_LIST_TAG = 0x5c;
21
24
  private static readonly NAME_OF_HOLDER_TAG = 0x5f0e;
25
+ private static readonly PERMANENT_ADDRESS_TAG = 0x5f42;
22
26
 
23
27
  private firstName: string | null = null;
24
28
  private lastName: string | null = null;
29
+ private address: string | null = null;
25
30
 
26
31
  constructor(is?: InputStream) {
27
32
  super(DG11File.DG11_TAG, is);
@@ -37,6 +42,11 @@ export class DG11File extends DataGroup {
37
42
  return this.lastName;
38
43
  }
39
44
 
45
+ /** Returns the permanent address from DG11, or null if not present. */
46
+ getAddress(): string | null {
47
+ return this.address;
48
+ }
49
+
40
50
  async read(): Promise<void> {
41
51
  await super.readObject();
42
52
  }
@@ -47,23 +57,59 @@ export class DG11File extends DataGroup {
47
57
  ? inputStream
48
58
  : new TLVInputStream(inputStream);
49
59
 
50
- // Skip the tag list (0x5C) we only care about 0x5F0E
51
- // The tag list tells us which optional tags are present; we probe directly.
52
- try {
53
- await tlvIn.skipToTag(DG11File.NAME_OF_HOLDER_TAG);
54
- } catch {
55
- // Tag not present in this chip's DG11
56
- return;
60
+ // Iterate the DG11 TLV structure once, capturing the tags we care about and
61
+ // skipping the rest (incl. the 0x5C tag list). A single forward scan handles
62
+ // any field ordering and lets us read name AND address in one pass.
63
+ while (true) {
64
+ let tag: number;
65
+ try {
66
+ tag = await tlvIn.readTag();
67
+ } catch {
68
+ break; // end of data
69
+ }
70
+ if (tag < 0) break;
71
+
72
+ let length: number;
73
+ try {
74
+ length = await tlvIn.readLength();
75
+ } catch {
76
+ break;
77
+ }
78
+
79
+ if (
80
+ (tag === DG11File.NAME_OF_HOLDER_TAG ||
81
+ tag === DG11File.PERMANENT_ADDRESS_TAG) &&
82
+ length > 0
83
+ ) {
84
+ // readValue keeps the TLV stream's value-byte accounting in sync so the
85
+ // next readTag starts cleanly — important now that we read multiple tags.
86
+ const value = await tlvIn.readValue();
87
+ const text = Buffer.from(Uint8Array.from(value)).toString('utf8');
88
+ if (tag === DG11File.NAME_OF_HOLDER_TAG) {
89
+ this.parseName(text.trim());
90
+ } else {
91
+ this.address = this.parseAddress(text);
92
+ }
93
+ } else if (length > 0) {
94
+ // Tag we don't use (incl. the 0x5C tag list) — skip its value.
95
+ await tlvIn.skip(length);
96
+ }
57
97
  }
98
+ }
58
99
 
59
- const length = await tlvIn.readLength();
60
- if (length <= 0) return;
61
-
62
- const bytes = new Uint8Array(length);
63
- await inputStream.readBytesWithOffset(bytes, 0, length);
64
-
65
- const nameString = Buffer.from(bytes).toString('utf8').trim();
66
- this.parseName(nameString);
100
+ /**
101
+ * Normalize a DG11 permanent-address value. Components are delimited by `<`
102
+ * (single = field separator, double = stronger break); convert to a single
103
+ * comma-separated line, dropping filler.
104
+ */
105
+ private parseAddress(raw: string): string | null {
106
+ const cleaned = raw
107
+ .replace(/<{2,}/g, '<') // collapse multi-filler to one separator
108
+ .split('<')
109
+ .map((part) => part.trim())
110
+ .filter((part) => part.length > 0);
111
+ if (cleaned.length === 0) return null;
112
+ return cleaned.join(', ');
67
113
  }
68
114
 
69
115
  protected writeContent(): void {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Minimal DER (ASN.1) reader for Passive Authentication parsing (ICAO 9303-11
3
+ * §5.1). Purpose-built and dependency-free so the EF.SOD / LDSSecurityObject
4
+ * parse is fully unit-testable offline. Only the constructs PA needs are
5
+ * supported: SEQUENCE, SET, INTEGER, OCTET STRING, OID, and tagged wrappers.
6
+ */
7
+
8
+ export interface DerNode {
9
+ /** Raw first tag byte (class + constructed bit + number for low tags). */
10
+ tag: number;
11
+ /** Tag number with class/constructed bits masked off (low-tag form only). */
12
+ tagNumber: number;
13
+ /** True if the constructed bit (0x20) is set. */
14
+ constructed: boolean;
15
+ /** Value bytes (content octets), excluding tag and length. */
16
+ content: Uint8Array;
17
+ /** Offset of this node's first tag byte within the parent buffer. */
18
+ start: number;
19
+ /** Offset just past this node's content (start of the next sibling). */
20
+ end: number;
21
+ }
22
+
23
+ /** Read the TLV node starting at `offset` in `buf`. */
24
+ export function readNode(buf: Uint8Array, offset: number): DerNode {
25
+ const start = offset;
26
+ let i = offset;
27
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (tag)');
28
+
29
+ const tag = buf[i++];
30
+ // High-tag-number form (tag & 0x1f === 0x1f) — consume continuation bytes.
31
+ let tagNumber = tag & 0x1f;
32
+ if (tagNumber === 0x1f) {
33
+ tagNumber = 0;
34
+ let b: number;
35
+ do {
36
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (tag#)');
37
+ b = buf[i++];
38
+ tagNumber = (tagNumber << 7) | (b & 0x7f);
39
+ } while ((b & 0x80) === 0x80);
40
+ }
41
+
42
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (length)');
43
+ let length = buf[i++];
44
+ if (length & 0x80) {
45
+ const numBytes = length & 0x7f;
46
+ if (numBytes === 0) throw new Error('DER: indefinite length not supported');
47
+ length = 0;
48
+ for (let n = 0; n < numBytes; n++) {
49
+ if (i >= buf.length) throw new Error('DER: unexpected end of input (len bytes)');
50
+ length = (length << 8) | buf[i++];
51
+ }
52
+ }
53
+
54
+ const contentStart = i;
55
+ const end = contentStart + length;
56
+ if (end > buf.length) throw new Error('DER: content exceeds buffer');
57
+
58
+ return {
59
+ tag,
60
+ tagNumber,
61
+ constructed: (tag & 0x20) === 0x20,
62
+ content: buf.subarray(contentStart, end),
63
+ start,
64
+ end,
65
+ };
66
+ }
67
+
68
+ /** Read all immediate child TLVs of a constructed node's content. */
69
+ export function readChildren(content: Uint8Array): DerNode[] {
70
+ const out: DerNode[] = [];
71
+ let offset = 0;
72
+ while (offset < content.length) {
73
+ const node = readNode(content, offset);
74
+ out.push(node);
75
+ offset = node.end;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /** Decode an OID node's content to dotted-decimal string. */
81
+ export function decodeOid(content: Uint8Array): string {
82
+ if (content.length === 0) return '';
83
+ const parts: number[] = [];
84
+ const first = content[0];
85
+ parts.push(Math.floor(first / 40), first % 40);
86
+ let value = 0;
87
+ for (let i = 1; i < content.length; i++) {
88
+ const b = content[i];
89
+ value = (value << 7) | (b & 0x7f);
90
+ if ((b & 0x80) === 0) {
91
+ parts.push(value);
92
+ value = 0;
93
+ }
94
+ }
95
+ return parts.join('.');
96
+ }
97
+
98
+ /** Decode an INTEGER node's content to a JS number (small values only). */
99
+ export function decodeInteger(content: Uint8Array): number {
100
+ let value = 0;
101
+ for (const b of content) value = value * 256 + b;
102
+ return value;
103
+ }
104
+
105
+ /** Lowercase hex string for a byte array. */
106
+ export function toHex(bytes: Uint8Array): string {
107
+ let s = '';
108
+ for (const b of bytes) s += b.toString(16).padStart(2, '0');
109
+ return s;
110
+ }
@@ -0,0 +1,97 @@
1
+ import { sha1 } from '@noble/hashes/sha1';
2
+ import { sha256, sha224 } from '@noble/hashes/sha256';
3
+ import { sha512, sha384 } from '@noble/hashes/sha512';
4
+ import { toHex } from './der';
5
+ import type { SodSecurityObject } from './sodParser';
6
+
7
+ /**
8
+ * On-device Passive Authentication integrity check (ICAO 9303-11 §5.1, step 3).
9
+ * Recompute the hash of each raw data-group the chip returned and compare it to
10
+ * the hash the issuer signed into EF.SOD. A mismatch means the data was altered
11
+ * after signing → TAMPERED. This runs offline, with no trust store; SOD
12
+ * signature + CSCA chaining is verified server-side.
13
+ */
14
+
15
+ export type IntegrityStatus = 'MATCH' | 'TAMPERED' | 'INCOMPLETE';
16
+
17
+ export interface DgIntegrityResult {
18
+ dataGroup: number;
19
+ expectedHash: string;
20
+ actualHash: string;
21
+ matched: boolean;
22
+ }
23
+
24
+ export interface IntegrityReport {
25
+ status: IntegrityStatus;
26
+ digestAlgorithm: string;
27
+ /** Per-DG comparison for the data groups we actually read. */
28
+ results: DgIntegrityResult[];
29
+ /** DG numbers present in the SOD that we did not read (cannot verify). */
30
+ unverified: number[];
31
+ }
32
+
33
+ function digest(algorithm: string, data: Uint8Array): Uint8Array {
34
+ switch (algorithm) {
35
+ case 'SHA-1':
36
+ return sha1(data);
37
+ case 'SHA-224':
38
+ return sha224(data);
39
+ case 'SHA-256':
40
+ return sha256(data);
41
+ case 'SHA-384':
42
+ return sha384(data);
43
+ case 'SHA-512':
44
+ return sha512(data);
45
+ default:
46
+ throw new Error(`Unsupported digest algorithm: ${algorithm}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Compare the raw bytes of each read data group against the SOD's hash list.
52
+ *
53
+ * @param sod parsed LDSSecurityObject (digest alg + per-DG hashes)
54
+ * @param rawDataGroups map of DG number → exact wire bytes read from the chip
55
+ */
56
+ export function checkIntegrity(
57
+ sod: SodSecurityObject,
58
+ rawDataGroups: Record<number, Uint8Array>
59
+ ): IntegrityReport {
60
+ const results: DgIntegrityResult[] = [];
61
+
62
+ for (const [dgStr, bytes] of Object.entries(rawDataGroups)) {
63
+ const dg = Number(dgStr);
64
+ const expectedHash = sod.dataGroupHashes[dg];
65
+ if (!expectedHash) continue; // DG not listed in SOD — nothing to compare
66
+ const actualHash = toHex(digest(sod.digestAlgorithm, bytes));
67
+ results.push({
68
+ dataGroup: dg,
69
+ expectedHash: expectedHash.toLowerCase(),
70
+ actualHash,
71
+ matched: actualHash === expectedHash.toLowerCase(),
72
+ });
73
+ }
74
+
75
+ // DG hashes present in the SOD that we did not read → cannot verify those.
76
+ const readDgs = new Set(Object.keys(rawDataGroups).map(Number));
77
+ const unverified = Object.keys(sod.dataGroupHashes)
78
+ .map(Number)
79
+ .filter((dg) => !readDgs.has(dg))
80
+ .sort((a, b) => a - b);
81
+
82
+ let status: IntegrityStatus;
83
+ if (results.some((r) => !r.matched)) {
84
+ status = 'TAMPERED';
85
+ } else if (results.length === 0) {
86
+ status = 'INCOMPLETE';
87
+ } else {
88
+ status = 'MATCH';
89
+ }
90
+
91
+ return {
92
+ status,
93
+ digestAlgorithm: sod.digestAlgorithm,
94
+ results,
95
+ unverified,
96
+ };
97
+ }