@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.
- 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/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/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/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
|
@@ -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 (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|