@trustchex/react-native-sdk 1.500.2 → 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/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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readNode,
|
|
3
|
+
readChildren,
|
|
4
|
+
decodeOid,
|
|
5
|
+
decodeInteger,
|
|
6
|
+
toHex,
|
|
7
|
+
type DerNode,
|
|
8
|
+
} from './der';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse EF.SOD (ICAO 9303-11 §5.1) to extract the LDSSecurityObject: the digest
|
|
12
|
+
* algorithm and the per-data-group hash list the chip's issuer signed. This is
|
|
13
|
+
* the input to the on-device integrity check (recompute each read DG's hash and
|
|
14
|
+
* compare). Signature verification + CSCA chaining is done server-side.
|
|
15
|
+
*
|
|
16
|
+
* Dependency-free DER walking so it is fully unit-testable from hex fixtures.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Map of OID → digest algorithm name used for DG hashing. */
|
|
20
|
+
const DIGEST_OIDS: Record<string, string> = {
|
|
21
|
+
'1.3.14.3.2.26': 'SHA-1',
|
|
22
|
+
'2.16.840.1.101.3.4.2.1': 'SHA-256',
|
|
23
|
+
'2.16.840.1.101.3.4.2.2': 'SHA-384',
|
|
24
|
+
'2.16.840.1.101.3.4.2.3': 'SHA-512',
|
|
25
|
+
'2.16.840.1.101.3.4.2.4': 'SHA-224',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const OID_SIGNED_DATA = '1.2.840.113549.1.7.2';
|
|
29
|
+
|
|
30
|
+
export interface SodSecurityObject {
|
|
31
|
+
/** Digest algorithm name, e.g. "SHA-256". */
|
|
32
|
+
digestAlgorithm: string;
|
|
33
|
+
/** Per-DG hashes from the SOD: dataGroupNumber → lowercase hex hash. */
|
|
34
|
+
dataGroupHashes: Record<number, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TAG_SEQUENCE = 0x10;
|
|
38
|
+
const TAG_OID = 0x06;
|
|
39
|
+
const TAG_OCTET_STRING = 0x04;
|
|
40
|
+
const TAG_INTEGER = 0x02;
|
|
41
|
+
const TAG_SET = 0x11;
|
|
42
|
+
|
|
43
|
+
/** Find the first child whose masked tag number equals `tagNumber`. */
|
|
44
|
+
function findChild(children: DerNode[], tagNumber: number): DerNode | undefined {
|
|
45
|
+
return children.find((c) => c.tagNumber === tagNumber);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parse the EF.SOD bytes (with or without the outer 0x77 application wrapper).
|
|
50
|
+
* Throws on a malformed structure.
|
|
51
|
+
*/
|
|
52
|
+
export function parseSod(sodBytes: Uint8Array): SodSecurityObject {
|
|
53
|
+
let root = readNode(sodBytes, 0);
|
|
54
|
+
|
|
55
|
+
// Unwrap the [APPLICATION 23] (0x77) wrapper if present.
|
|
56
|
+
if (root.tag === 0x77) {
|
|
57
|
+
root = readNode(root.content, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// root = ContentInfo SEQUENCE { OID id-signedData, [0] SignedData }
|
|
61
|
+
if (root.tagNumber !== TAG_SEQUENCE) {
|
|
62
|
+
throw new Error('SOD: expected ContentInfo SEQUENCE');
|
|
63
|
+
}
|
|
64
|
+
const contentInfo = readChildren(root.content);
|
|
65
|
+
const ciOid = findChild(contentInfo, TAG_OID);
|
|
66
|
+
if (!ciOid || decodeOid(ciOid.content) !== OID_SIGNED_DATA) {
|
|
67
|
+
throw new Error('SOD: ContentInfo is not signedData');
|
|
68
|
+
}
|
|
69
|
+
// [0] explicit wrapper around SignedData.
|
|
70
|
+
const signedDataWrapper = contentInfo.find((c) => c.constructed && c.tag === 0xa0);
|
|
71
|
+
if (!signedDataWrapper) throw new Error('SOD: missing SignedData wrapper');
|
|
72
|
+
const signedData = readNode(signedDataWrapper.content, 0);
|
|
73
|
+
if (signedData.tagNumber !== TAG_SEQUENCE) {
|
|
74
|
+
throw new Error('SOD: SignedData not a SEQUENCE');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SignedData = { version, digestAlgorithms SET, encapContentInfo SEQUENCE, ... }
|
|
78
|
+
const sdChildren = readChildren(signedData.content);
|
|
79
|
+
// encapContentInfo is the first SEQUENCE after the digestAlgorithms SET.
|
|
80
|
+
const setIdx = sdChildren.findIndex((c) => c.tagNumber === TAG_SET);
|
|
81
|
+
const encap = sdChildren
|
|
82
|
+
.slice(setIdx + 1)
|
|
83
|
+
.find((c) => c.tagNumber === TAG_SEQUENCE);
|
|
84
|
+
if (!encap) throw new Error('SOD: missing encapContentInfo');
|
|
85
|
+
|
|
86
|
+
// encapContentInfo = { eContentType OID, [0] eContent OCTET STRING }
|
|
87
|
+
const encapChildren = readChildren(encap.content);
|
|
88
|
+
const eContentWrapper = encapChildren.find((c) => c.constructed && c.tag === 0xa0);
|
|
89
|
+
if (!eContentWrapper) throw new Error('SOD: missing eContent');
|
|
90
|
+
const eContentOctet = readNode(eContentWrapper.content, 0);
|
|
91
|
+
if (eContentOctet.tagNumber !== TAG_OCTET_STRING) {
|
|
92
|
+
throw new Error('SOD: eContent not an OCTET STRING');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return parseLdsSecurityObject(eContentOctet.content);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse the LDSSecurityObject:
|
|
100
|
+
* SEQUENCE { version INTEGER, hashAlgorithm AlgorithmIdentifier,
|
|
101
|
+
* SEQUENCE OF DataGroupHash { number INTEGER, hash OCTET STRING } }
|
|
102
|
+
*/
|
|
103
|
+
function parseLdsSecurityObject(bytes: Uint8Array): SodSecurityObject {
|
|
104
|
+
const lds = readNode(bytes, 0);
|
|
105
|
+
if (lds.tagNumber !== TAG_SEQUENCE) {
|
|
106
|
+
throw new Error('SOD: LDSSecurityObject not a SEQUENCE');
|
|
107
|
+
}
|
|
108
|
+
const children = readChildren(lds.content);
|
|
109
|
+
|
|
110
|
+
// hashAlgorithm AlgorithmIdentifier = SEQUENCE { OID, params? }
|
|
111
|
+
const algId = children.find((c) => c.tagNumber === TAG_SEQUENCE);
|
|
112
|
+
if (!algId) throw new Error('SOD: missing hashAlgorithm');
|
|
113
|
+
const algOidNode = findChild(readChildren(algId.content), TAG_OID);
|
|
114
|
+
if (!algOidNode) throw new Error('SOD: missing hashAlgorithm OID');
|
|
115
|
+
const digestAlgorithm = DIGEST_OIDS[decodeOid(algOidNode.content)];
|
|
116
|
+
if (!digestAlgorithm) {
|
|
117
|
+
throw new Error(`SOD: unsupported digest OID ${decodeOid(algOidNode.content)}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// dataGroupHashValues — the SEQUENCE OF after the hashAlgorithm.
|
|
121
|
+
const algIdx = children.indexOf(algId);
|
|
122
|
+
const hashSeq = children
|
|
123
|
+
.slice(algIdx + 1)
|
|
124
|
+
.find((c) => c.tagNumber === TAG_SEQUENCE);
|
|
125
|
+
if (!hashSeq) throw new Error('SOD: missing dataGroupHashValues');
|
|
126
|
+
|
|
127
|
+
const dataGroupHashes: Record<number, string> = {};
|
|
128
|
+
for (const dgh of readChildren(hashSeq.content)) {
|
|
129
|
+
if (dgh.tagNumber !== TAG_SEQUENCE) continue;
|
|
130
|
+
const parts = readChildren(dgh.content);
|
|
131
|
+
const numNode = parts.find((p) => p.tagNumber === TAG_INTEGER);
|
|
132
|
+
const hashNode = parts.find((p) => p.tagNumber === TAG_OCTET_STRING);
|
|
133
|
+
if (!numNode || !hashNode) continue;
|
|
134
|
+
dataGroupHashes[decodeInteger(numNode.content)] = toHex(hashNode.content);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (Object.keys(dataGroupHashes).length === 0) {
|
|
138
|
+
throw new Error('SOD: no data-group hashes found');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { digestAlgorithm, dataGroupHashes };
|
|
142
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { DocumentData } from '../Types/documentData';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Verdict of comparing the NFC chip-read (DG1) identity data against the
|
|
5
|
+
* camera-OCR-read (printed / VIZ) identity data.
|
|
6
|
+
*
|
|
7
|
+
* Required by MASAK Tebliğ No. 32, Article 4/C(1)(b): for remote identity
|
|
8
|
+
* verification of non-Turkish nationals via NFC passport, the chip data must be
|
|
9
|
+
* verified — via NFC — to match the data printed on the passport. If it cannot
|
|
10
|
+
* be verified (`MISMATCH` or `UNVERIFIABLE`), no business relationship may be
|
|
11
|
+
* established through the remote flow.
|
|
12
|
+
*
|
|
13
|
+
* - `MATCH` — every identity-critical field is equal.
|
|
14
|
+
* - `MISMATCH` — at least one identity-critical field differs.
|
|
15
|
+
* - `UNVERIFIABLE` — one of the two sources is missing, so no comparison is possible.
|
|
16
|
+
*/
|
|
17
|
+
export type ChipVizVerdict = 'MATCH' | 'MISMATCH' | 'UNVERIFIABLE';
|
|
18
|
+
|
|
19
|
+
/** Why a comparison came back `UNVERIFIABLE`. */
|
|
20
|
+
export type ChipVizUnverifiableReason =
|
|
21
|
+
| 'missing-ocr-source'
|
|
22
|
+
| 'missing-chip-source';
|
|
23
|
+
|
|
24
|
+
export interface ChipVizMatchResult {
|
|
25
|
+
verdict: ChipVizVerdict;
|
|
26
|
+
/**
|
|
27
|
+
* Fields that differed (empty on `MATCH` / `UNVERIFIABLE`). Used for
|
|
28
|
+
* diagnostics and server-side review — never surfaced to the user, to avoid
|
|
29
|
+
* coaching fraud.
|
|
30
|
+
*/
|
|
31
|
+
mismatchedFields: Array<keyof DocumentData>;
|
|
32
|
+
/** Present only when `verdict === 'UNVERIFIABLE'`. */
|
|
33
|
+
reason?: ChipVizUnverifiableReason;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ChipVizCompareOptions {
|
|
37
|
+
/**
|
|
38
|
+
* When `true`, a difference in the raw MRZ name (first/last) alone flips the
|
|
39
|
+
* verdict to `MISMATCH`. When `false` (default), name differences are
|
|
40
|
+
* recorded in `mismatchedFields` but do not by themselves cause a mismatch —
|
|
41
|
+
* a single-character OCR slip on a name should not hard-block on its own.
|
|
42
|
+
* The identity-critical fields below always drive the verdict regardless.
|
|
43
|
+
*/
|
|
44
|
+
strictNames?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Raw MRZ ASCII names (OCR-B, before Unicode/DG11 display conversion). The
|
|
47
|
+
* normalized `DocumentData.firstName/lastName` are overwritten with the
|
|
48
|
+
* display form in the read path, so the raw forms must be passed in
|
|
49
|
+
* explicitly for an apples-to-apples comparison.
|
|
50
|
+
*/
|
|
51
|
+
chipName?: { first: string; last: string };
|
|
52
|
+
vizName?: { first: string; last: string };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Identity-critical fields. These are the ICAO check-digit-protected MRZ fields
|
|
57
|
+
* plus the core identity claim; any difference here is a hard mismatch.
|
|
58
|
+
*/
|
|
59
|
+
const IDENTITY_CRITICAL_FIELDS: Array<keyof DocumentData> = [
|
|
60
|
+
'documentNumber',
|
|
61
|
+
'dateOfBirth',
|
|
62
|
+
'dateOfExpiry',
|
|
63
|
+
'nationality',
|
|
64
|
+
'sex',
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/** Uppercase, strip `<` filler and surrounding whitespace. */
|
|
68
|
+
function normalizeField(value: string | null | undefined): string {
|
|
69
|
+
if (!value) return '';
|
|
70
|
+
return value.replace(/</g, '').trim().toUpperCase();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Collapse internal whitespace runs and strip `<` filler, for name comparison. */
|
|
74
|
+
function normalizeName(value: string | null | undefined): string {
|
|
75
|
+
if (!value) return '';
|
|
76
|
+
return value
|
|
77
|
+
.replace(/</g, ' ')
|
|
78
|
+
.replace(/\s+/g, ' ')
|
|
79
|
+
.trim()
|
|
80
|
+
.toUpperCase();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Compare the chip-read (DG1) document data against the OCR-read (printed/VIZ)
|
|
85
|
+
* document data and produce a match verdict.
|
|
86
|
+
*
|
|
87
|
+
* Both inputs are the normalized {@link DocumentData} shape, so they are
|
|
88
|
+
* directly comparable regardless of which read path produced them. Pass raw MRZ
|
|
89
|
+
* names via {@link ChipVizCompareOptions} — the `firstName`/`lastName` on
|
|
90
|
+
* `DocumentData` may already be the Unicode display form, which legitimately
|
|
91
|
+
* diverges between paths and would cause false mismatches.
|
|
92
|
+
*
|
|
93
|
+
* @param chip Chip-read data (from `normalizeFromMRZInfo`), or `undefined` if NFC did not complete.
|
|
94
|
+
* @param viz OCR-read data (from `normalizeFromMRZFields`), or `undefined` if no OCR step ran.
|
|
95
|
+
*/
|
|
96
|
+
export function compareChipToViz(
|
|
97
|
+
chip: DocumentData | undefined,
|
|
98
|
+
viz: DocumentData | undefined,
|
|
99
|
+
options: ChipVizCompareOptions = {}
|
|
100
|
+
): ChipVizMatchResult {
|
|
101
|
+
if (!chip) {
|
|
102
|
+
return {
|
|
103
|
+
verdict: 'UNVERIFIABLE',
|
|
104
|
+
mismatchedFields: [],
|
|
105
|
+
reason: 'missing-chip-source',
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!viz) {
|
|
109
|
+
return {
|
|
110
|
+
verdict: 'UNVERIFIABLE',
|
|
111
|
+
mismatchedFields: [],
|
|
112
|
+
reason: 'missing-ocr-source',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const mismatchedFields: Array<keyof DocumentData> = [];
|
|
117
|
+
|
|
118
|
+
for (const field of IDENTITY_CRITICAL_FIELDS) {
|
|
119
|
+
if (normalizeField(chip[field] as string) !== normalizeField(viz[field] as string)) {
|
|
120
|
+
mismatchedFields.push(field);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Names: compare the raw MRZ ASCII when provided, otherwise fall back to the
|
|
125
|
+
// (possibly display-form) DocumentData names. Recorded always; only flips the
|
|
126
|
+
// verdict when strictNames is enabled.
|
|
127
|
+
const chipFirst = options.chipName?.first ?? chip.firstName;
|
|
128
|
+
const chipLast = options.chipName?.last ?? chip.lastName;
|
|
129
|
+
const vizFirst = options.vizName?.first ?? viz.firstName;
|
|
130
|
+
const vizLast = options.vizName?.last ?? viz.lastName;
|
|
131
|
+
|
|
132
|
+
const firstDiffers = normalizeName(chipFirst) !== normalizeName(vizFirst);
|
|
133
|
+
const lastDiffers = normalizeName(chipLast) !== normalizeName(vizLast);
|
|
134
|
+
|
|
135
|
+
const nameMismatchedFields: Array<keyof DocumentData> = [];
|
|
136
|
+
if (firstDiffers) nameMismatchedFields.push('firstName');
|
|
137
|
+
if (lastDiffers) nameMismatchedFields.push('lastName');
|
|
138
|
+
mismatchedFields.push(...nameMismatchedFields);
|
|
139
|
+
|
|
140
|
+
// Identity-critical fields always drive the verdict; names only when strict.
|
|
141
|
+
const hardMismatch =
|
|
142
|
+
mismatchedFields.some((f) => IDENTITY_CRITICAL_FIELDS.includes(f)) ||
|
|
143
|
+
(options.strictNames === true && nameMismatchedFields.length > 0);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
verdict: hardMismatch ? 'MISMATCH' : 'MATCH',
|
|
147
|
+
mismatchedFields,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Country allow-list check for a workflow step's `allowedCountries`.
|
|
3
|
+
*
|
|
4
|
+
* Semantics (mirrors the server engine): an empty or unset list means ALL
|
|
5
|
+
* countries are allowed; a non-empty list allows ONLY those ICAO-3 codes.
|
|
6
|
+
* Matching is case-insensitive. `allowedCountries` carries any ICAO-3 code
|
|
7
|
+
* (server-driven), so the client treats the values as opaque strings.
|
|
8
|
+
*/
|
|
9
|
+
export function isCountryAllowed(
|
|
10
|
+
issuingState: string | null | undefined,
|
|
11
|
+
allowedCountries: string[] | null | undefined
|
|
12
|
+
): boolean {
|
|
13
|
+
if (!allowedCountries || allowedCountries.length === 0) return true;
|
|
14
|
+
const code = issuingState?.toUpperCase();
|
|
15
|
+
if (!code) return false;
|
|
16
|
+
return allowedCountries.map((c) => c.toUpperCase()).includes(code);
|
|
17
|
+
}
|
|
@@ -3,26 +3,39 @@ import type { DocumentData } from '../Types/documentData';
|
|
|
3
3
|
import type { MRZInfo } from '../EIDReader/lds/icao/mrzInfo';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Expand a 2-digit MRZ year to a 4-digit calendar year
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Expand a 2-digit MRZ year to a 4-digit calendar year, using a century rule
|
|
7
|
+
* appropriate to the kind of date:
|
|
8
|
+
*
|
|
9
|
+
* - `'birth'`: a birth date is never in the future. Any YY greater than the
|
|
10
|
+
* current 2-digit year must be the previous century (e.g. in 2026, "85" →
|
|
11
|
+
* 1985, "20" → 2020). This is the ICAO-style sliding window pegged to "now".
|
|
12
|
+
* - `'expiry'`: a travel/ID document's expiry is recent-past-to-future and no
|
|
13
|
+
* document valid today expired in the 1900s, so expiry years are always 20YY
|
|
14
|
+
* (e.g. "36" → 2036, not 1936 — the previous fixed-threshold bug).
|
|
9
15
|
*/
|
|
10
|
-
function expandYear(yy: string): string {
|
|
16
|
+
function expandYear(yy: string, kind: 'birth' | 'expiry'): string {
|
|
17
|
+
if (kind === 'expiry') return `20${yy}`;
|
|
11
18
|
const n = parseInt(yy, 10);
|
|
12
|
-
|
|
19
|
+
// Current 2-digit year as the sliding pivot (e.g. 26 in 2026). Years after it
|
|
20
|
+
// can't be a birth this century, so they belong to the 1900s.
|
|
21
|
+
const pivot = new Date().getFullYear() % 100;
|
|
22
|
+
return n > pivot ? `19${yy}` : `20${yy}`;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
/**
|
|
16
26
|
* Convert a YYMMDD MRZ date string to ISO 8601 YYYY-MM-DD.
|
|
17
27
|
* Returns null for empty, null, or malformed input.
|
|
18
28
|
*/
|
|
19
|
-
function mrzDateToISO(
|
|
29
|
+
function mrzDateToISO(
|
|
30
|
+
raw: string | null | undefined,
|
|
31
|
+
kind: 'birth' | 'expiry'
|
|
32
|
+
): string | null {
|
|
20
33
|
if (!raw || raw.length !== 6) return null;
|
|
21
34
|
const yy = raw.slice(0, 2);
|
|
22
35
|
const mm = raw.slice(2, 4);
|
|
23
36
|
const dd = raw.slice(4, 6);
|
|
24
37
|
if (!/^\d{6}$/.test(raw)) return null;
|
|
25
|
-
return `${expandYear(yy)}-${mm}-${dd}`;
|
|
38
|
+
return `${expandYear(yy, kind)}-${mm}-${dd}`;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
/**
|
|
@@ -84,8 +97,8 @@ export function normalizeFromMRZInfo(mrz: MRZInfo): DocumentData {
|
|
|
84
97
|
lastName: mrz.getPrimaryIdentifier().trim(),
|
|
85
98
|
firstName: mrz.getSecondaryIdentifier().trim(),
|
|
86
99
|
sex: normalizeSex(mrz.getGender().getStrCode()),
|
|
87
|
-
dateOfBirth: mrzDateToISO(mrz.getDateOfBirth()),
|
|
88
|
-
dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry()),
|
|
100
|
+
dateOfBirth: mrzDateToISO(mrz.getDateOfBirth(), 'birth'),
|
|
101
|
+
dateOfExpiry: mrzDateToISO(mrz.getDateOfExpiry(), 'expiry'),
|
|
89
102
|
};
|
|
90
103
|
}
|
|
91
104
|
|
|
@@ -102,7 +115,7 @@ export function normalizeFromMRZFields(fields: MRZFields): DocumentData {
|
|
|
102
115
|
lastName: clean(fields.lastName) ?? '',
|
|
103
116
|
firstName: clean(fields.firstName) ?? '',
|
|
104
117
|
sex: normalizeSex(fields.sex),
|
|
105
|
-
dateOfBirth: mrzDateToISO(fields.birthDate),
|
|
106
|
-
dateOfExpiry: mrzDateToISO(fields.expirationDate),
|
|
118
|
+
dateOfBirth: mrzDateToISO(fields.birthDate, 'birth'),
|
|
119
|
+
dateOfExpiry: mrzDateToISO(fields.expirationDate, 'expiry'),
|
|
107
120
|
};
|
|
108
121
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Platform, NativeModules, PermissionsAndroid } from 'react-native';
|
|
2
|
+
import { debugError } from './debug.utils';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Coarse geolocation capture for the MASAK Tebliğ No. 32 Article 4/C(1)(e)
|
|
6
|
+
* technical-data requirement. Invoked when a workflow step enables
|
|
7
|
+
* collectGeolocation.
|
|
8
|
+
*
|
|
9
|
+
* Implemented entirely with the platform's native location framework via the
|
|
10
|
+
* SDK's own native modules (iOS Core Location through PermissionModule, Android
|
|
11
|
+
* LocationManager through GeolocationModule) — no third-party module. The host
|
|
12
|
+
* app must declare the OS permission strings (iOS
|
|
13
|
+
* `NSLocationWhenInUseUsageDescription`, Android `ACCESS_COARSE_LOCATION`).
|
|
14
|
+
*
|
|
15
|
+
* Everything is fail-soft: if the native module is missing, services are off,
|
|
16
|
+
* permission is denied, or the fix times out, this returns null and the flow
|
|
17
|
+
* proceeds — the backend treats a missing geolocation as "not provided".
|
|
18
|
+
*
|
|
19
|
+
* @returns "lat,lng" rounded to ~city-block precision (~110 m), or null.
|
|
20
|
+
*/
|
|
21
|
+
export async function captureCoarseGeolocation(
|
|
22
|
+
timeoutMs = 8000
|
|
23
|
+
): Promise<string | null> {
|
|
24
|
+
try {
|
|
25
|
+
if (Platform.OS === 'android') {
|
|
26
|
+
const granted = await requestAndroidLocationPermission();
|
|
27
|
+
if (!granted) return null;
|
|
28
|
+
const mod = NativeModules.GeolocationModule;
|
|
29
|
+
if (!mod?.getCoarseLocation) return null;
|
|
30
|
+
return normalize(
|
|
31
|
+
await withJsBackstop(mod.getCoarseLocation(timeoutMs), timeoutMs)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// iOS: Core Location (via the existing PermissionModule) requests
|
|
36
|
+
// when-in-use authorization itself given the Info.plist usage string.
|
|
37
|
+
const mod = NativeModules.PermissionModule;
|
|
38
|
+
if (!mod?.getCoarseLocation) return null;
|
|
39
|
+
return normalize(
|
|
40
|
+
await withJsBackstop(mod.getCoarseLocation(timeoutMs), timeoutMs)
|
|
41
|
+
);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
debugError('geolocation.utils', 'native capture failed', e);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalize(value: string | null | undefined): string | null {
|
|
49
|
+
if (typeof value !== 'string' || value.length === 0) return null;
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* JS-side backstop: the native side enforces its own timeout, but if a native
|
|
55
|
+
* regression left the promise unsettled, this resolves null so the flow can't
|
|
56
|
+
* hang. The margin keeps the native timeout authoritative on the happy path.
|
|
57
|
+
*/
|
|
58
|
+
async function withJsBackstop(
|
|
59
|
+
native: Promise<string | null>,
|
|
60
|
+
timeoutMs: number
|
|
61
|
+
): Promise<string | null> {
|
|
62
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
63
|
+
const backstop = new Promise<string | null>((resolve) => {
|
|
64
|
+
timer = setTimeout(() => resolve(null), timeoutMs + 2000);
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
return await Promise.race([native, backstop]);
|
|
68
|
+
} finally {
|
|
69
|
+
if (timer) clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Request coarse location permission on Android. The host app must declare
|
|
75
|
+
* ACCESS_COARSE_LOCATION in its manifest. iOS handles its own prompt through
|
|
76
|
+
* Core Location, so this is Android-only.
|
|
77
|
+
*/
|
|
78
|
+
async function requestAndroidLocationPermission(): Promise<boolean> {
|
|
79
|
+
try {
|
|
80
|
+
const result = await PermissionsAndroid.request(
|
|
81
|
+
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION
|
|
82
|
+
);
|
|
83
|
+
return result === PermissionsAndroid.RESULTS.GRANTED;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -18,6 +18,17 @@ const runWithRetry = async <T>(
|
|
|
18
18
|
}
|
|
19
19
|
return result;
|
|
20
20
|
} catch (error) {
|
|
21
|
+
// Non-retryable errors (e.g. a server-instructed session termination,
|
|
22
|
+
// MASAK 4/C(1)(e)) must propagate immediately, with their flags intact —
|
|
23
|
+
// retrying can't help and would just delay surfacing the outcome.
|
|
24
|
+
if (
|
|
25
|
+
error &&
|
|
26
|
+
typeof error === 'object' &&
|
|
27
|
+
((error as { sessionTerminated?: boolean }).sessionTerminated ||
|
|
28
|
+
(error as { nonRetryable?: boolean }).nonRetryable)
|
|
29
|
+
) {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
21
32
|
lastError = error;
|
|
22
33
|
retries++;
|
|
23
34
|
console.error(
|
|
@@ -74,12 +74,35 @@ export class DataUploadService {
|
|
|
74
74
|
gender: this.getGenderEnumType(mrzFields.sex),
|
|
75
75
|
number: mrzFields.documentNumber,
|
|
76
76
|
country: mrzFields.issuingState,
|
|
77
|
+
// Raw ICAO nationality (for the MASAK 4/C(3) risky-country block and the
|
|
78
|
+
// server chip↔VIZ nationality comparison).
|
|
79
|
+
nationality: mrzFields.nationality,
|
|
77
80
|
barcodeValue: scannedDocument.barcodeValue,
|
|
78
81
|
personalNumber: mrzFields.personalNumber || mrzFields.optional1,
|
|
79
82
|
birthDate: mrzUtils.convertMRZDateToISODate(mrzFields.birthDate),
|
|
80
83
|
expiryDate: mrzUtils.convertMRZDateToISODate(mrzFields.expirationDate),
|
|
81
84
|
dataSource: scannedDocument.dataSource,
|
|
82
85
|
mrzText: scannedDocument.mrzText,
|
|
86
|
+
// Chip↔VIZ match (MASAK 4/C(1)(b)). Send the client verdict (advisory)
|
|
87
|
+
// plus the printed/VIZ MRZ comparison fields so the server can
|
|
88
|
+
// independently re-derive the authoritative verdict. Recorded for every
|
|
89
|
+
// EID scan as a security signal; absent on non-EID submissions.
|
|
90
|
+
chipVizMatch: scannedDocument.chipVizMatch,
|
|
91
|
+
vizMrz: scannedDocument.vizMrzFields
|
|
92
|
+
? {
|
|
93
|
+
number: scannedDocument.vizMrzFields.documentNumber,
|
|
94
|
+
birthDate: mrzUtils.convertMRZDateToISODate(
|
|
95
|
+
scannedDocument.vizMrzFields.birthDate
|
|
96
|
+
),
|
|
97
|
+
expiryDate: mrzUtils.convertMRZDateToISODate(
|
|
98
|
+
scannedDocument.vizMrzFields.expirationDate
|
|
99
|
+
),
|
|
100
|
+
nationality: scannedDocument.vizMrzFields.nationality,
|
|
101
|
+
gender: this.getGenderEnumType(scannedDocument.vizMrzFields.sex),
|
|
102
|
+
name: scannedDocument.vizMrzFields.firstName,
|
|
103
|
+
surname: scannedDocument.vizMrzFields.lastName,
|
|
104
|
+
}
|
|
105
|
+
: undefined,
|
|
83
106
|
};
|
|
84
107
|
|
|
85
108
|
debugLog(
|
|
@@ -49,4 +49,12 @@ export interface DocumentData {
|
|
|
49
49
|
* Null if the document does not encode it.
|
|
50
50
|
*/
|
|
51
51
|
dateOfExpiry: string | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Holder's permanent address as read from the chip's DG11 (NFC path only),
|
|
55
|
+
* normalized to a comma-separated line. Null when the chip does not provide
|
|
56
|
+
* it (most passports) or on the OCR path. Used to pre-fill the address step
|
|
57
|
+
* (MASAK 4/C(1)(d)).
|
|
58
|
+
*/
|
|
59
|
+
address?: string | null;
|
|
52
60
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DocumentData } from './documentData';
|
|
2
|
+
import type { ChipVizMatchResult } from '../Libs/chipVizMatch';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* How the display name was derived from the raw MRZ ASCII name.
|
|
@@ -44,4 +45,23 @@ export interface DocumentReadResult {
|
|
|
44
45
|
* face image was successfully obtained.
|
|
45
46
|
*/
|
|
46
47
|
face?: DocumentFaceImage;
|
|
48
|
+
/**
|
|
49
|
+
* Result of comparing this chip read against a prior camera-OCR (printed/VIZ)
|
|
50
|
+
* read, when one is available. Present only on the NFC path in the
|
|
51
|
+
* MASAK 4/C remote-passport flow. The client verdict is advisory — the server
|
|
52
|
+
* re-derives the authoritative verdict from the uploaded fields.
|
|
53
|
+
*/
|
|
54
|
+
chipVizMatch?: ChipVizMatchResult;
|
|
55
|
+
/**
|
|
56
|
+
* Passive Authentication result (ICAO 9303-11 §5.1), present only on the NFC
|
|
57
|
+
* path when the eID step enabled passiveAuth. `integrityStatus` is the
|
|
58
|
+
* on-device DG-hash check (MATCH/TAMPERED/INCOMPLETE); full signature + CSCA
|
|
59
|
+
* verification is performed server-side from the SOD.
|
|
60
|
+
*/
|
|
61
|
+
passiveAuth?: {
|
|
62
|
+
integrityStatus: 'MATCH' | 'TAMPERED' | 'INCOMPLETE' | null;
|
|
63
|
+
/** Raw EF.SOD (base64) for server-side signature + CSCA chaining. */
|
|
64
|
+
sodBase64?: string | null;
|
|
65
|
+
error?: string;
|
|
66
|
+
};
|
|
47
67
|
}
|