@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,169 @@
1
+ import { readTlv, readTlvSequence } from './der';
2
+ import { curveForParameterId, type EcCurve } from './ec-curves';
3
+ import { DES_EDE2_SUITE, aesSuite, type CipherSuite } from './suites';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // PACE protocols, domain parameters, and EF.CardAccess (ICAO 9303 Part 11 §9.2).
7
+ //
8
+ // A chip advertises which PACE variants it supports as object identifiers in
9
+ // EF.CardAccess — a small file at the Master File level, readable with NO
10
+ // session at all, which is how a terminal discovers whether the chip speaks
11
+ // PACE and with which parameters.
12
+ //
13
+ // Only Generic Mapping over elliptic curves is implemented. Integrated Mapping
14
+ // and Chip Authentication Mapping are distinct protocols rather than
15
+ // variations, and PACE over finite-field Diffie-Hellman needs the RFC 5114
16
+ // modular groups embedded as constants, where a subtly wrong 2048-bit prime
17
+ // fails in a way that looks like a bad document. Virtually every issued
18
+ // passport offers the elliptic-curve variants, and anything reaching those gaps
19
+ // still reads over BAC.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** How the chip's nonce is mapped onto a fresh generator. */
23
+ export type PaceMapping = 'generic' | 'integrated' | 'chipAuthentication';
24
+
25
+ /** Which key-agreement primitive the mapping runs over. */
26
+ export type PaceKeyAgreement = 'dh' | 'ecdh';
27
+
28
+ /** A PACE variant the chip advertised. */
29
+ export interface PaceProtocol {
30
+ /**
31
+ * The raw OID bytes. Re-sent to the chip and folded into the authentication
32
+ * tokens, so they must be preserved exactly as received.
33
+ */
34
+ readonly oid: Uint8Array;
35
+ readonly mapping: PaceMapping;
36
+ readonly keyAgreement: PaceKeyAgreement;
37
+ readonly suite: CipherSuite;
38
+ }
39
+
40
+ /** Whether this build can actually run the variant. */
41
+ export function isSupportedProtocol(protocol: PaceProtocol): boolean {
42
+ return protocol.mapping === 'generic' && protocol.keyAgreement === 'ecdh';
43
+ }
44
+
45
+ export function describeProtocol(protocol: PaceProtocol): string {
46
+ return `PACE-${protocol.keyAgreement.toUpperCase()}-${protocol.mapping} ${protocol.suite.name}`;
47
+ }
48
+
49
+ /** `0.4.0.127.0.7.2.2.4` — the arc every PACE protocol identifier sits under. */
50
+ const PACE_ARC = [0x04, 0x00, 0x7f, 0x00, 0x07, 0x02, 0x02, 0x04];
51
+
52
+ /** The last-but-one arc component selects mapping + key agreement. */
53
+ const BRANCHES: Record<number, [PaceMapping, PaceKeyAgreement]> = {
54
+ 1: ['generic', 'dh'],
55
+ 2: ['generic', 'ecdh'],
56
+ 3: ['integrated', 'dh'],
57
+ 4: ['integrated', 'ecdh'],
58
+ 6: ['chipAuthentication', 'ecdh'],
59
+ };
60
+
61
+ /** The final component selects the cipher suite. */
62
+ const CIPHERS: Record<number, CipherSuite> = {
63
+ 1: DES_EDE2_SUITE,
64
+ 2: aesSuite(16),
65
+ 3: aesSuite(24),
66
+ 4: aesSuite(32),
67
+ };
68
+
69
+ /**
70
+ * Decode a PACE protocol OID, or null when it is not one.
71
+ *
72
+ * An unknown OID in EF.CardAccess is NORMAL, not an error: the file also
73
+ * carries security information for chip authentication and terminal
74
+ * authentication, which this SDK does not run.
75
+ */
76
+ export function paceProtocolFromOid(oid: Uint8Array): PaceProtocol | null {
77
+ if (oid.length !== PACE_ARC.length + 2) return null;
78
+ for (let i = 0; i < PACE_ARC.length; i++) {
79
+ if (oid[i] !== PACE_ARC[i]) return null;
80
+ }
81
+ const branch = BRANCHES[oid[PACE_ARC.length]!];
82
+ const suite = CIPHERS[oid[PACE_ARC.length + 1]!];
83
+ if (!branch || !suite) return null;
84
+ return { oid: new Uint8Array(oid), mapping: branch[0], keyAgreement: branch[1], suite };
85
+ }
86
+
87
+ /** One PACE offering from the chip. */
88
+ export interface PaceOffer {
89
+ readonly protocol: PaceProtocol;
90
+ /** The standardised domain-parameter id, naming a curve or DH group. */
91
+ readonly parameterId: number | null;
92
+ }
93
+
94
+ /**
95
+ * The PACE offerings advertised in EF.CardAccess, in file order.
96
+ *
97
+ * Returns empty for a chip that advertises no PACE, for a file that cannot be
98
+ * parsed, and for one that is simply absent — all of which mean the same thing
99
+ * to the caller: use BAC.
100
+ */
101
+ export function parseCardAccess(file: Uint8Array): PaceOffer[] {
102
+ const offers: PaceOffer[] = [];
103
+ // The file is a SET (0x31) of SEQUENCEs. Some chips wrap it differently, so
104
+ // accept the SET's contents or a bare run of SEQUENCEs.
105
+ const outer = readTlv(file);
106
+ const body = outer && outer.tag === 0x31 ? outer.value : file;
107
+
108
+ for (const info of readTlvSequence(body)) {
109
+ if (info.tag !== 0x30) continue; // not a SecurityInfo
110
+ const fields = readTlvSequence(info.value);
111
+ const first = fields[0];
112
+ if (!first || first.tag !== 0x06) continue;
113
+
114
+ const protocol = paceProtocolFromOid(first.value);
115
+ if (!protocol) continue; // some other protocol's security info
116
+
117
+ // PACEInfo ::= SEQUENCE { protocol OID, version INTEGER,
118
+ // parameterId INTEGER OPTIONAL }
119
+ const ints = fields.slice(1).filter((f) => f.tag === 0x02);
120
+ const parameterId = ints.length > 1 ? intFromBytes(ints[1]!.value) : null;
121
+ offers.push({ protocol, parameterId });
122
+ }
123
+ return offers;
124
+ }
125
+
126
+ /** A small DER INTEGER as a number. */
127
+ function intFromBytes(bytes: Uint8Array): number {
128
+ let n = 0;
129
+ for (const b of bytes) n = n * 256 + b;
130
+ return n;
131
+ }
132
+
133
+ /** A usable offering plus its resolved curve. */
134
+ export interface SelectedPaceOffer {
135
+ readonly offer: PaceOffer;
136
+ readonly curve: EcCurve;
137
+ }
138
+
139
+ /**
140
+ * The offering to actually attempt, or null when none is usable.
141
+ *
142
+ * Prefers the strongest cipher the chip offers among the variants this build
143
+ * supports, so a chip advertising both AES-256 and 3DES is run at AES-256.
144
+ */
145
+ export function selectPaceOffer(offers: PaceOffer[]): SelectedPaceOffer | null {
146
+ let best: SelectedPaceOffer | null = null;
147
+ for (const offer of offers) {
148
+ if (!isSupportedProtocol(offer.protocol)) continue;
149
+ if (offer.parameterId === null) continue;
150
+ const curve = curveForParameterId(offer.parameterId);
151
+ if (!curve) continue;
152
+ if (!best || offer.protocol.suite.keyLength > best.offer.protocol.suite.keyLength) {
153
+ best = { offer, curve };
154
+ }
155
+ }
156
+ return best;
157
+ }
158
+
159
+ /** Why no offering was usable. Diagnostics only — the read falls back to BAC. */
160
+ export type PaceGap = 'mapping' | 'keyAgreement' | 'domainParameters';
161
+
162
+ export function paceGapFor(offers: PaceOffer[]): PaceGap | null {
163
+ if (offers.length === 0) return null;
164
+ for (const offer of offers) {
165
+ if (offer.protocol.keyAgreement !== 'ecdh') return 'keyAgreement';
166
+ if (offer.protocol.mapping !== 'generic') return 'mapping';
167
+ }
168
+ return 'domainParameters';
169
+ }
@@ -0,0 +1,295 @@
1
+ import { concat, timingSafeEqual, unpadFromBlock } from './bytes';
2
+ import { encodeTlv, findTlv, readTlv } from './der';
3
+ import type { EcCurve } from './ec-curves';
4
+ import {
5
+ bigIntToBytes,
6
+ bytesToBigInt,
7
+ decodePoint,
8
+ encodePoint,
9
+ generator,
10
+ pointAdd,
11
+ pointMultiply,
12
+ randomScalar,
13
+ type EcPoint,
14
+ } from './ec';
15
+ import type { EmrtdPrimitives } from './crypto';
16
+ import { SecureMessagingSession } from './secureMessaging';
17
+ import type { PaceProtocol } from './pace-params';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // PACE, Generic Mapping over elliptic curves (ICAO 9303 Part 11 §4.4).
21
+ //
22
+ // The newer way into a chip. BAC derives its keys straight from the MRZ, so an
23
+ // attacker who photographs the passport page can decrypt a recorded session
24
+ // forever. PACE uses the MRZ only to unlock a fresh random nonce, then runs a
25
+ // Diffie-Hellman exchange over a generator derived from it — so the session
26
+ // keys are new every time and the MRZ alone never reveals them.
27
+ //
28
+ // The handshake is four exchanges after a setup command:
29
+ //
30
+ // 0. MSE:Set AT name the protocol and which password we hold
31
+ // 1. get nonce chip sends a nonce encrypted under the password key
32
+ // 2. map nonce both sides contribute a key; the nonce and the shared
33
+ // secret combine into a fresh generator
34
+ // 3. key agreement a second exchange over THAT generator gives the session
35
+ // secret, and the session keys derive from it
36
+ // 4. mutual auth each side proves it derived the same keys, by MACing
37
+ // the other's public key
38
+ //
39
+ // Steps 1-3 are sent with the command-chaining class byte; step 4 closes the
40
+ // chain. Getting that wrong makes a chip abandon the handshake midway.
41
+ //
42
+ // Mirrors the Flutter SDK's emrtd_pace.dart step for step, so a chip that reads
43
+ // on one platform reads on the other.
44
+ // ---------------------------------------------------------------------------
45
+
46
+ export class PaceError extends Error {
47
+ constructor(
48
+ message: string,
49
+ /**
50
+ * `auth_failed` when the chip refuses the password, which in practice
51
+ * always means a wrong MRZ. Anything else is a protocol or transport fault.
52
+ */
53
+ readonly code: 'auth_failed' | 'read_failed',
54
+ ) {
55
+ super(message);
56
+ this.name = 'PaceError';
57
+ Object.setPrototypeOf(this, PaceError.prototype);
58
+ }
59
+ }
60
+
61
+ /** Data objects inside the dynamic authentication template, by step. */
62
+ const DO = {
63
+ template: 0x7c,
64
+ encryptedNonce: 0x80,
65
+ mappingCommand: 0x81,
66
+ mappingResponse: 0x82,
67
+ keyCommand: 0x83,
68
+ keyResponse: 0x84,
69
+ tokenCommand: 0x85,
70
+ tokenResponse: 0x86,
71
+ publicKey: 0x7f49,
72
+ objectIdentifier: 0x06,
73
+ ecPoint: 0x86,
74
+ } as const;
75
+
76
+ const SW_OK = 0x9000;
77
+
78
+ /** Sends one raw APDU and returns the response data with its status word. */
79
+ export type PaceTransceive = (
80
+ command: Uint8Array,
81
+ ) => Promise<{ data: Uint8Array; statusWord: number }>;
82
+
83
+ /**
84
+ * Run PACE-GM over an elliptic curve and return the secured session.
85
+ *
86
+ * `passwordKey` is the MRZ-derived password key (counter 3), NOT a session key
87
+ * — PACE uses the MRZ only to unlock the chip's nonce.
88
+ */
89
+ export async function runPaceEcdhGm(options: {
90
+ p: EmrtdPrimitives;
91
+ transceive: PaceTransceive;
92
+ protocol: PaceProtocol;
93
+ curve: EcCurve;
94
+ passwordKey: Uint8Array;
95
+ /** Injectable for tests; production draws real randomness. */
96
+ fixed?: { mapPrivate?: bigint; sessionPrivate?: bigint };
97
+ }): Promise<SecureMessagingSession> {
98
+ const { p, transceive, protocol, curve, passwordKey } = options;
99
+ const suite = protocol.suite;
100
+
101
+ // Step 0 — announce the protocol and which password we are using. No Le:
102
+ // MSE:Set AT returns no data, and appending one makes it a case-4 command
103
+ // the chip answers 0x6700 "wrong length" to.
104
+ await exchange(
105
+ transceive,
106
+ concat(
107
+ new Uint8Array([0x00, 0x22, 0xc1, 0xa4]),
108
+ body(
109
+ [encodeTlv(0x80, protocol.oid), encodeTlv(0x83, new Uint8Array([0x01]))],
110
+ false,
111
+ ),
112
+ ),
113
+ 'select',
114
+ );
115
+
116
+ // Step 1 — the chip's nonce, encrypted under the password key. A chip that
117
+ // refuses here has decided the password is wrong.
118
+ const nonceReply = await generalAuthenticate(
119
+ transceive,
120
+ encodeTlv(DO.template, new Uint8Array(0)),
121
+ false,
122
+ 'nonce',
123
+ );
124
+ // No unpadding: the nonce is whole blocks of random, not a padded message.
125
+ const nonce = suite.decrypt(p, passwordKey, expect(nonceReply, DO.encryptedNonce, 'nonce'));
126
+
127
+ // Step 2 — map the nonce onto a fresh generator. Each side sends an ephemeral
128
+ // public key; the shared point plus the nonce give a generator neither side
129
+ // chose alone.
130
+ const mapPrivate = options.fixed?.mapPrivate ?? randomScalar(curve, p.randomBytes);
131
+ const mapPublic = pointMultiply(curve, mapPrivate, generator(curve));
132
+ const mapReply = await generalAuthenticate(
133
+ transceive,
134
+ encodeTlv(DO.template, encodeTlv(DO.mappingCommand, encodePoint(curve, mapPublic))),
135
+ false,
136
+ 'mapping',
137
+ );
138
+ const chipMapPoint = decodeChipPoint(curve, expect(mapReply, DO.mappingResponse, 'mapping'));
139
+
140
+ const shared = pointMultiply(curve, mapPrivate, chipMapPoint);
141
+ const mappedGenerator = pointAdd(
142
+ curve,
143
+ pointMultiply(curve, bytesToBigInt(nonce) % curve.n, generator(curve)),
144
+ shared,
145
+ );
146
+ if (mappedGenerator.inf) {
147
+ throw new PaceError('The mapped generator was degenerate.', 'read_failed');
148
+ }
149
+
150
+ // Step 3 — the real key agreement, over the mapped generator.
151
+ const sessionPrivate = options.fixed?.sessionPrivate ?? randomScalar(curve, p.randomBytes);
152
+ const sessionPublic = pointMultiply(curve, sessionPrivate, mappedGenerator);
153
+ const keyReply = await generalAuthenticate(
154
+ transceive,
155
+ encodeTlv(DO.template, encodeTlv(DO.keyCommand, encodePoint(curve, sessionPublic))),
156
+ false,
157
+ 'key agreement',
158
+ );
159
+ const chipSessionPoint = decodeChipPoint(
160
+ curve,
161
+ expect(keyReply, DO.keyResponse, 'key agreement'),
162
+ );
163
+
164
+ // Identical public keys mean the exchange contributed nothing.
165
+ if (chipSessionPoint.x === sessionPublic.x && chipSessionPoint.y === sessionPublic.y) {
166
+ throw new PaceError('The chip echoed our public key.', 'read_failed');
167
+ }
168
+
169
+ const agreed = pointMultiply(curve, sessionPrivate, chipSessionPoint);
170
+ if (agreed.inf) {
171
+ throw new PaceError('Key agreement produced no secret.', 'read_failed');
172
+ }
173
+ // The shared secret is the agreed point's x coordinate, left-padded to the
174
+ // curve's field width: a short value must not shorten the key derivation.
175
+ const secret = bigIntToBytes(agreed.x, curve.byteLen);
176
+
177
+ const ksEnc = suite.deriveKey(p, secret, 1);
178
+ const ksMac = suite.deriveKey(p, secret, 2);
179
+
180
+ // Step 4 — each side MACs the OTHER side's public key. Matching tokens prove
181
+ // both derived the same session keys without either revealing them.
182
+ const ourToken = suite.token(p, ksMac, tokenInput(curve, protocol.oid, chipSessionPoint));
183
+ const tokenReply = await generalAuthenticate(
184
+ transceive,
185
+ encodeTlv(DO.template, encodeTlv(DO.tokenCommand, ourToken)),
186
+ true,
187
+ 'authentication',
188
+ );
189
+ const chipToken = expect(tokenReply, DO.tokenResponse, 'authentication');
190
+ const expected = suite.token(p, ksMac, tokenInput(curve, protocol.oid, sessionPublic));
191
+ if (!timingSafeEqual(chipToken, expected)) {
192
+ throw new PaceError(
193
+ 'The chip proved a different key. The document details do not match.',
194
+ 'auth_failed',
195
+ );
196
+ }
197
+
198
+ // A PACE session starts its counter at zero, unlike BAC's nonce-derived one.
199
+ return new SecureMessagingSession(p, { ksEnc, ksMac }, new Uint8Array(suite.blockSize), suite);
200
+ }
201
+
202
+ /**
203
+ * GENERAL AUTHENTICATE. Steps before the last are sent with the chaining class
204
+ * byte, which tells the chip more of the same command is coming.
205
+ */
206
+ async function generalAuthenticate(
207
+ transceive: PaceTransceive,
208
+ data: Uint8Array,
209
+ last: boolean,
210
+ step: string,
211
+ ): Promise<Uint8Array> {
212
+ const command = concat(
213
+ new Uint8Array([last ? 0x00 : 0x10, 0x86, 0x00, 0x00]),
214
+ body([data], true),
215
+ );
216
+ const response = await exchange(transceive, command, step);
217
+ const template = readTlv(response);
218
+ if (!template || template.tag !== DO.template) {
219
+ throw new PaceError(`The ${step} reply was not authentication data.`, 'read_failed');
220
+ }
221
+ return template.value;
222
+ }
223
+
224
+ async function exchange(
225
+ transceive: PaceTransceive,
226
+ command: Uint8Array,
227
+ step: string,
228
+ ): Promise<Uint8Array> {
229
+ const { data, statusWord } = await transceive(command);
230
+ if (statusWord !== SW_OK) {
231
+ // 0x6300 and 0x63CX are how chips say the password was wrong; some older
232
+ // ones use the latter specifically for bad session data.
233
+ const wrongPassword = statusWord === 0x6300 || (statusWord & 0xfff0) === 0x63c0;
234
+ throw new PaceError(
235
+ // The STEP is in both messages on purpose. A refusal at 'authentication'
236
+ // means our token did not match, which points at the password key
237
+ // derivation; one at 'nonce' means the chip would not even start. Without
238
+ // the step both read as "wrong MRZ" and send debugging the wrong way —
239
+ // which is exactly what happened the first time this ran against a real
240
+ // passport.
241
+ wrongPassword
242
+ ? `The chip rejected the document details at the ${step} step.`
243
+ : `The chip returned ${statusWord.toString(16).padStart(4, '0')} at the ${step} step.`,
244
+ wrongPassword ? 'auth_failed' : 'read_failed',
245
+ );
246
+ }
247
+ return data;
248
+ }
249
+
250
+ function expect(template: Uint8Array, tag: number, step: string): Uint8Array {
251
+ const found = findTlv(template, tag);
252
+ if (!found) {
253
+ throw new PaceError(`The ${step} reply was missing its data.`, 'read_failed');
254
+ }
255
+ return found.value;
256
+ }
257
+
258
+ /** A chip-supplied point, validated before any arithmetic touches it. */
259
+ function decodeChipPoint(curve: EcCurve, bytes: Uint8Array): EcPoint {
260
+ const pt = decodePoint(curve, bytes);
261
+ // A point off the curve or at infinity would leak our private scalar to a
262
+ // chip that chose it deliberately, so it is refused rather than used.
263
+ if (!pt) throw new PaceError('The chip sent an invalid key.', 'read_failed');
264
+ return pt;
265
+ }
266
+
267
+ /**
268
+ * The object each side MACs to prove it holds the session keys: the protocol
269
+ * identifier and the other party's public point.
270
+ */
271
+ function tokenInput(curve: EcCurve, oid: Uint8Array, pt: EcPoint): Uint8Array {
272
+ return encodeTlv(
273
+ DO.publicKey,
274
+ concat(encodeTlv(DO.objectIdentifier, oid), encodeTlv(DO.ecPoint, encodePoint(curve, pt))),
275
+ );
276
+ }
277
+
278
+ /**
279
+ * Lc, body, and Le only when the command actually returns data.
280
+ *
281
+ * ISO 7816 distinguishes a command that sends data (case 3) from one that
282
+ * sends and receives (case 4) by the presence of that trailing byte, and a
283
+ * chip asked for a response it has none of replies 0x6700.
284
+ */
285
+ function body(parts: Uint8Array[], expectsResponse: boolean): Uint8Array {
286
+ const joined = concat(...parts);
287
+ return concat(
288
+ new Uint8Array([joined.length]),
289
+ joined,
290
+ expectsResponse ? new Uint8Array([0x00]) : new Uint8Array(0),
291
+ );
292
+ }
293
+
294
+ /** Re-exported so the session can strip padding from a PACE-decrypted body. */
295
+ export { unpadFromBlock };
@@ -1,13 +1,7 @@
1
1
  import { concat, padToBlock, timingSafeEqual, unpadFromBlock } from './bytes';
