@trustchex/react-native-sdk 1.475.1 → 1.481.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/camera/TrustchexCameraView.kt +76 -24
- package/android/src/main/java/com/trustchex/reactnativesdk/mlkit/MLKitModule.kt +1 -0
- package/ios/Camera/TrustchexCameraView.swift +20 -15
- package/lib/module/Screens/Dynamic/LivenessDetectionScreen.js +3 -2
- package/lib/module/Screens/Static/QrCodeScanningScreen.js +12 -2
- package/lib/module/Screens/Static/ResultScreen.js +145 -113
- package/lib/module/Shared/Components/DebugOverlay.js +5 -2
- package/lib/module/Shared/Components/DiagnosticReportButton.js +64 -0
- package/lib/module/Shared/Components/IdentityDocumentCamera.js +185 -30
- package/lib/module/Shared/Components/IdentityDocumentCamera.utils.js +8 -1
- package/lib/module/Shared/Components/NavigationManager.js +7 -9
- package/lib/module/Shared/EIDReader/eidReader.js +159 -38
- package/lib/module/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/lib/module/Shared/Libs/SignalingClient.js +6 -5
- package/lib/module/Shared/Libs/crypto.utils.js +25 -3
- package/lib/module/Shared/Libs/deeplink.utils.js +37 -7
- package/lib/module/Shared/Libs/diagnosticReport.js +133 -0
- package/lib/module/Shared/Libs/diagnostics.js +171 -0
- package/lib/module/Shared/Libs/http-client.js +19 -6
- package/lib/module/Shared/Libs/mrz.utils.js +193 -33
- package/lib/module/Shared/Libs/mrzFrameAggregator.js +74 -3
- package/lib/module/Shared/Libs/native-device-info.utils.js +67 -0
- package/lib/module/Shared/Libs/promise.utils.js +4 -3
- package/lib/module/Shared/Libs/sendDiagnosticReport.js +130 -0
- package/lib/module/Shared/Libs/shuffle.utils.js +17 -0
- package/lib/module/Shared/Services/AnalyticsService.js +8 -2
- package/lib/module/Shared/Services/DataUploadService.js +114 -127
- package/lib/module/Shared/Services/VideoSessionService.js +4 -3
- package/lib/module/Translation/Resources/en.js +6 -0
- package/lib/module/Translation/Resources/tr.js +6 -0
- package/lib/module/Trustchex.js +20 -4
- package/lib/module/version.js +1 -1
- package/lib/typescript/src/Screens/Dynamic/LivenessDetectionScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/QrCodeScanningScreen.d.ts.map +1 -1
- package/lib/typescript/src/Screens/Static/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DebugOverlay.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts +20 -0
- package/lib/typescript/src/Shared/Components/DiagnosticReportButton.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.types.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts +1 -1
- package/lib/typescript/src/Shared/Components/IdentityDocumentCamera.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/NavigationManager.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts +10 -0
- package/lib/typescript/src/Shared/Components/TrustchexCamera.d.ts.map +1 -1
- package/lib/typescript/src/Shared/EIDReader/eidReader.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/SignalingClient.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/crypto.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts +1 -0
- package/lib/typescript/src/Shared/Libs/deeplink.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts +48 -0
- package/lib/typescript/src/Shared/Libs/diagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts +145 -0
- package/lib/typescript/src/Shared/Libs/diagnostics.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts +2 -0
- package/lib/typescript/src/Shared/Libs/http-client.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrz.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts +55 -0
- package/lib/typescript/src/Shared/Libs/mrzFrameAggregator.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts +33 -0
- package/lib/typescript/src/Shared/Libs/native-device-info.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/promise.utils.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts +30 -0
- package/lib/typescript/src/Shared/Libs/sendDiagnosticReport.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts +3 -0
- package/lib/typescript/src/Shared/Libs/shuffle.utils.d.ts.map +1 -0
- package/lib/typescript/src/Shared/Services/AnalyticsService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/DataUploadService.d.ts.map +1 -1
- package/lib/typescript/src/Shared/Services/VideoSessionService.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/en.d.ts +6 -0
- package/lib/typescript/src/Translation/Resources/en.d.ts.map +1 -1
- package/lib/typescript/src/Translation/Resources/tr.d.ts +6 -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 +17 -3
- package/src/Screens/Dynamic/LivenessDetectionScreen.tsx +4 -3
- package/src/Screens/Static/QrCodeScanningScreen.tsx +19 -2
- package/src/Screens/Static/ResultScreen.tsx +172 -162
- package/src/Shared/Components/DebugOverlay.tsx +5 -2
- package/src/Shared/Components/DiagnosticReportButton.tsx +77 -0
- package/src/Shared/Components/IdentityDocumentCamera.tsx +200 -29
- package/src/Shared/Components/IdentityDocumentCamera.types.ts +6 -0
- package/src/Shared/Components/IdentityDocumentCamera.utils.ts +9 -1
- package/src/Shared/Components/NavigationManager.tsx +7 -7
- package/src/Shared/Components/TrustchexCamera.tsx +6 -0
- package/src/Shared/EIDReader/eidReader.ts +181 -52
- package/src/Shared/Libs/MRZ_KNOWN_ISSUES.md +41 -0
- package/src/Shared/Libs/SignalingClient.ts +10 -5
- package/src/Shared/Libs/crypto.utils.ts +41 -4
- package/src/Shared/Libs/deeplink.utils.ts +39 -7
- package/src/Shared/Libs/diagnosticReport.ts +206 -0
- package/src/Shared/Libs/diagnostics.ts +251 -0
- package/src/Shared/Libs/http-client.ts +24 -8
- package/src/Shared/Libs/mrz.utils.ts +200 -34
- package/src/Shared/Libs/mrzFrameAggregator.ts +112 -2
- package/src/Shared/Libs/native-device-info.utils.ts +116 -0
- package/src/Shared/Libs/promise.utils.ts +11 -5
- package/src/Shared/Libs/sendDiagnosticReport.ts +165 -0
- package/src/Shared/Libs/shuffle.utils.ts +15 -0
- package/src/Shared/Services/AnalyticsService.ts +17 -1
- package/src/Shared/Services/DataUploadService.ts +155 -166
- package/src/Shared/Services/VideoSessionService.ts +15 -3
- package/src/Translation/Resources/en.ts +7 -0
- package/src/Translation/Resources/tr.ts +7 -0
- package/src/Trustchex.tsx +26 -3
- package/src/version.ts +1 -1
|
@@ -8,6 +8,34 @@ import { FaceImageInfo } from './lds/iso19794/faceImageInfo';
|
|
|
8
8
|
import { Buffer } from 'buffer';
|
|
9
9
|
import { MRZInfo } from './lds/icao/mrzInfo';
|
|
10
10
|
import { InputStream } from './java/inputStream';
|
|
11
|
+
import { debugLog } from '../Libs/debug.utils';
|
|
12
|
+
import { diagnostics } from '../Libs/diagnostics';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract an APDU status word (hex, e.g. "6982") from an error message if the
|
|
16
|
+
* protocol layer included one. Returns undefined when none is present. Kept
|
|
17
|
+
* deliberately loose — the protocol senders embed "SW=...." / "(6982)" forms.
|
|
18
|
+
*/
|
|
19
|
+
const extractStatusWord = (msg: string): string | undefined => {
|
|
20
|
+
const m = msg.match(/\b(?:SW[:= ]?)?([0-9a-fA-F]{4})\b/);
|
|
21
|
+
return m ? m[1].toUpperCase() : undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Categorise an NFC error message into a coarse, non-PII bucket. */
|
|
25
|
+
const categorizeNfcError = (msg: string): string => {
|
|
26
|
+
const m = msg.toLowerCase();
|
|
27
|
+
if (m.includes('cancel')) return 'user_cancelled';
|
|
28
|
+
if (m.includes('lost') || m.includes('steady')) return 'tag_lost';
|
|
29
|
+
if (m.includes('timeout')) return 'timeout';
|
|
30
|
+
if (m.includes('not supported') || m.includes('unsupported')) {
|
|
31
|
+
return 'unsupported';
|
|
32
|
+
}
|
|
33
|
+
if (m.includes('not enabled') || m.includes('disabled')) return 'not_enabled';
|
|
34
|
+
if (m.includes('auth') || m.includes('mutual') || m.includes('security')) {
|
|
35
|
+
return 'auth_failed';
|
|
36
|
+
}
|
|
37
|
+
return 'reading_error';
|
|
38
|
+
};
|
|
11
39
|
|
|
12
40
|
// --- Minimal PNG encoder (pure JS, no native deps) ---
|
|
13
41
|
// Uses deflate stored (uncompressed) blocks for O(n) encoding speed.
|
|
@@ -161,14 +189,15 @@ function convertJP2IfNeeded(
|
|
|
161
189
|
return { base64: imageBuffer.toString('base64'), mimeType };
|
|
162
190
|
}
|
|
163
191
|
|
|
164
|
-
|
|
192
|
+
debugLog('EID', '[EID] Face image is JP2, attempting conversion…');
|
|
165
193
|
try {
|
|
166
194
|
// Lazy-load jpeg2000 to avoid crashing if it fails to import
|
|
167
195
|
const { JpxImage } = require('jpeg2000');
|
|
168
196
|
const jpx = new JpxImage();
|
|
169
197
|
jpx.parse(imageBuffer);
|
|
170
198
|
const tile = jpx.tiles[0];
|
|
171
|
-
|
|
199
|
+
debugLog(
|
|
200
|
+
'EID',
|
|
172
201
|
`[EID] JP2 decoded: ${tile.width}x${tile.height}, ${jpx.componentsCount} channels, ${tile.items.length} bytes`
|
|
173
202
|
);
|
|
174
203
|
const pngBytes = pixelsToPng(
|
|
@@ -178,12 +207,13 @@ function convertJP2IfNeeded(
|
|
|
178
207
|
jpx.componentsCount
|
|
179
208
|
);
|
|
180
209
|
const pngBase64 = Buffer.from(pngBytes).toString('base64');
|
|
181
|
-
|
|
210
|
+
debugLog(
|
|
211
|
+
'EID',
|
|
182
212
|
`[EID] Converted JP2 → PNG (${pngBytes.length} bytes, ${pngBase64.length} base64 chars)`
|
|
183
213
|
);
|
|
184
214
|
return { base64: pngBase64, mimeType: 'image/png' };
|
|
185
215
|
} catch (err) {
|
|
186
|
-
|
|
216
|
+
debugLog('EID', '[EID] JP2 conversion failed:', err);
|
|
187
217
|
return { base64: imageBuffer.toString('base64'), mimeType };
|
|
188
218
|
}
|
|
189
219
|
}
|
|
@@ -304,8 +334,40 @@ const eidReader = async (
|
|
|
304
334
|
true
|
|
305
335
|
);
|
|
306
336
|
|
|
337
|
+
// Structured NFC diagnostics: record each step's outcome + timing + APDU SW
|
|
338
|
+
// into the session collector so the result screen can surface device-specific
|
|
339
|
+
// read failures. Best-effort and side-effect-only — never alters the flow.
|
|
340
|
+
diagnostics.nfcAttemptStarted();
|
|
341
|
+
const stepTimer = () => {
|
|
342
|
+
const t0 = Date.now();
|
|
343
|
+
return (step: string, ok: boolean, error?: unknown) => {
|
|
344
|
+
const msg = error instanceof Error ? error.message : error ? String(error) : undefined;
|
|
345
|
+
// Store only the APDU status word and a COARSE category — never the raw
|
|
346
|
+
// error text, which could in theory embed field/PII values. The SW + step
|
|
347
|
+
// name + category carry all the actionable signal.
|
|
348
|
+
diagnostics.nfcStep({
|
|
349
|
+
step,
|
|
350
|
+
ok,
|
|
351
|
+
ms: Date.now() - t0,
|
|
352
|
+
sw: msg ? extractStatusWord(msg) : undefined,
|
|
353
|
+
error: msg ? categorizeNfcError(msg) : undefined,
|
|
354
|
+
});
|
|
355
|
+
};
|
|
356
|
+
};
|
|
357
|
+
|
|
307
358
|
try {
|
|
308
|
-
|
|
359
|
+
{
|
|
360
|
+
const done = stepTimer();
|
|
361
|
+
try {
|
|
362
|
+
await passportService.open();
|
|
363
|
+
// Reaching open() without throwing means the chip powered on / was seen.
|
|
364
|
+
diagnostics.nfcChipDetected();
|
|
365
|
+
done('open', true);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
done('open', false, e);
|
|
368
|
+
throw e;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
309
371
|
|
|
310
372
|
progress = 1;
|
|
311
373
|
if (progressCallback) {
|
|
@@ -317,14 +379,20 @@ const eidReader = async (
|
|
|
317
379
|
// regardless of which application the chip activates on power-on.
|
|
318
380
|
let hasPACESucceeded = false;
|
|
319
381
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
382
|
+
{
|
|
383
|
+
const done = stepTimer();
|
|
384
|
+
try {
|
|
385
|
+
await passportService.sendSelectMF();
|
|
386
|
+
done('selectMF', true);
|
|
387
|
+
debugLog('EID', '[EID] SELECT MF OK');
|
|
388
|
+
} catch (mfErr) {
|
|
389
|
+
done('selectMF', false, mfErr);
|
|
390
|
+
const mfMsg = mfErr instanceof Error ? mfErr.message : String(mfErr);
|
|
391
|
+
debugLog(
|
|
392
|
+
'EID',
|
|
393
|
+
`[EID] SELECT MF failed (${mfMsg}), continuing with SFI read anyway`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
328
396
|
}
|
|
329
397
|
|
|
330
398
|
progress = 2;
|
|
@@ -335,26 +403,33 @@ const eidReader = async (
|
|
|
335
403
|
// Read EF.CardAccess — kept in a separate try/catch so we can distinguish
|
|
336
404
|
// "file read failure" from "PACE protocol failure" in the logs.
|
|
337
405
|
let cardAccessBuf: Buffer | null = null;
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
buf
|
|
406
|
+
{
|
|
407
|
+
const done = stepTimer();
|
|
408
|
+
try {
|
|
409
|
+
const cardAccessStream = passportService.getInputStream(
|
|
410
|
+
EIDService.EF_CARD_ACCESS
|
|
411
|
+
);
|
|
412
|
+
await cardAccessStream.init();
|
|
413
|
+
const cardAccessLen = cardAccessStream.getLength();
|
|
414
|
+
const buf = Buffer.alloc(cardAccessLen);
|
|
415
|
+
for (let i = 0; i < cardAccessLen; i++) {
|
|
416
|
+
buf[i] = await cardAccessStream.read();
|
|
417
|
+
}
|
|
418
|
+
cardAccessBuf = buf;
|
|
419
|
+
done('readCardAccess', true);
|
|
420
|
+
debugLog(
|
|
421
|
+
'EID',
|
|
422
|
+
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
423
|
+
);
|
|
424
|
+
} catch (readErr) {
|
|
425
|
+
done('readCardAccess', false, readErr);
|
|
426
|
+
const readMsg =
|
|
427
|
+
readErr instanceof Error ? readErr.message : String(readErr);
|
|
428
|
+
debugLog(
|
|
429
|
+
'EID',
|
|
430
|
+
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
431
|
+
);
|
|
347
432
|
}
|
|
348
|
-
cardAccessBuf = buf;
|
|
349
|
-
console.debug(
|
|
350
|
-
`[EID] EF.CardAccess read OK: ${cardAccessLen} bytes = ${cardAccessBuf.toString('hex')}`
|
|
351
|
-
);
|
|
352
|
-
} catch (readErr) {
|
|
353
|
-
const readMsg =
|
|
354
|
-
readErr instanceof Error ? readErr.message : String(readErr);
|
|
355
|
-
console.debug(
|
|
356
|
-
`[EID] EF.CardAccess read FAILED: ${readMsg} — falling back to BAC`
|
|
357
|
-
);
|
|
358
433
|
}
|
|
359
434
|
|
|
360
435
|
progress = 3;
|
|
@@ -363,14 +438,21 @@ const eidReader = async (
|
|
|
363
438
|
}
|
|
364
439
|
|
|
365
440
|
// If EF.CardAccess was read successfully, attempt PACE.
|
|
441
|
+
if (cardAccessBuf === null) {
|
|
442
|
+
// EF.CardAccess unreadable (the readCardAccess step already recorded the
|
|
443
|
+
// failure/SW) → PACE can't be attempted; BAC will be used.
|
|
444
|
+
diagnostics.nfcPaceFellBackToBac('no_card_access');
|
|
445
|
+
}
|
|
366
446
|
if (cardAccessBuf !== null) {
|
|
367
447
|
try {
|
|
368
448
|
const paceInfo = parsePACEInfoFromCardAccess(cardAccessBuf);
|
|
369
449
|
|
|
370
450
|
if (paceInfo) {
|
|
371
|
-
|
|
451
|
+
debugLog(
|
|
452
|
+
'EID',
|
|
372
453
|
`[EID] PACE available: oid=${paceInfo.oid} parameterId=${paceInfo.parameterId}`
|
|
373
454
|
);
|
|
455
|
+
diagnostics.nfcSetAuth('PACE', paceInfo.oid);
|
|
374
456
|
// PACE is available - derive key from MRZ and perform PACE
|
|
375
457
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
376
458
|
const paceKey = PACEKeySpec.createMRZKey(bacKey.getKeySeedForPACE());
|
|
@@ -380,28 +462,39 @@ const eidReader = async (
|
|
|
380
462
|
progressCallback(progress);
|
|
381
463
|
}
|
|
382
464
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
465
|
+
const done = stepTimer();
|
|
466
|
+
try {
|
|
467
|
+
await passportService.doPACE(
|
|
468
|
+
paceKey,
|
|
469
|
+
paceInfo.oid,
|
|
470
|
+
paceInfo.parameterId,
|
|
471
|
+
progressCallback
|
|
472
|
+
);
|
|
473
|
+
hasPACESucceeded = true;
|
|
474
|
+
done('paceAuth', true);
|
|
475
|
+
debugLog('EID', '[EID] PACE succeeded');
|
|
476
|
+
} catch (paceErr) {
|
|
477
|
+
done('paceAuth', false, paceErr);
|
|
478
|
+
throw paceErr;
|
|
479
|
+
}
|
|
391
480
|
} else {
|
|
392
|
-
|
|
481
|
+
diagnostics.nfcPaceFellBackToBac('no_pace_info');
|
|
482
|
+
debugLog(
|
|
483
|
+
'EID',
|
|
393
484
|
'[EID] EF.CardAccess read OK but no PACE SecurityInfo found — document does not support PACE, falling back to BAC'
|
|
394
485
|
);
|
|
395
486
|
}
|
|
396
487
|
} catch (paceError) {
|
|
397
488
|
// PACE protocol exchange failed — fall back to BAC
|
|
489
|
+
diagnostics.nfcPaceFellBackToBac('pace_failed');
|
|
398
490
|
const msg =
|
|
399
491
|
paceError instanceof Error ? paceError.message : String(paceError);
|
|
400
|
-
|
|
492
|
+
debugLog(
|
|
493
|
+
'EID',
|
|
401
494
|
`[EID] PACE protocol failed (${msg}), falling back to BAC`
|
|
402
495
|
);
|
|
403
496
|
if (paceError instanceof Error && paceError.stack) {
|
|
404
|
-
|
|
497
|
+
debugLog('EID', '[EID] PACE error stack:', paceError.stack);
|
|
405
498
|
}
|
|
406
499
|
}
|
|
407
500
|
}
|
|
@@ -412,7 +505,16 @@ const eidReader = async (
|
|
|
412
505
|
}
|
|
413
506
|
|
|
414
507
|
// Select Applet for MRTD
|
|
415
|
-
|
|
508
|
+
{
|
|
509
|
+
const done = stepTimer();
|
|
510
|
+
try {
|
|
511
|
+
await passportService.sendSelectApplet(hasPACESucceeded);
|
|
512
|
+
done('selectApplet', true);
|
|
513
|
+
} catch (e) {
|
|
514
|
+
done('selectApplet', false, e);
|
|
515
|
+
throw e;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
416
518
|
|
|
417
519
|
progress = 10;
|
|
418
520
|
if (progressCallback) {
|
|
@@ -427,8 +529,16 @@ const eidReader = async (
|
|
|
427
529
|
await efComStream.read();
|
|
428
530
|
} catch (error) {
|
|
429
531
|
// EF_COM not available -> try to do BAC
|
|
532
|
+
diagnostics.nfcSetAuth('BAC');
|
|
430
533
|
const bacKey = new BACKey(documentNumber, dateOfBirth, dateOfExpiry);
|
|
431
|
-
|
|
534
|
+
const done = stepTimer();
|
|
535
|
+
try {
|
|
536
|
+
await passportService.doBAC(bacKey);
|
|
537
|
+
done('bacAuth', true);
|
|
538
|
+
} catch (e) {
|
|
539
|
+
done('bacAuth', false, e);
|
|
540
|
+
throw e;
|
|
541
|
+
}
|
|
432
542
|
}
|
|
433
543
|
}
|
|
434
544
|
|
|
@@ -438,10 +548,18 @@ const eidReader = async (
|
|
|
438
548
|
}
|
|
439
549
|
|
|
440
550
|
// Read DG1 (MRZ Info)
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
551
|
+
const dg1Done = stepTimer();
|
|
552
|
+
let mrzInfo: MRZInfo;
|
|
553
|
+
try {
|
|
554
|
+
const dg1InputStream = passportService.getInputStream(EIDService.EF_DG1);
|
|
555
|
+
await dg1InputStream.init();
|
|
556
|
+
const dg1File = new DG1File(undefined, dg1InputStream);
|
|
557
|
+
mrzInfo = await dg1File.getMRZInfo();
|
|
558
|
+
dg1Done('readDG1', true);
|
|
559
|
+
} catch (e) {
|
|
560
|
+
dg1Done('readDG1', false, e);
|
|
561
|
+
throw e;
|
|
562
|
+
}
|
|
445
563
|
|
|
446
564
|
progress = 30;
|
|
447
565
|
if (progressCallback) {
|
|
@@ -449,6 +567,7 @@ const eidReader = async (
|
|
|
449
567
|
}
|
|
450
568
|
|
|
451
569
|
// Read DG2 (Face Image)
|
|
570
|
+
const dg2Done = stepTimer();
|
|
452
571
|
const dg2InputStream = passportService.getInputStream(EIDService.EF_DG2);
|
|
453
572
|
await dg2InputStream.init();
|
|
454
573
|
InputStream.onRead((totalBytesRead, fileLength) => {
|
|
@@ -462,7 +581,14 @@ const eidReader = async (
|
|
|
462
581
|
let mimeType = '';
|
|
463
582
|
|
|
464
583
|
const allFaceImageInfos: FaceImageInfo[] = [];
|
|
465
|
-
|
|
584
|
+
let faceInfos;
|
|
585
|
+
try {
|
|
586
|
+
faceInfos = await dg2File.getFaceInfos();
|
|
587
|
+
dg2Done('readDG2', true);
|
|
588
|
+
} catch (e) {
|
|
589
|
+
dg2Done('readDG2', false, e);
|
|
590
|
+
throw e;
|
|
591
|
+
}
|
|
466
592
|
for (let faceInfo of faceInfos) {
|
|
467
593
|
allFaceImageInfos.push(...faceInfo.getFaceImageInfos());
|
|
468
594
|
}
|
|
@@ -484,13 +610,16 @@ const eidReader = async (
|
|
|
484
610
|
progressCallback(progress);
|
|
485
611
|
}
|
|
486
612
|
|
|
613
|
+
diagnostics.nfcResult(true);
|
|
487
614
|
return {
|
|
488
615
|
mrz: mrzInfo,
|
|
489
616
|
faceImage: imageAsBase64,
|
|
490
617
|
mimeType: mimeType,
|
|
491
618
|
};
|
|
492
619
|
} catch (error) {
|
|
493
|
-
|
|
620
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
621
|
+
diagnostics.nfcResult(false, categorizeNfcError(msg));
|
|
622
|
+
debugLog('EID', 'Error reading passport data', error);
|
|
494
623
|
} finally {
|
|
495
624
|
await passportService.close();
|
|
496
625
|
}
|
|
@@ -56,6 +56,40 @@ primary safeguard. A single noisy frame is not trusted; the consensus is.
|
|
|
56
56
|
|
|
57
57
|
---
|
|
58
58
|
|
|
59
|
+
## 2a. Turkish e-ID document numbers: leading serial LETTER must be preserved
|
|
60
|
+
|
|
61
|
+
**Issue.** Turkish identity cards put a **serial letter** at the start of the
|
|
62
|
+
9-char document number (e.g. `A53032352`, `A49M38410`). Empirically — verified
|
|
63
|
+
against real cards — **Türkiye computes the document-number check digit over the
|
|
64
|
+
number with the LEADING letter EXCLUDED** (interior letters are kept), which
|
|
65
|
+
diverges from strict ICAO 9303 (where `A`=10 is weighted in). So a genuine card
|
|
66
|
+
`A53032352` (printed check digit `5`) **fails** the strict ICAO check (which
|
|
67
|
+
wants `7`), even though the card is valid — `icaoCheckDigit("53032352") === "5"`.
|
|
68
|
+
The TD1 **composite** on these cards is likewise non-standard (no interpretation
|
|
69
|
+
of the leading letter reproduces it), so it is **not** a reliable anchor.
|
|
70
|
+
|
|
71
|
+
**Why it matters.** The leading letter is **real** and **must be preserved**: the
|
|
72
|
+
**NFC eID chip derives its BAC/PACE access key from the document number exactly
|
|
73
|
+
as stored** (with the letter). Rewriting `A53032352`→`453032352` — as the
|
|
74
|
+
ambiguous-char recovery would, to satisfy strict ICAO — makes the **chip read
|
|
75
|
+
FAIL**. (The OCR-B `4` glyph closely resembles `A`, which is why the recovery
|
|
76
|
+
used to "correct" it.)
|
|
77
|
+
|
|
78
|
+
**Mitigation.** `acceptsTurkishLetterPrefix` (in `mrz.utils.ts`; ported to
|
|
79
|
+
`mrz_utils.dart`) accepts a TUR TD1 whose only failing checks are
|
|
80
|
+
document-number/composite **when** the document number starts with a letter
|
|
81
|
+
**and** `icaoCheckDigit(documentNumber.slice(1))` matches the printed
|
|
82
|
+
document-number check digit. The parse is then accepted **as-is**, preserving the
|
|
83
|
+
letter; the composite is deliberately not required. The subsequent **NFC read is
|
|
84
|
+
the authoritative confirmation** — it only succeeds with the correct
|
|
85
|
+
(letter-preserving) number. Contrast `A49M38410`, whose letters validate under
|
|
86
|
+
*standard* ICAO and so are kept by the normal path.
|
|
87
|
+
|
|
88
|
+
Covered by the "Turkish e-ID … leading serial LETTER" tests in
|
|
89
|
+
`mrz.ambiguous.test.ts` (RN) and `mrz_ambiguous_test.dart` (Flutter).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
59
93
|
## 3. Filler-letter normalisation is conservative in name fields (by design)
|
|
60
94
|
|
|
61
95
|
**Issue.** The `<` filler is frequently OCR'd as a letter (`K`, and under glare
|
|
@@ -84,6 +118,13 @@ Covered by the "real letters in names are not eaten" tests in `mrz.utils.test.ts
|
|
|
84
118
|
30–44-char MRZ line below that floor, so the native pipeline **crops to the
|
|
85
119
|
on-screen scan frame and upscales + grayscale/contrast-enhances** before OCR.
|
|
86
120
|
The MRZ band must be reasonably filled within the on-screen guide.
|
|
121
|
+
- Two distinct regions exist in the native code (don't confuse them):
|
|
122
|
+
the **MRZ-OCR crop** (`buildScanFrameRoiImage`: top 25% / bottom 12% / sides
|
|
123
|
+
4%, upscaled ≤2.5× to ≤2200px, grayscale, contrast 1.6×) and the
|
|
124
|
+
**brightness-sampling region** (the on-screen guide: 36% / 36% / 5%). Both are
|
|
125
|
+
the same on iOS and Android. The diagnostic report records the measured MRZ
|
|
126
|
+
band pixel height so you can tell when a device leaves the band below the
|
|
127
|
+
~16 px/char floor.
|
|
87
128
|
- **Glare / lamination.** Heavy glare on a glossy laminated card still degrades
|
|
88
129
|
OCR; the enhancement helps but does not eliminate it. Steadying the card and
|
|
89
130
|
reducing direct glare improves convergence.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventSource, { type EventSourceListener } from 'react-native-sse';
|
|
2
|
+
import { debugLog } from './debug.utils';
|
|
2
3
|
|
|
3
4
|
export type SignalingMessageType =
|
|
4
5
|
| 'offer'
|
|
@@ -51,7 +52,7 @@ export class SignalingClient {
|
|
|
51
52
|
}
|
|
52
53
|
const url = `${this.baseUrl}/api/app/mobile/video-sessions/${this.sessionId}/signaling/stream?${urlParams.toString()}`;
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
debugLog('SignalingClient', '[SignalingClient] Connecting to SSE:', url);
|
|
55
56
|
|
|
56
57
|
this.eventSource = new EventSource(url, {
|
|
57
58
|
pollingInterval: 0, // 0 means default/no polling if SSE is real
|
|
@@ -59,7 +60,7 @@ export class SignalingClient {
|
|
|
59
60
|
|
|
60
61
|
const listener: EventSourceListener = (event) => {
|
|
61
62
|
if (event.type === 'open') {
|
|
62
|
-
|
|
63
|
+
debugLog('SignalingClient', '[SignalingClient] Connected');
|
|
63
64
|
this.onConnected?.();
|
|
64
65
|
} else if (event.type === 'error') {
|
|
65
66
|
console.error(
|
|
@@ -108,8 +109,8 @@ export class SignalingClient {
|
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
// Ping handler - server sends pings to keep connection alive
|
|
111
|
-
const pingListener = (
|
|
112
|
-
|
|
112
|
+
const pingListener = (_event: any) => {
|
|
113
|
+
debugLog('SignalingClient', '[SignalingClient] Received ping');
|
|
113
114
|
// Just acknowledge by doing nothing, server-side heartbeat keeps connection alive
|
|
114
115
|
};
|
|
115
116
|
|
|
@@ -117,7 +118,11 @@ export class SignalingClient {
|
|
|
117
118
|
const sessionEndedListener = (event: any) => {
|
|
118
119
|
try {
|
|
119
120
|
const data = JSON.parse(event.data || '{}');
|
|
120
|
-
|
|
121
|
+
debugLog(
|
|
122
|
+
'SignalingClient',
|
|
123
|
+
'[SignalingClient] Session ended:',
|
|
124
|
+
data.state
|
|
125
|
+
);
|
|
121
126
|
if (this.onSessionEnded) {
|
|
122
127
|
this.onSessionEnded(data.state);
|
|
123
128
|
}
|
|
@@ -3,21 +3,56 @@ import { Buffer } from 'buffer';
|
|
|
3
3
|
import { gcm } from '@noble/ciphers/aes';
|
|
4
4
|
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
5
5
|
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
6
|
-
import
|
|
6
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
7
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
8
|
+
import httpClient, { setSessionToken } from './http-client';
|
|
9
|
+
|
|
10
|
+
// Must stay byte-for-byte identical to the backend derivation
|
|
11
|
+
// (web-app/src/lib/crypto-utils.ts).
|
|
12
|
+
const SESSION_KEY_KDF_INFO = 'trustchex-session-key-v2';
|
|
13
|
+
const SESSION_KEY_KDF_VERSION = 2;
|
|
14
|
+
|
|
15
|
+
type AuthResponse = {
|
|
16
|
+
serverPublicKey: string;
|
|
17
|
+
kdfVersion?: number;
|
|
18
|
+
sessionToken?: string;
|
|
19
|
+
};
|
|
7
20
|
|
|
8
21
|
const getSessionKey = async (apiUrl: string, sessionId: string) => {
|
|
9
22
|
const clientPrivateKey = secp256k1.utils.randomPrivateKey();
|
|
10
23
|
const clientPublicKey = secp256k1.getPublicKey(clientPrivateKey);
|
|
11
24
|
const serverResponse = await httpClient.post<
|
|
12
|
-
|
|
13
|
-
{ clientPublicKey: string }
|
|
25
|
+
AuthResponse,
|
|
26
|
+
{ clientPublicKey: string; kdfVersion: number }
|
|
14
27
|
>(`${apiUrl}/verification-sessions/${sessionId}/auth`, {
|
|
15
28
|
clientPublicKey: Buffer.from(clientPublicKey).toString('hex'),
|
|
29
|
+
kdfVersion: SESSION_KEY_KDF_VERSION,
|
|
16
30
|
});
|
|
31
|
+
|
|
32
|
+
if (serverResponse.sessionToken) {
|
|
33
|
+
setSessionToken(serverResponse.sessionToken);
|
|
34
|
+
}
|
|
35
|
+
|
|
17
36
|
const sharedKey = secp256k1.getSharedSecret(
|
|
18
37
|
clientPrivateKey,
|
|
19
38
|
Buffer.from(serverResponse.serverPublicKey, 'hex').toString('hex')
|
|
20
39
|
);
|
|
40
|
+
|
|
41
|
+
// The server echoes the kdfVersion it actually used; older backends don't
|
|
42
|
+
// send the field and derive the legacy key.
|
|
43
|
+
if (serverResponse.kdfVersion === SESSION_KEY_KDF_VERSION) {
|
|
44
|
+
// sharedKey is a compressed secp256k1 point; byte 0 is the 0x02/0x03
|
|
45
|
+
// parity prefix and carries no secret entropy.
|
|
46
|
+
const derived = hkdf(
|
|
47
|
+
sha256,
|
|
48
|
+
Buffer.from(sharedKey).subarray(1),
|
|
49
|
+
Buffer.from(sessionId, 'utf-8'),
|
|
50
|
+
SESSION_KEY_KDF_INFO,
|
|
51
|
+
32
|
|
52
|
+
);
|
|
53
|
+
return Buffer.from(derived).toString('hex');
|
|
54
|
+
}
|
|
55
|
+
|
|
21
56
|
const sessionKey = (
|
|
22
57
|
Buffer.from(new Date().getUTCDate().toString(), 'utf-8').toString('hex') +
|
|
23
58
|
Buffer.from(sharedKey).toString('hex')
|
|
@@ -27,7 +62,9 @@ const getSessionKey = async (apiUrl: string, sessionId: string) => {
|
|
|
27
62
|
};
|
|
28
63
|
|
|
29
64
|
const encryptWithAes = (data: string, key: string) => {
|
|
30
|
-
|
|
65
|
+
// Standard 96-bit GCM IV; the backend reads the nonce from the payload, so
|
|
66
|
+
// this stays compatible with servers that previously saw 32-byte nonces.
|
|
67
|
+
const nonce = randomBytes(12);
|
|
31
68
|
const dataBytes = Buffer.from(data, 'utf-8');
|
|
32
69
|
const aes = gcm(Buffer.from(key, 'hex'), nonce);
|
|
33
70
|
const ciphertext = aes.encrypt(dataBytes);
|
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
import { debugLog } from './debug.utils';
|
|
1
|
+
import { debugLog, logWarn } from './debug.utils';
|
|
2
|
+
|
|
3
|
+
// Hostname (optionally with port) — rejects paths, credentials, and other
|
|
4
|
+
// URL components smuggled into the app-url segment.
|
|
5
|
+
const HOST_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9.-]*(:\d{1,5})?$/;
|
|
6
|
+
|
|
7
|
+
const getUrlScheme = (url: string): string => {
|
|
8
|
+
const match = url.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
9
|
+
return match ? match[1].toLowerCase() : '';
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// The SDK uploads identity documents and biometrics to the derived baseUrl, so
|
|
13
|
+
// cleartext destinations are only allowed in development builds.
|
|
14
|
+
const isAllowedScheme = (scheme: string): boolean => {
|
|
15
|
+
return scheme === 'https' || (__DEV__ && scheme === 'http');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getUrlHost = (url: string): string => {
|
|
19
|
+
const withoutScheme = url.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//, '');
|
|
20
|
+
return (withoutScheme.split('/')[0] ?? '').toLowerCase();
|
|
21
|
+
};
|
|
2
22
|
|
|
3
23
|
const handleDeepLink = ({ url }: { url: string }) => {
|
|
4
24
|
debugLog('handleDeepLink', 'Received URL:', url);
|
|
5
|
-
const
|
|
6
|
-
const route = url.
|
|
25
|
+
const scheme = getUrlScheme(url);
|
|
26
|
+
const route = scheme ? url.slice(`${scheme}://`.length) : url;
|
|
7
27
|
const segments = route.split('/');
|
|
8
28
|
debugLog('handleDeepLink', 'Parsed segments:', segments);
|
|
9
29
|
let baseUrl = '';
|
|
@@ -17,16 +37,28 @@ const handleDeepLink = ({ url }: { url: string }) => {
|
|
|
17
37
|
sessionId = segments[i + 1] ?? '';
|
|
18
38
|
debugLog('handleDeepLink', 'Found sessionId:', sessionId);
|
|
19
39
|
} else if (segments[i] === 'app-url') {
|
|
20
|
-
|
|
40
|
+
const host = segments[i + 1] ?? '';
|
|
41
|
+
if (!HOST_PATTERN.test(host)) {
|
|
42
|
+
logWarn('handleDeepLink', 'Rejected invalid app-url host');
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const hostScheme = scheme === 'http' ? 'http' : 'https';
|
|
46
|
+
if (!isAllowedScheme(hostScheme)) {
|
|
47
|
+
logWarn('handleDeepLink', 'Rejected non-HTTPS app-url');
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
baseUrl = `${hostScheme}://${host}`;
|
|
21
51
|
debugLog('handleDeepLink', 'Found baseUrl:', baseUrl);
|
|
22
52
|
}
|
|
23
53
|
}
|
|
24
54
|
|
|
25
55
|
// If no app-url segment found, derive baseUrl from the URL itself
|
|
26
56
|
if (!baseUrl && sessionId) {
|
|
27
|
-
const match = url.match(/^(https
|
|
28
|
-
if (match) {
|
|
29
|
-
baseUrl = match[1]
|
|
57
|
+
const match = url.match(/^(https?):\/\/([^/]+)/);
|
|
58
|
+
if (match && isAllowedScheme(match[1].toLowerCase())) {
|
|
59
|
+
baseUrl = `${match[1].toLowerCase()}://${match[2]}`;
|
|
60
|
+
} else if (match) {
|
|
61
|
+
logWarn('handleDeepLink', 'Rejected non-HTTPS deep link URL');
|
|
30
62
|
}
|
|
31
63
|
}
|
|
32
64
|
|