@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.
Files changed (51) hide show
  1. package/package.json +1 -2
  2. package/src/components/BrandBar.tsx +137 -0
  3. package/src/components/DialCodePicker.tsx +11 -17
  4. package/src/components/DocumentReviewSide.tsx +34 -14
  5. package/src/components/Icon.tsx +5 -0
  6. package/src/components/KycSheet.tsx +161 -180
  7. package/src/components/MediaSourceSheet.tsx +7 -37
  8. package/src/components/MyazaDateField.tsx +6 -20
  9. package/src/components/MyazaInput.tsx +6 -1
  10. package/src/components/MyazaSelect.tsx +20 -39
  11. package/src/components/PoweredBy.tsx +20 -15
  12. package/src/components/ProgressBar.tsx +91 -0
  13. package/src/components/SandboxBanner.tsx +92 -0
  14. package/src/components/StepHeader.tsx +15 -2
  15. package/src/components/StepIndicator.tsx +145 -31
  16. package/src/components/fonts.ts +23 -0
  17. package/src/components/glass/ChromeGlass.tsx +65 -0
  18. package/src/components/glass/FloatingSheet.tsx +190 -0
  19. package/src/components/glass/GlassSheet.tsx +38 -0
  20. package/src/components/glass/GlassSurface.tsx +31 -3
  21. package/src/components/viewfinder/ImmersiveBottomBar.tsx +28 -17
  22. package/src/components/viewfinder/ImmersiveControls.tsx +26 -14
  23. package/src/components/viewfinder/ViewfinderControls.tsx +29 -7
  24. package/src/config/questionnaire.ts +45 -4
  25. package/src/config/workflowMerge.ts +1 -0
  26. package/src/emrtd/crypto.ts +1 -1
  27. package/src/emrtd/dg1.ts +55 -0
  28. package/src/emrtd/ec-curves.ts +142 -0
  29. package/src/emrtd/ec.ts +196 -0
  30. package/src/emrtd/index.ts +1 -0
  31. package/src/emrtd/mrzKey.ts +21 -1
  32. package/src/emrtd/open.ts +169 -0
  33. package/src/emrtd/pace-params.ts +169 -0
  34. package/src/emrtd/pace.ts +295 -0
  35. package/src/emrtd/secureMessaging.ts +32 -14
  36. package/src/emrtd/session.ts +124 -20
  37. package/src/emrtd/suites.ts +99 -0
  38. package/src/index.ts +1 -0
  39. package/src/lib/step-log.ts +43 -0
  40. package/src/lib/step-window.ts +96 -0
  41. package/src/liveness/useLiveness.ts +1 -1
  42. package/src/screens/IdTypeStep.tsx +15 -2
  43. package/src/screens/NfcStep.tsx +12 -0
  44. package/src/screens/QuestionnaireField.tsx +30 -0
  45. package/src/screens/QuestionnaireStep.tsx +3 -1
  46. package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
  47. package/src/services/deviceMetadata.ts +9 -1
  48. package/src/store/derive.ts +7 -0
  49. package/src/store/kycStore.ts +25 -1
  50. package/src/types/config.ts +22 -0
  51. package/src/types/workflow.ts +10 -0
@@ -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
+ ];
@@ -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
+ }
@@ -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';
@@ -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 };