2
2
  import { encodeTlv, readTlvSequence } from './der';
3
- import {
4
- BLOCK_SIZE,
5
- decrypt3Des,
6
- encrypt3Des,
7
- macWithPadding,
8
- type EmrtdPrimitives,
9
- type SessionKeys,
10
- } from './crypto';
3
+ import type { EmrtdPrimitives, SessionKeys } from './crypto';
4
+ import { DES_EDE2_SUITE, type CipherSuite } from './suites';
11
5
 
12
6
  // ---------------------------------------------------------------------------
13
7
  // Secure messaging (ICAO 9303 Part 11 §9.8).
@@ -61,6 +55,13 @@ export class SecureMessagingSession {
61
55
  private readonly p: EmrtdPrimitives,
62
56
  private readonly keys: SessionKeys,
63
57
  initialSsc: Uint8Array,
58
+ /**
59
+ * Which cipher the session runs on. BAC is always 3DES (the default, so
60
+ * every existing call site is unchanged); a PACE session may have
61
+ * negotiated AES, which changes the block size, the padding unit, the
62
+ * counter width and how the IV is derived.
63
+ */
64
+ private readonly suite: CipherSuite = DES_EDE2_SUITE,
64
65
  ) {
65
66
  this.ssc = new Uint8Array(initialSsc);
66
67
  }
