@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.
- package/android/src/main/java/com/trustchex/reactnativesdk/GeolocationModule.kt +159 -0
- package/android/src/main/java/com/trustchex/reactnativesdk/TrustchexSDKPackage.kt +12 -0
- package/ios/Permission/CoarseLocationProvider.swift +111 -0
- package/ios/Permission/PermissionModule.m +4 -0
- package/ios/Permission/PermissionModule.swift +21 -1
- package/lib/module/Screens/Dynamic/AddressCaptureScreen.js +230 -0
- package/lib/module/Screens/Dynamic/ContractAcceptanceScreen.js +12 -0
- package/lib/module/Screens/Dynamic/IdentityDocumentEIDScanningScreen.js +33 -10
- package/lib/module/Screens/Dynamic/IdentityDocumentScanningScreen.js +8 -10
- package/lib/module/Screens/Static/ResultScreen.js +74 -3
- package/lib/module/Shared/Components/EIDScanner.js +59 -3
- package/lib/module/Shared/Components/NavigationManager.js +6 -2
- package/lib/module/Shared/EIDReader/eidReader.js +52 -3
- package/lib/module/Shared/EIDReader/java/inputStream.js +6 -0
- package/lib/module/Shared/EIDReader/lds/icao/dg11File.js +60 -20
- package/lib/module/Shared/EIDReader/passiveAuth/der.js +93 -0
- package/lib/module/Shared/EIDReader/passiveAuth/integrity.js +71 -0
- package/lib/module/Shared/EIDReader/passiveAuth/sodParser.js +123 -0
- package/lib/module/Shared/Libs/chipVizMatch.js +93 -0
- package/lib/module/Shared/Libs/country-allow.js +16 -0
- package/lib/module/Shared/Libs/documentDataNormalizer.js +21 -11
- package/lib/module/Shared/Libs/geolocation.utils.js +77 -0
- package/lib/module/Shared/Libs/mrz.utils.js +24 -1
- package/lib/module/Shared/Libs/promise.utils.js +6 -0
- package/lib/module/Shared/Services/DataUploadService.js +18 -1
- package/lib/module/Translation/Resources/en.js +16 -0
- package/lib/module/Translation/Resources/tr.js +16 -0
- package/lib/module/Trustchex.js +4 -0
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts +9 -0
- package/lib/typescript/src/Screens/Dynamic/AddressCaptureScreen.d.ts.map +1 -0
- package/lib/typescript/src/Screens/Dynamic/ContractAcceptanceScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Dynamic/IdentityDocumentScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts +7 -1
- package/lib/typescript/src/Shared/Components/EIDScanner.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts +14 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts +3 -0
- package/lib/typescript/src/Shared/EIDReader/java/inputStream.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts +20 -7
- package/lib/typescript/src/Shared/EIDReader/lds/icao/dg11File.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts +31 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/der.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts +31 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/integrity.d.ts.map +1 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts +12 -0
- package/lib/typescript/src/Shared/EIDReader/passiveAuth/sodParser.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts +68 -0
- package/lib/typescript/src/Shared/Libs/chipVizMatch.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/country-allow.d.ts +10 -0
- package/lib/typescript/src/Shared/Libs/country-allow.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/documentDataNormalizer.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts +19 -0
- package/lib/typescript/src/Shared/Libs/geolocation.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentData.d.ts +7 -0
- package/lib/typescript/src/Shared/Types/documentData.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts +20 -0
- package/lib/typescript/src/Shared/Types/documentReadResult.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts +95 -2
- package/lib/typescript/src/Shared/Types/identificationInfo.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +16 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +16 -0
- package/lib/typescript/src/Translation/Resources/tr.d.ts.map +1 -1
- package/lib/typescript/src/Trustchex.d.ts.map +1 -1
- package/lib/typescript/src/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/Screens/Dynamic/AddressCaptureScreen.tsx +196 -0
- package/src/Screens/Dynamic/ContractAcceptanceScreen.tsx +12 -0
- package/src/Screens/Dynamic/IdentityDocumentEIDScanningScreen.tsx +46 -14
- package/src/Screens/Dynamic/IdentityDocumentScanningScreen.tsx +9 -11
- package/src/Screens/Static/ResultScreen.tsx +101 -6
- package/src/Shared/Components/EIDScanner.tsx +69 -2
- package/src/Shared/Components/NavigationManager.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +75 -2
- package/src/Shared/EIDReader/java/inputStream.ts +6 -0
- package/src/Shared/EIDReader/lds/icao/dg11File.ts +68 -22
- package/src/Shared/EIDReader/passiveAuth/der.ts +110 -0
- package/src/Shared/EIDReader/passiveAuth/integrity.ts +97 -0
- package/src/Shared/EIDReader/passiveAuth/sodParser.ts +142 -0
- package/src/Shared/Libs/chipVizMatch.ts +149 -0
- package/src/Shared/Libs/country-allow.ts +17 -0
- package/src/Shared/Libs/documentDataNormalizer.ts +24 -11
- package/src/Shared/Libs/geolocation.utils.ts +87 -0
- package/src/Shared/Libs/mrz.utils.ts +27 -1
- package/src/Shared/Libs/promise.utils.ts +11 -0
- package/src/Shared/Services/DataUploadService.ts +23 -0
- package/src/Shared/Types/documentData.ts +8 -0
- package/src/Shared/Types/documentReadResult.ts +20 -0
- package/src/Shared/Types/identificationInfo.ts +97 -2
- package/src/Translation/Resources/en.ts +21 -0
- package/src/Translation/Resources/tr.ts +21 -0
- package/src/Trustchex.tsx +5 -0
- 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 {
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
+
}
|