@myazahq/kyc-sdk-react-native 2.2.0 → 2.4.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/package.json +1 -2
- package/src/components/BrandBar.tsx +137 -0
- package/src/components/DialCodePicker.tsx +11 -17
- package/src/components/DocumentReviewSide.tsx +34 -14
- package/src/components/Icon.tsx +5 -0
- package/src/components/KycSheet.tsx +161 -180
- package/src/components/MediaSourceSheet.tsx +7 -37
- package/src/components/MyazaDateField.tsx +6 -20
- package/src/components/MyazaInput.tsx +6 -1
- package/src/components/MyazaSelect.tsx +20 -39
- package/src/components/PoweredBy.tsx +20 -15
- package/src/components/ProgressBar.tsx +91 -0
- package/src/components/SandboxBanner.tsx +92 -0
- package/src/components/StepHeader.tsx +15 -2
- package/src/components/StepIndicator.tsx +145 -31
- package/src/components/fonts.ts +23 -0
- package/src/components/glass/ChromeGlass.tsx +65 -0
- package/src/components/glass/FloatingSheet.tsx +190 -0
- package/src/components/glass/GlassSheet.tsx +38 -0
- package/src/components/glass/GlassSurface.tsx +31 -3
- package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
- package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
- package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
- package/src/config/questionnaire.ts +45 -4
- package/src/config/workflowMerge.ts +1 -0
- package/src/emrtd/crypto.ts +1 -1
- package/src/emrtd/dg1.ts +55 -0
- package/src/emrtd/ec-curves.ts +142 -0
- package/src/emrtd/ec.ts +196 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/mrzKey.ts +21 -1
- package/src/emrtd/open.ts +169 -0
- package/src/emrtd/pace-params.ts +169 -0
- package/src/emrtd/pace.ts +295 -0
- package/src/emrtd/secureMessaging.ts +32 -14
- package/src/emrtd/session.ts +124 -20
- package/src/emrtd/suites.ts +99 -0
- package/src/index.ts +1 -0
- package/src/lib/step-log.ts +43 -0
- package/src/lib/step-window.ts +96 -0
- package/src/liveness/useLiveness.ts +1 -1
- package/src/screens/IdTypeStep.tsx +15 -2
- package/src/screens/NfcStep.tsx +12 -0
- package/src/screens/QuestionnaireField.tsx +30 -0
- package/src/screens/QuestionnaireStep.tsx +3 -1
- package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
- package/src/services/deviceMetadata.ts +9 -1
- package/src/store/derive.ts +7 -0
- package/src/store/kycStore.ts +25 -1
- package/src/types/config.ts +22 -0
- package/src/types/workflow.ts +10 -0
package/src/emrtd/dg1.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { fromBase64 } from './bytes';
|
|
2
|
+
import { findTlvDeep } from './der';
|
|
3
|
+
import { parseMrz, type MrzScan } from '../mrz/parse';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// THE CHIP'S OWN MRZ.
|
|
7
|
+
//
|
|
8
|
+
// DG1 is the machine-readable zone exactly as the ISSUING STATE wrote it, and
|
|
9
|
+
// passive authentication hashes it against the signed security object. It is
|
|
10
|
+
// the strongest copy of the holder's details that exists on the document.
|
|
11
|
+
//
|
|
12
|
+
// The camera scan is a GUESS at the same characters, and it is not a safe
|
|
13
|
+
// substitute for display. Measured on a real Nigerian passport read on a Galaxy
|
|
14
|
+
// S24: the on-device recogniser read the first '<' of the '<<' surname
|
|
15
|
+
// separator as 'K', so `INGWE<<RICHARD<UNIMKE` arrived as `INGWEK<RICHARD…`.
|
|
16
|
+
// The split then never fired at the surname boundary and landed in the trailing
|
|
17
|
+
// filler instead, yielding lastName "INGWEK RICHARD UNIMKE" and firstName
|
|
18
|
+
// "KKKK" — displayed under a caption promising we had read the secure chip.
|
|
19
|
+
//
|
|
20
|
+
// TD3 carries NO check digit over the name field (only line 2 is protected), so
|
|
21
|
+
// that corruption passes `parseMrz` validation silently. There is no way to
|
|
22
|
+
// detect it from the scan alone, which is exactly why the chip has to be the
|
|
23
|
+
// source once we hold it.
|
|
24
|
+
//
|
|
25
|
+
// Reuses `parseMrz` rather than parsing here: DG1's value IS the continuous
|
|
26
|
+
// 88/90-character string that function already takes, and a second MRZ parser
|
|
27
|
+
// would be one more thing to drift.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/** DG1's MRZ lives under tag 5F1F, inside the 0x61 template. */
|
|
31
|
+
const TAG_MRZ = 0x5f1f;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The MRZ read off the chip, or null when DG1 is absent, malformed, or not a
|
|
35
|
+
* size `parseMrz` recognises.
|
|
36
|
+
*
|
|
37
|
+
* Null is a normal outcome the caller falls back from, never an error: the
|
|
38
|
+
* read itself already succeeded and its bytes still go to the server, which
|
|
39
|
+
* parses DG1 authoritatively regardless of what this returns.
|
|
40
|
+
*/
|
|
41
|
+
export function parseDg1(dg1Base64: string | undefined | null): MrzScan | null {
|
|
42
|
+
if (!dg1Base64) return null;
|
|
43
|
+
try {
|
|
44
|
+
const bytes = fromBase64(dg1Base64);
|
|
45
|
+
const mrz = findTlvDeep(bytes, TAG_MRZ);
|
|
46
|
+
if (!mrz) return null;
|
|
47
|
+
let text = '';
|
|
48
|
+
for (const byte of mrz.value) text += String.fromCharCode(byte);
|
|
49
|
+
return parseMrz(text);
|
|
50
|
+
} catch {
|
|
51
|
+
// Display-path only. A malformed DG1 must never take down the success
|
|
52
|
+
// screen of a read that otherwise worked.
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Standardised elliptic-curve domain parameters (ICAO 9303 / BSI TR-03110).
|
|
3
|
+
//
|
|
4
|
+
// A chip names its PACE curve by a standardised parameter id rather than
|
|
5
|
+
// sending the parameters, so both sides must agree on this table. Every curve
|
|
6
|
+
// here is a prime-field short-Weierstrass curve (y² = x³ + ax + b), so one set
|
|
7
|
+
// of point arithmetic (ec.ts) covers all of them.
|
|
8
|
+
//
|
|
9
|
+
// The constants are transcribed from the authoritative sources — NIST FIPS
|
|
10
|
+
// 186-4 for the secp/P- curves and RFC 5639 for the brainpool curves — and are
|
|
11
|
+
// verified in ec.test.ts, which asserts G lies on each curve and that n·G is
|
|
12
|
+
// the point at infinity. A transcription typo fails that test rather than a
|
|
13
|
+
// passport.
|
|
14
|
+
//
|
|
15
|
+
// The six curves below are the ones issued eMRTDs actually use. The other
|
|
16
|
+
// standardised ids (the 192/224-bit curves, DH groups) are left unmapped: an
|
|
17
|
+
// unknown id resolves to null, which the caller treats exactly like a chip that
|
|
18
|
+
// does not offer PACE — it reads over BAC. So the coverage gap can only ever
|
|
19
|
+
// cost a fallback, never a failed read.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** A prime-field short-Weierstrass curve and its generator. */
|
|
23
|
+
export interface EcCurve {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
/** Field prime. */
|
|
26
|
+
readonly p: bigint;
|
|
27
|
+
readonly a: bigint;
|
|
28
|
+
readonly b: bigint;
|
|
29
|
+
/** Generator coordinates. */
|
|
30
|
+
readonly gx: bigint;
|
|
31
|
+
readonly gy: bigint;
|
|
32
|
+
/** Group order. */
|
|
33
|
+
readonly n: bigint;
|
|
34
|
+
/** Field width in bytes — the fixed length x/y coordinates encode to. */
|
|
35
|
+
readonly byteLen: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const hex = (s: string): bigint => BigInt(`0x${s.replace(/\s+/g, '')}`);
|
|
39
|
+
|
|
40
|
+
function curve(
|
|
41
|
+
name: string,
|
|
42
|
+
byteLen: number,
|
|
43
|
+
p: string,
|
|
44
|
+
a: string,
|
|
45
|
+
b: string,
|
|
46
|
+
gx: string,
|
|
47
|
+
gy: string,
|
|
48
|
+
n: string,
|
|
49
|
+
): EcCurve {
|
|
50
|
+
return { name, byteLen, p: hex(p), a: hex(a), b: hex(b), gx: hex(gx), gy: hex(gy), n: hex(n) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const secp256r1 = curve(
|
|
54
|
+
'secp256r1',
|
|
55
|
+
32,
|
|
56
|
+
'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF',
|
|
57
|
+
'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC',
|
|
58
|
+
'5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B',
|
|
59
|
+
'6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296',
|
|
60
|
+
'4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5',
|
|
61
|
+
'FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const brainpoolP256r1 = curve(
|
|
65
|
+
'brainpoolP256r1',
|
|
66
|
+
32,
|
|
67
|
+
'A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377',
|
|
68
|
+
'7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9',
|
|
69
|
+
'26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6',
|
|
70
|
+
'8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262',
|
|
71
|
+
'547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997',
|
|
72
|
+
'A9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7',
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const secp384r1 = curve(
|
|
76
|
+
'secp384r1',
|
|
77
|
+
48,
|
|
78
|
+
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF',
|
|
79
|
+
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC',
|
|
80
|
+
'B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF',
|
|
81
|
+
'AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7',
|
|
82
|
+
'3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F',
|
|
83
|
+
'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const brainpoolP384r1 = curve(
|
|
87
|
+
'brainpoolP384r1',
|
|
88
|
+
48,
|
|
89
|
+
'8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53',
|
|
90
|
+
'7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826',
|
|
91
|
+
'04A8C7DD22CE28268B39B55416F0447C2FB77DE107DCD2A62E880EA53EEB62D57CB4390295DBC9943AB78696FA504C11',
|
|
92
|
+
'1D1C64F068CF45FFA2A63A81B7C13F6B8847A3E77EF14FE3DB7FCAFE0CBD10E8E826E03436D646AAEF87B2E247D4AF1E',
|
|
93
|
+
'8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129280E4646217791811142820341263C5315',
|
|
94
|
+
'8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const brainpoolP512r1 = curve(
|
|
98
|
+
'brainpoolP512r1',
|
|
99
|
+
64,
|
|
100
|
+
'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3',
|
|
101
|
+
'7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA',
|
|
102
|
+
'3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CADC083E67984050B75EBAE5DD2809BD638016F723',
|
|
103
|
+
'81AEE4BDD82ED9645A21322E9C4C6A9385ED9F70B5D916C1B43B62EEF4D0098EFF3B1F78E2D0D48D50D1687B93B97D5F7C6D5047406A5E688B352209BCB9F822',
|
|
104
|
+
'7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F8111B2DCDE494A5F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892',
|
|
105
|
+
'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069',
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const secp521r1 = curve(
|
|
109
|
+
'secp521r1',
|
|
110
|
+
66,
|
|
111
|
+
'01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
|
|
112
|
+
'01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC',
|
|
113
|
+
'0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00',
|
|
114
|
+
'00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66',
|
|
115
|
+
'011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650',
|
|
116
|
+
'01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409',
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/** ICAO standardised parameter id → curve, for the curves this build runs. */
|
|
120
|
+
const BY_ID: Record<number, EcCurve> = {
|
|
121
|
+
12: secp256r1,
|
|
122
|
+
13: brainpoolP256r1,
|
|
123
|
+
15: secp384r1,
|
|
124
|
+
16: brainpoolP384r1,
|
|
125
|
+
17: brainpoolP512r1,
|
|
126
|
+
18: secp521r1,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** The curve for a standardised parameter id, or null when unmapped. */
|
|
130
|
+
export function curveForParameterId(id: number): EcCurve | null {
|
|
131
|
+
return BY_ID[id] ?? null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Exposed for tests — every curve this build claims to support. */
|
|
135
|
+
export const ALL_CURVES: readonly EcCurve[] = [
|
|
136
|
+
secp256r1,
|
|
137
|
+
brainpoolP256r1,
|
|
138
|
+
secp384r1,
|
|
139
|
+
brainpoolP384r1,
|
|
140
|
+
brainpoolP512r1,
|
|
141
|
+
secp521r1,
|
|
142
|
+
];
|
package/src/emrtd/ec.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { EcCurve } from './ec-curves';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Elliptic-curve point arithmetic over a prime field, in BigInt.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS IS IN TYPESCRIPT while the block ciphers are native. The native side
|
|
7
|
+
// exposes 3DES/AES/SHA/CMAC because those are audited, constant-time, hardware
|
|
8
|
+
// -accelerated platform primitives. It exposes no EC point arithmetic, and
|
|
9
|
+
// adding it would mean writing and shipping matching Swift and Kotlin — two
|
|
10
|
+
// more implementations of the same maths, on the path that authenticates a
|
|
11
|
+
// passport. One implementation, checked against the curve's own published
|
|
12
|
+
// generator and order (ec.test.ts), is the smaller risk.
|
|
13
|
+
//
|
|
14
|
+
// The scalar multiply is a fixed-sequence Montgomery ladder: every bit of the
|
|
15
|
+
// scalar performs the same add-then-double regardless of its value, so the
|
|
16
|
+
// operation sequence does not depend on the secret. That is as far as constant
|
|
17
|
+
// time goes here — BigInt itself is variable-time, so this is not hardened
|
|
18
|
+
// against a local timing attacker. It does not need to be: the private scalar
|
|
19
|
+
// is ephemeral, used for exactly one handshake with a chip held against the
|
|
20
|
+
// phone, and discarded.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** An affine point, or the point at infinity (`inf`). */
|
|
24
|
+
export interface EcPoint {
|
|
25
|
+
readonly x: bigint;
|
|
26
|
+
readonly y: bigint;
|
|
27
|
+
readonly inf: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const INFINITY: EcPoint = { x: 0n, y: 0n, inf: true };
|
|
31
|
+
|
|
32
|
+
export function point(x: bigint, y: bigint): EcPoint {
|
|
33
|
+
return { x, y, inf: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Least non-negative residue — `%` alone keeps the sign of the dividend. */
|
|
37
|
+
function mod(a: bigint, p: bigint): bigint {
|
|
38
|
+
const r = a % p;
|
|
39
|
+
return r < 0n ? r + p : r;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Modular inverse by the extended Euclidean algorithm.
|
|
44
|
+
*
|
|
45
|
+
* Throws when the value is not invertible, which for a prime field means it was
|
|
46
|
+
* congruent to zero. Callers reach this only through point arithmetic that has
|
|
47
|
+
* already excluded the cases where that happens.
|
|
48
|
+
*/
|
|
49
|
+
export function modInverse(value: bigint, p: bigint): bigint {
|
|
50
|
+
let [old_r, r] = [mod(value, p), p];
|
|
51
|
+
let [old_s, s] = [1n, 0n];
|
|
52
|
+
while (r !== 0n) {
|
|
53
|
+
const q = old_r / r;
|
|
54
|
+
[old_r, r] = [r, old_r - q * r];
|
|
55
|
+
[old_s, s] = [s, old_s - q * s];
|
|
56
|
+
}
|
|
57
|
+
if (old_r !== 1n) throw new Error('value is not invertible');
|
|
58
|
+
return mod(old_s, p);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether the point satisfies y² = x³ + ax + b over the curve's field. */
|
|
62
|
+
export function isOnCurve(c: EcCurve, pt: EcPoint): boolean {
|
|
63
|
+
if (pt.inf) return false;
|
|
64
|
+
if (pt.x < 0n || pt.x >= c.p || pt.y < 0n || pt.y >= c.p) return false;
|
|
65
|
+
const lhs = mod(pt.y * pt.y, c.p);
|
|
66
|
+
const rhs = mod(pt.x * pt.x * pt.x + c.a * pt.x + c.b, c.p);
|
|
67
|
+
return lhs === rhs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function pointDouble(c: EcCurve, pt: EcPoint): EcPoint {
|
|
71
|
+
if (pt.inf) return INFINITY;
|
|
72
|
+
// A point with y = 0 is its own inverse, so doubling it lands on infinity.
|
|
73
|
+
if (pt.y === 0n) return INFINITY;
|
|
74
|
+
const lambda = mod(
|
|
75
|
+
(3n * pt.x * pt.x + c.a) * modInverse(2n * pt.y, c.p),
|
|
76
|
+
c.p,
|
|
77
|
+
);
|
|
78
|
+
const x = mod(lambda * lambda - 2n * pt.x, c.p);
|
|
79
|
+
return point(x, mod(lambda * (pt.x - x) - pt.y, c.p));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function pointAdd(c: EcCurve, a: EcPoint, b: EcPoint): EcPoint {
|
|
83
|
+
if (a.inf) return b;
|
|
84
|
+
if (b.inf) return a;
|
|
85
|
+
if (a.x === b.x) {
|
|
86
|
+
// Same x: either the same point (double) or inverses (sum is infinity).
|
|
87
|
+
return mod(a.y + b.y, c.p) === 0n ? INFINITY : pointDouble(c, a);
|
|
88
|
+
}
|
|
89
|
+
const lambda = mod((b.y - a.y) * modInverse(b.x - a.x, c.p), c.p);
|
|
90
|
+
const x = mod(lambda * lambda - a.x - b.x, c.p);
|
|
91
|
+
return point(x, mod(lambda * (a.x - x) - a.y, c.p));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scalar multiplication by a Montgomery ladder.
|
|
96
|
+
*
|
|
97
|
+
* Both branches of every bit do the same work, so the sequence of field
|
|
98
|
+
* operations is independent of the scalar's bits (see the header note on how
|
|
99
|
+
* far that guarantee goes). The ladder starts at the scalar's own bit width
|
|
100
|
+
* rather than the curve's, which is safe here because the scalar is uniformly
|
|
101
|
+
* random in [1, n-1] and never a low-entropy value whose bit length would leak
|
|
102
|
+
* something meaningful.
|
|
103
|
+
*/
|
|
104
|
+
export function pointMultiply(c: EcCurve, k: bigint, pt: EcPoint): EcPoint {
|
|
105
|
+
if (k <= 0n || pt.inf) return INFINITY;
|
|
106
|
+
let r0: EcPoint = INFINITY;
|
|
107
|
+
let r1: EcPoint = pt;
|
|
108
|
+
for (let i = BigInt(k.toString(2).length) - 1n; i >= 0n; i--) {
|
|
109
|
+
if (((k >> i) & 1n) === 0n) {
|
|
110
|
+
r1 = pointAdd(c, r0, r1);
|
|
111
|
+
r0 = pointDouble(c, r0);
|
|
112
|
+
} else {
|
|
113
|
+
r0 = pointAdd(c, r0, r1);
|
|
114
|
+
r1 = pointDouble(c, r1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return r0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The curve's generator as a point. */
|
|
121
|
+
export function generator(c: EcCurve): EcPoint {
|
|
122
|
+
return point(c.gx, c.gy);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Encoding ────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** Big-endian bytes → integer. */
|
|
128
|
+
export function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
129
|
+
let n = 0n;
|
|
130
|
+
for (const b of bytes) n = (n << 8n) | BigInt(b);
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Integer → fixed-width big-endian bytes.
|
|
136
|
+
*
|
|
137
|
+
* The width is fixed on purpose: a shared secret whose x coordinate happens to
|
|
138
|
+
* start with a zero byte must still derive the same-length key material, and a
|
|
139
|
+
* variable-length encoding there silently produces the wrong session keys on
|
|
140
|
+
* roughly one handshake in 256.
|
|
141
|
+
*/
|
|
142
|
+
export function bigIntToBytes(value: bigint, width: number): Uint8Array {
|
|
143
|
+
const out = new Uint8Array(width);
|
|
144
|
+
let v = value;
|
|
145
|
+
for (let i = width - 1; i >= 0 && v > 0n; i--) {
|
|
146
|
+
out[i] = Number(v & 0xffn);
|
|
147
|
+
v >>= 8n;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Uncompressed point encoding: 0x04 || X || Y, each coordinate field-width. */
|
|
153
|
+
export function encodePoint(c: EcCurve, pt: EcPoint): Uint8Array {
|
|
154
|
+
if (pt.inf) throw new Error('cannot encode the point at infinity');
|
|
155
|
+
const out = new Uint8Array(1 + c.byteLen * 2);
|
|
156
|
+
out[0] = 0x04;
|
|
157
|
+
out.set(bigIntToBytes(pt.x, c.byteLen), 1);
|
|
158
|
+
out.set(bigIntToBytes(pt.y, c.byteLen), 1 + c.byteLen);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Decode an uncompressed point, returning null when the bytes are not a valid
|
|
164
|
+
* point on the curve.
|
|
165
|
+
*
|
|
166
|
+
* Validation is the security-critical part, not a formality: a chip that picks
|
|
167
|
+
* a point off the curve (or a small-order point on a related curve) can learn
|
|
168
|
+
* our private scalar from how we respond, so anything that does not verify is
|
|
169
|
+
* refused rather than used. Compressed encodings are rejected too, since PACE
|
|
170
|
+
* mandates the uncompressed form and accepting more would only widen what an
|
|
171
|
+
* attacker can hand us.
|
|
172
|
+
*/
|
|
173
|
+
export function decodePoint(c: EcCurve, bytes: Uint8Array): EcPoint | null {
|
|
174
|
+
if (bytes.length !== 1 + c.byteLen * 2 || bytes[0] !== 0x04) return null;
|
|
175
|
+
const pt = point(
|
|
176
|
+
bytesToBigInt(bytes.subarray(1, 1 + c.byteLen)),
|
|
177
|
+
bytesToBigInt(bytes.subarray(1 + c.byteLen)),
|
|
178
|
+
);
|
|
179
|
+
return isOnCurve(c, pt) ? pt : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A uniformly random private scalar in [1, n-1].
|
|
184
|
+
*
|
|
185
|
+
* Rejection sampling rather than a modulo: reducing a random integer mod n
|
|
186
|
+
* biases the result toward small values, and biased nonces are how ECDSA keys
|
|
187
|
+
* have historically been recovered. The loop is bounded because each draw
|
|
188
|
+
* succeeds with probability very close to 1 for every curve here.
|
|
189
|
+
*/
|
|
190
|
+
export function randomScalar(c: EcCurve, randomBytes: (n: number) => Uint8Array): bigint {
|
|
191
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
192
|
+
const candidate = bytesToBigInt(randomBytes(c.byteLen));
|
|
193
|
+
if (candidate > 0n && candidate < c.n) return candidate;
|
|
194
|
+
}
|
|
195
|
+
throw new Error('could not draw a private scalar');
|
|
196
|
+
}
|
package/src/emrtd/index.ts
CHANGED
|
@@ -26,5 +26,6 @@ export {
|
|
|
26
26
|
nfcStageLabel,
|
|
27
27
|
nfcStageProgress,
|
|
28
28
|
} from './stages';
|
|
29
|
+
export { parseDg1 } from './dg1';
|
|
29
30
|
export { decodeChipImage, isNfcAvailable, nfcUnavailableReason } from './native';
|
|
30
31
|
export { cancelChipRead, readPassportChip, type ChipReadOptions } from './read';
|
package/src/emrtd/mrzKey.ts
CHANGED
|
@@ -48,7 +48,27 @@ export function mrzKeySeedInput(fields: MrzKeyFields): string {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Kseed: the first 16 bytes of SHA-1 over the MRZ information. */
|
|
51
|
+
/** Kseed for BAC: the first 16 bytes of SHA-1 over the MRZ information. */
|
|
52
52
|
export function keySeed(p: EmrtdPrimitives, fields: MrzKeyFields): Uint8Array {
|
|
53
53
|
return p.sha1(fromAscii(mrzKeySeedInput(fields))).subarray(0, 16);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The seed PACE derives its password key from: the FULL SHA-1 digest, NOT
|
|
58
|
+
* truncated to 16 bytes.
|
|
59
|
+
*
|
|
60
|
+
* This is the one place the two protocols disagree about the same hash, and it
|
|
61
|
+
* is silent when wrong: a truncated seed yields a valid-looking K_π, the
|
|
62
|
+
* handshake runs to its last step, and the chip answers 0x6300 — which reads
|
|
63
|
+
* exactly like a mistyped MRZ rather than a derivation bug. It cost this SDK a
|
|
64
|
+
* real passport read to find (the same document opened over BAC moments later,
|
|
65
|
+
* proving the MRZ was right).
|
|
66
|
+
*
|
|
67
|
+
* ICAO 9303 Part 11 keeps the two apart: §9.7.3 truncates for BAC's Kseed,
|
|
68
|
+
* while §9.7.2 feeds the whole digest to KDF_π. JMRTD encodes the same split as
|
|
69
|
+
* `computeKeySeedForBAC(… truncate: true)` vs `computeKeySeedForPACE(…
|
|
70
|
+
* truncate: false)`.
|
|
71
|
+
*/
|
|
72
|
+
export function paceKeySeed(p: EmrtdPrimitives, fields: MrzKeyFields): Uint8Array {
|
|
73
|
+
return p.sha1(fromAscii(mrzKeySeedInput(fields)));
|
|
74
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from './bytes';
|
|
2
|
+
import type { EmrtdPrimitives, MrzKeyFields } from './crypto';
|
|
3
|
+
import { paceKeySeed } from './crypto';
|
|
4
|
+
import { curveForParameterId } from './ec-curves';
|
|
5
|
+
import { runPaceEcdhGm, PaceError, type PaceTransceive } from './pace';
|
|
6
|
+
import {
|
|
7
|
+
describeProtocol,
|
|
8
|
+
paceGapFor,
|
|
9
|
+
parseCardAccess,
|
|
10
|
+
selectPaceOffer,
|
|
11
|
+
} from './pace-params';
|
|
12
|
+
import type { SecureMessagingSession } from './secureMessaging';
|
|
13
|
+
import type { EmrtdTransport } from './session';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Choosing how to get into the chip.
|
|
17
|
+
//
|
|
18
|
+
// A chip may accept BAC, PACE, or both, and this decides which is tried first.
|
|
19
|
+
//
|
|
20
|
+
// The standard prefers PACE, and so should we eventually: BAC derives its keys
|
|
21
|
+
// from the MRZ alone, so anyone who photographs the passport page can decrypt a
|
|
22
|
+
// recorded session afterwards, while PACE agrees fresh keys every time.
|
|
23
|
+
//
|
|
24
|
+
// But this SDK's BAC has been reading real passports for a long time and its
|
|
25
|
+
// PACE has not read any, so the default here is deliberately the other way
|
|
26
|
+
// round: BAC first, PACE only when BAC is refused. In that order PACE can only
|
|
27
|
+
// ever ADD documents we can read — chips that have retired BAC — and can never
|
|
28
|
+
// take away one that already worked.
|
|
29
|
+
//
|
|
30
|
+
// PACE has since been confirmed against a real document — a Nigerian e-passport
|
|
31
|
+
// on 2026-08-11, over PACE-ECDH-GM with AES-256 on brainpoolP256r1, on both an
|
|
32
|
+
// iPhone 16 Pro Max and a Galaxy S24, with passive authentication passing and
|
|
33
|
+
// the DG2 portrait read. So the code is no longer unproven.
|
|
34
|
+
//
|
|
35
|
+
// The ordering STAYS BAC-first anyway, which is the part worth explaining:
|
|
36
|
+
// "confirmed on one document" is not "confirmed on the population". BAC has
|
|
37
|
+
// read every passport this SDK has ever seen; PACE has read one model of one
|
|
38
|
+
// issuer's. Going PACE-first would put the less-travelled path in front of
|
|
39
|
+
// every document in the world to buy a property (forward secrecy against a
|
|
40
|
+
// recorded session) that matters far less than reading the passport at all.
|
|
41
|
+
//
|
|
42
|
+
// PACE still runs — as the fallback, where it can only ever ADD documents we
|
|
43
|
+
// can read, namely chips that have retired BAC. Revisit when PACE has spanned
|
|
44
|
+
// several issuers, not before.
|
|
45
|
+
//
|
|
46
|
+
// Mirrors the Flutter SDK's emrtd_open.dart, including this reasoning.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Which access protocol is tried first.
|
|
51
|
+
*
|
|
52
|
+
* `false` (the shipping default) means BAC first, PACE only if BAC is refused.
|
|
53
|
+
* `true` reverses it, which is how PACE gets exercised against a real chip: a
|
|
54
|
+
* passport that accepts BAC would otherwise never reach the PACE code at all.
|
|
55
|
+
* Set it to `true` temporarily to test PACE against a document; do not ship it.
|
|
56
|
+
*
|
|
57
|
+
* Safe either way — whichever goes first, the other still runs as the fallback,
|
|
58
|
+
* so this cannot turn a readable document into an unreadable one.
|
|
59
|
+
*/
|
|
60
|
+
export const PREFER_PACE = false;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Why a session ended up on the protocol it did.
|
|
64
|
+
*
|
|
65
|
+
* The protocol name alone cannot answer the question that matters while PACE is
|
|
66
|
+
* new: a chip reading over BAC may never have OFFERED PACE, or may have offered
|
|
67
|
+
* it and had our implementation fail. Those call for opposite responses — one
|
|
68
|
+
* is nothing to do, the other is a bug — so the reason is recorded rather than
|
|
69
|
+
* inferred.
|
|
70
|
+
*/
|
|
71
|
+
export type PaceOutcome =
|
|
72
|
+
/** PACE opened the session. */
|
|
73
|
+
| 'used'
|
|
74
|
+
/** The chip published no EF.CardAccess: it does not speak PACE at all. */
|
|
75
|
+
| 'notOffered'
|
|
76
|
+
/** EF.CardAccess offers only variants this build cannot run. */
|
|
77
|
+
| 'unsupportedVariant'
|
|
78
|
+
/** PACE was attempted and did not complete. THIS is the one worth chasing. */
|
|
79
|
+
| 'failed'
|
|
80
|
+
/** Not tried — BAC succeeded first. */
|
|
81
|
+
| 'notAttempted';
|
|
82
|
+
|
|
83
|
+
export interface AccessResult {
|
|
84
|
+
sm: SecureMessagingSession;
|
|
85
|
+
/** Which protocol secured the session. */
|
|
86
|
+
chipAuth: 'bac' | 'pace';
|
|
87
|
+
outcome: PaceOutcome;
|
|
88
|
+
/** The negotiated protocol when PACE was used, or why it was not. */
|
|
89
|
+
detail?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** EF.CardAccess: at the Master File, readable with no session at all. */
|
|
93
|
+
const EF_CARD_ACCESS = 0x011c;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read EF.CardAccess, or null when the chip does not offer it — which simply
|
|
97
|
+
* means BAC. Absent, unreadable, and unparseable all mean the same thing to
|
|
98
|
+
* the caller, so none of them throw.
|
|
99
|
+
*/
|
|
100
|
+
export async function readCardAccess(transport: EmrtdTransport): Promise<Uint8Array | null> {
|
|
101
|
+
try {
|
|
102
|
+
// SELECT the Master File, then EF.CardAccess by identifier.
|
|
103
|
+
const mf = await transport.transceive(
|
|
104
|
+
toBase64(new Uint8Array([0x00, 0xa4, 0x00, 0x0c, 0x02, 0x3f, 0x00])),
|
|
105
|
+
);
|
|
106
|
+
if (mf.statusWord !== 0x9000) return null;
|
|
107
|
+
const select = await transport.transceive(
|
|
108
|
+
toBase64(
|
|
109
|
+
new Uint8Array([0x00, 0xa4, 0x02, 0x0c, 0x02, (EF_CARD_ACCESS >> 8) & 0xff, EF_CARD_ACCESS & 0xff]),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
if (select.statusWord !== 0x9000) return null;
|
|
113
|
+
const read = await transport.transceive(
|
|
114
|
+
toBase64(new Uint8Array([0x00, 0xb0, 0x00, 0x00, 0x00])),
|
|
115
|
+
);
|
|
116
|
+
if (read.statusWord !== 0x9000) return null;
|
|
117
|
+
const data = fromBase64(read.data);
|
|
118
|
+
return data.length > 0 ? data : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null; // absent or unreadable — the caller falls back to BAC
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Try PACE. Resolves with the session when it worked, or reports why it did
|
|
126
|
+
* not — a `null` result always means "fall back to BAC", never a failed read.
|
|
127
|
+
*/
|
|
128
|
+
export async function tryPace(
|
|
129
|
+
p: EmrtdPrimitives,
|
|
130
|
+
transport: EmrtdTransport,
|
|
131
|
+
mrz: MrzKeyFields,
|
|
132
|
+
): Promise<{ sm: SecureMessagingSession; detail: string } | { outcome: PaceOutcome; detail?: string }> {
|
|
133
|
+
const file = await readCardAccess(transport);
|
|
134
|
+
if (!file) return { outcome: 'notOffered' };
|
|
135
|
+
|
|
136
|
+
const offers = parseCardAccess(file);
|
|
137
|
+
const selected = selectPaceOffer(offers);
|
|
138
|
+
if (!selected) {
|
|
139
|
+
return { outcome: 'unsupportedVariant', detail: paceGapFor(offers) ?? undefined };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { offer, curve } = selected;
|
|
143
|
+
// The MRZ unlocks the chip's nonce rather than the session itself: counter 3
|
|
144
|
+
// is the password key, where BAC uses 1 and 2 for its session keys. The seed
|
|
145
|
+
// is the UNTRUNCATED SHA-1 — see paceKeySeed for why that distinction matters
|
|
146
|
+
// and how it fails when it is wrong.
|
|
147
|
+
const passwordKey = offer.protocol.suite.deriveKey(p, paceKeySeed(p, mrz), 3);
|
|
148
|
+
|
|
149
|
+
const transceive: PaceTransceive = async (command) => {
|
|
150
|
+
const res = await transport.transceive(toBase64(command));
|
|
151
|
+
return { data: fromBase64(res.data), statusWord: res.statusWord };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const sm = await runPaceEcdhGm({
|
|
155
|
+
p,
|
|
156
|
+
transceive,
|
|
157
|
+
protocol: offer.protocol,
|
|
158
|
+
curve,
|
|
159
|
+
passwordKey,
|
|
160
|
+
});
|
|
161
|
+
return { sm, detail: `${describeProtocol(offer.protocol)} params=${offer.parameterId}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether a parameter id resolves to a curve this build runs. Test seam. */
|
|
165
|
+
export function supportsParameterId(id: number): boolean {
|
|
166
|
+
return curveForParameterId(id) !== null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { PaceError };
|