@@ -80,15 +81,21 @@ export class SecureMessagingSession {
80
81
  // The class byte gains the secure-messaging bits, and the MAC is computed
81
82
  // over the PADDED header — a detail the standard is easy to misread.
82
83
  const cla = apdu.cla | 0x0c;
84
+ const block = this.suite.blockSize;
83
85
  const header = padToBlock(
84
86
  new Uint8Array([cla, apdu.ins, apdu.p1, apdu.p2]),
85
- BLOCK_SIZE,
87
+ block,
86
88
  );
87
89
 
88
90
  const parts: Uint8Array[] = [];
89
91
 
90
92
  if (apdu.data && apdu.data.length > 0) {
91
- const encrypted = encrypt3Des(this.p, this.keys.ksEnc, padToBlock(apdu.data, BLOCK_SIZE));
93
+ const encrypted = this.suite.encrypt(
94
+ this.p,
95
+ this.keys.ksEnc,
96
+ padToBlock(apdu.data, block),
97
+ this.ssc,
98
+ );
92
99
  // The leading 0x01 is the padding-content indicator: "ISO 9797-1 method
93
100
  // 2 was used". Omitting it is a common source of chips rejecting DO'87'.
94
101
  parts.push(encodeTlv(DO87, concat(new Uint8Array([0x01]), encrypted)));
@@ -103,7 +110,11 @@ export class SecureMessagingSession {
103
110
  }
104
111
 
105
112
  const body = concat(...parts);
106
- const mac = macWithPadding(this.p, this.keys.ksMac, concat(this.ssc, header, body));
113
+ const mac = this.suite.mac(
114
+ this.p,
115
+ this.keys.ksMac,
116
+ padToBlock(concat(this.ssc, header, body), block),
117
+ );
107
118
  const checksum = encodeTlv(DO8E, mac);
108
119
 
109
120
  const payload = concat(body, checksum);
@@ -140,7 +151,11 @@ export class SecureMessagingSession {
140
151
  this.ssc,
141
152
  ...objects.map((o) => encodeTlv(o.tag, o.value)),
142
153
  );
143
- const expected = macWithPadding(this.p, this.keys.ksMac, macInput);
154
+ const expected = this.suite.mac(
155
+ this.p,
156
+ this.keys.ksMac,
157
+ padToBlock(macInput, this.suite.blockSize),
158
+ );
144
159
  if (!timingSafeEqual(expected, checksum.value)) {
145
160
  throw new SecureMessagingError('The response failed its integrity check.');
146
161
  }
@@ -161,12 +176,15 @@ export class SecureMessagingSession {
161
176
  // does not.
162
177
  const body =
163
178
  encrypted.tag === DO87 ? encrypted.value.subarray(1) : encrypted.value;
164
- if (body.length === 0 || body.length % BLOCK_SIZE !== 0) {
179
+ if (body.length === 0 || body.length % this.suite.blockSize !== 0) {
165
180
  throw new SecureMessagingError('The response ciphertext was misaligned.');
166
181
  }
167
182
 
168
183
  return {
169
- data: unpadFromBlock(decrypt3Des(this.p, this.keys.ksEnc, body), BLOCK_SIZE),
184
+ data: unpadFromBlock(
185
+ this.suite.decrypt(this.p, this.keys.ksEnc, body, this.ssc),
186
+ this.suite.blockSize,
187
+ ),
170
188
  statusWord,
171
189
  };
172
190
  }