@ukeyfe/react-native-nfc-litecard 1.0.1 → 1.0.3

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/dist/crypto.js CHANGED
@@ -16,6 +16,10 @@ exports.aesDecrypt = aesDecrypt;
16
16
  exports.aesEncrypt = aesEncrypt;
17
17
  exports.rotateLeft8 = rotateLeft8;
18
18
  exports.arraysEqual = arraysEqual;
19
+ exports.aesEcbEncrypt = aesEcbEncrypt;
20
+ exports.generateCmacSubkeys = generateCmacSubkeys;
21
+ exports.aesCmac = aesCmac;
22
+ exports.deriveSessionKey = deriveSessionKey;
19
23
  exports.generateRandom16 = generateRandom16;
20
24
  const crypto_js_1 = __importDefault(require("crypto-js"));
21
25
  // ---------------------------------------------------------------------------
@@ -30,7 +34,7 @@ function passwordToAesKey(password) {
30
34
  const hash = crypto_js_1.default.SHA256(String(safePassword)).toString();
31
35
  const key = new Uint8Array(16);
32
36
  for (let i = 0; i < 16; i++) {
33
- key[i] = parseInt(hash.substr(i * 2, 2), 16);
37
+ key[i] = parseInt(hash.substring(i * 2, i * 2 + 2), 16);
34
38
  }
35
39
  return key;
36
40
  }
@@ -46,7 +50,7 @@ function aesDecrypt(key, data, iv) {
46
50
  const hex = decrypted.toString();
47
51
  const result = new Uint8Array(16);
48
52
  for (let i = 0; i < 16; i++) {
49
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
53
+ result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
50
54
  }
51
55
  return result;
52
56
  }
@@ -63,7 +67,7 @@ function aesEncrypt(key, data, iv) {
63
67
  const hex = encrypted.ciphertext.toString();
64
68
  const result = new Uint8Array(hex.length / 2);
65
69
  for (let i = 0; i < result.length; i++) {
66
- result[i] = parseInt(hex.substr(i * 2, 2), 16);
70
+ result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
67
71
  }
68
72
  return result;
69
73
  }
@@ -94,6 +98,158 @@ function arraysEqual(a, b) {
94
98
  return true;
95
99
  }
96
100
  // ---------------------------------------------------------------------------
101
+ // AES-128 ECB (single block, for CMAC subkey generation)
102
+ // ---------------------------------------------------------------------------
103
+ /** AES-128 ECB encrypt a single 16-byte block. */
104
+ function aesEcbEncrypt(key, data) {
105
+ const keyWords = crypto_js_1.default.lib.WordArray.create(key);
106
+ const dataWords = crypto_js_1.default.lib.WordArray.create(data);
107
+ const encrypted = crypto_js_1.default.AES.encrypt(dataWords, keyWords, {
108
+ mode: crypto_js_1.default.mode.ECB,
109
+ padding: crypto_js_1.default.pad.NoPadding,
110
+ });
111
+ const hex = encrypted.ciphertext.toString();
112
+ const result = new Uint8Array(16);
113
+ for (let i = 0; i < 16; i++) {
114
+ result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
115
+ }
116
+ return result;
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // AES-CMAC (NIST SP 800-38B)
120
+ // ---------------------------------------------------------------------------
121
+ const CONST_RB = 0x87; // constant for 128-bit block cipher
122
+ /** Left-shift a 16-byte array by 1 bit. */
123
+ function shiftLeft1(data) {
124
+ const result = new Uint8Array(16);
125
+ let carry = 0;
126
+ for (let i = 15; i >= 0; i--) {
127
+ const shifted = (data[i] << 1) | carry;
128
+ result[i] = shifted & 0xff;
129
+ carry = (data[i] & 0x80) ? 1 : 0;
130
+ }
131
+ return result;
132
+ }
133
+ /** XOR two 16-byte arrays. */
134
+ function xor16(a, b) {
135
+ const result = new Uint8Array(16);
136
+ for (let i = 0; i < 16; i++) {
137
+ result[i] = a[i] ^ b[i];
138
+ }
139
+ return result;
140
+ }
141
+ /**
142
+ * Generate CMAC subkeys K1 and K2 (NIST 800-38B §6.1).
143
+ */
144
+ function generateCmacSubkeys(key) {
145
+ const zero = new Uint8Array(16);
146
+ const L = aesEcbEncrypt(key, zero);
147
+ let k1 = shiftLeft1(L);
148
+ if (L[0] & 0x80) {
149
+ k1[15] ^= CONST_RB;
150
+ }
151
+ let k2 = shiftLeft1(k1);
152
+ if (k1[0] & 0x80) {
153
+ k2[15] ^= CONST_RB;
154
+ }
155
+ return { k1, k2 };
156
+ }
157
+ /**
158
+ * Compute AES-CMAC (NIST 800-38B §6.2).
159
+ * Returns full 16-byte MAC.
160
+ */
161
+ function aesCmac(key, message) {
162
+ const { k1, k2 } = generateCmacSubkeys(key);
163
+ const n = message.length === 0 ? 1 : Math.ceil(message.length / 16);
164
+ const lastBlockComplete = message.length > 0 && message.length % 16 === 0;
165
+ // Prepare last block
166
+ const lastBlock = new Uint8Array(16);
167
+ if (lastBlockComplete) {
168
+ // Complete: XOR with K1
169
+ const offset = (n - 1) * 16;
170
+ for (let i = 0; i < 16; i++) {
171
+ lastBlock[i] = message[offset + i] ^ k1[i];
172
+ }
173
+ }
174
+ else {
175
+ // Incomplete: pad with 10* then XOR with K2
176
+ const offset = (n - 1) * 16;
177
+ const remaining = message.length - offset;
178
+ for (let i = 0; i < 16; i++) {
179
+ if (i < remaining) {
180
+ lastBlock[i] = message[offset + i];
181
+ }
182
+ else if (i === remaining) {
183
+ lastBlock[i] = 0x80;
184
+ }
185
+ else {
186
+ lastBlock[i] = 0x00;
187
+ }
188
+ }
189
+ for (let i = 0; i < 16; i++) {
190
+ lastBlock[i] ^= k2[i];
191
+ }
192
+ }
193
+ // CBC-MAC
194
+ const iv0 = new Uint8Array(16);
195
+ let x = iv0;
196
+ for (let i = 0; i < n - 1; i++) {
197
+ const block = new Uint8Array(message.slice(i * 16, (i + 1) * 16));
198
+ const y = xor16(x, block);
199
+ x = aesEcbEncrypt(key, y);
200
+ }
201
+ const y = xor16(x, lastBlock);
202
+ return aesEcbEncrypt(key, y);
203
+ }
204
+ /**
205
+ * Derive SesAuthMACKey from authentication key and random numbers.
206
+ * (MF0AES(H)20 §8.8.1)
207
+ *
208
+ * SV2 = 5Ah||A5h||00h||01h||00h||80h||RndA[15..14]||(RndA[13..8] XOR RndB[15..10])||RndB[9..0]||RndA[7..0]
209
+ * SesAuthMACKey = AES-CMAC(Kx, SV2)
210
+ */
211
+ function deriveSessionKey(authKey, rndA, rndB) {
212
+ const sv2 = new Uint8Array(32);
213
+ // Header: 5Ah A5h 00h 01h 00h 80h
214
+ sv2[0] = 0x5a;
215
+ sv2[1] = 0xa5;
216
+ sv2[2] = 0x00;
217
+ sv2[3] = 0x01;
218
+ sv2[4] = 0x00;
219
+ sv2[5] = 0x80;
220
+ // RndA[15..14] (2 bytes)
221
+ sv2[6] = rndA[15];
222
+ sv2[7] = rndA[14];
223
+ // RndA[13..8] XOR RndB[15..10] (6 bytes)
224
+ sv2[8] = rndA[13] ^ rndB[15];
225
+ sv2[9] = rndA[12] ^ rndB[14];
226
+ sv2[10] = rndA[11] ^ rndB[13];
227
+ sv2[11] = rndA[10] ^ rndB[12];
228
+ sv2[12] = rndA[9] ^ rndB[11];
229
+ sv2[13] = rndA[8] ^ rndB[10];
230
+ // RndB[9..0] (10 bytes)
231
+ sv2[14] = rndB[9];
232
+ sv2[15] = rndB[8];
233
+ sv2[16] = rndB[7];
234
+ sv2[17] = rndB[6];
235
+ sv2[18] = rndB[5];
236
+ sv2[19] = rndB[4];
237
+ sv2[20] = rndB[3];
238
+ sv2[21] = rndB[2];
239
+ sv2[22] = rndB[1];
240
+ sv2[23] = rndB[0];
241
+ // RndA[7..0] (8 bytes)
242
+ sv2[24] = rndA[7];
243
+ sv2[25] = rndA[6];
244
+ sv2[26] = rndA[5];
245
+ sv2[27] = rndA[4];
246
+ sv2[28] = rndA[3];
247
+ sv2[29] = rndA[2];
248
+ sv2[30] = rndA[1];
249
+ sv2[31] = rndA[0];
250
+ return aesCmac(authKey, sv2);
251
+ }
252
+ // ---------------------------------------------------------------------------
97
253
  // Secure random
98
254
  // ---------------------------------------------------------------------------
99
255
  /**
@@ -104,18 +260,12 @@ function arraysEqual(a, b) {
104
260
  * `Math.random()` with a console warning if the API is missing.
105
261
  */
106
262
  function generateRandom16() {
107
- const arr = new Uint8Array(16);
108
- if (typeof globalThis.crypto !== 'undefined' &&
109
- typeof globalThis.crypto.getRandomValues === 'function') {
110
- globalThis.crypto.getRandomValues(arr);
111
- }
112
- else {
113
- console.warn('[nfc-litecard] crypto.getRandomValues is not available. ' +
114
- 'Falling back to Math.random() which is NOT cryptographically secure. ' +
115
- 'Consider adding the react-native-get-random-values polyfill.');
116
- for (let i = 0; i < 16; i++) {
117
- arr[i] = Math.floor(Math.random() * 256);
118
- }
263
+ if (typeof globalThis.crypto === 'undefined' ||
264
+ typeof globalThis.crypto.getRandomValues !== 'function') {
265
+ throw new Error('[nfc-litecard] crypto.getRandomValues is not available. ' +
266
+ 'Please add the react-native-get-random-values polyfill or use Hermes >= 0.72.');
119
267
  }
268
+ const arr = new Uint8Array(16);
269
+ globalThis.crypto.getRandomValues(arr);
120
270
  return arr;
121
271
  }
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
5
5
  */
6
- export { ResultCode, type NfcResult, nfcResultRetryCountExhausted, } from './types';
7
- export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, } from './reader';
6
+ export { NfcStatusCode, type NfcResult, nfcResultRetryCountExhausted, } from './types';
7
+ export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, getCardVersion, readOriginality, } from './reader';
8
8
  export { initializeCard, updateCard, updatePassword, writeUserNickname, resetCard, } from './writer';
9
9
  export { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, } from './nfc-core';
10
10
  export { DEFAULT_PIN_RETRY_COUNT } from './constants';
package/dist/index.js CHANGED
@@ -5,12 +5,12 @@
5
5
  * NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.DEFAULT_PIN_RETRY_COUNT = exports.clearNfcOperationCancelledByCleanup = exports.getNfcOperationCancelledByCleanupTimestamp = exports.consumeNfcOperationCancelledByCleanup = exports.markNfcOperationCancelledByCleanup = exports.releaseNfcOperationLock = exports.isNfcOperationLocked = exports.resetCard = exports.writeUserNickname = exports.updatePassword = exports.updateCard = exports.initializeCard = exports.cardInfoToJson = exports.resetRetryCountTo10 = exports.readMnemonicRetryCount = exports.readUserNickname = exports.readMnemonic = exports.checkCard = exports.nfcResultRetryCountExhausted = exports.ResultCode = void 0;
8
+ exports.DEFAULT_PIN_RETRY_COUNT = exports.clearNfcOperationCancelledByCleanup = exports.getNfcOperationCancelledByCleanupTimestamp = exports.consumeNfcOperationCancelledByCleanup = exports.markNfcOperationCancelledByCleanup = exports.releaseNfcOperationLock = exports.isNfcOperationLocked = exports.resetCard = exports.writeUserNickname = exports.updatePassword = exports.updateCard = exports.initializeCard = exports.readOriginality = exports.getCardVersion = exports.cardInfoToJson = exports.resetRetryCountTo10 = exports.readMnemonicRetryCount = exports.readUserNickname = exports.readMnemonic = exports.checkCard = exports.nfcResultRetryCountExhausted = exports.NfcStatusCode = void 0;
9
9
  // ---------------------------------------------------------------------------
10
- // Unified types & constants (consumers can use a single ResultCode)
10
+ // Unified types & constants (consumers can use a single NfcStatusCode)
11
11
  // ---------------------------------------------------------------------------
12
12
  var types_1 = require("./types");
13
- Object.defineProperty(exports, "ResultCode", { enumerable: true, get: function () { return types_1.ResultCode; } });
13
+ Object.defineProperty(exports, "NfcStatusCode", { enumerable: true, get: function () { return types_1.NfcStatusCode; } });
14
14
  Object.defineProperty(exports, "nfcResultRetryCountExhausted", { enumerable: true, get: function () { return types_1.nfcResultRetryCountExhausted; } });
15
15
  // ---------------------------------------------------------------------------
16
16
  // Reader API
@@ -22,6 +22,8 @@ Object.defineProperty(exports, "readUserNickname", { enumerable: true, get: func
22
22
  Object.defineProperty(exports, "readMnemonicRetryCount", { enumerable: true, get: function () { return reader_1.readMnemonicRetryCount; } });
23
23
  Object.defineProperty(exports, "resetRetryCountTo10", { enumerable: true, get: function () { return reader_1.resetRetryCountTo10; } });
24
24
  Object.defineProperty(exports, "cardInfoToJson", { enumerable: true, get: function () { return reader_1.cardInfoToJson; } });
25
+ Object.defineProperty(exports, "getCardVersion", { enumerable: true, get: function () { return reader_1.getCardVersion; } });
26
+ Object.defineProperty(exports, "readOriginality", { enumerable: true, get: function () { return reader_1.readOriginality; } });
25
27
  // ---------------------------------------------------------------------------
26
28
  // Writer API
27
29
  // ---------------------------------------------------------------------------
@@ -23,9 +23,16 @@ export declare function markNfcOperationCancelledByCleanup(): void;
23
23
  export declare function consumeNfcOperationCancelledByCleanup(): boolean;
24
24
  export declare function getNfcOperationCancelledByCleanupTimestamp(): number;
25
25
  export declare function clearNfcOperationCancelledByCleanup(): void;
26
+ /** Clear CMAC session state. Called on NFC release. */
27
+ export declare function clearCmacSession(): void;
26
28
  /**
27
29
  * Send a command to the NFC tag and return the response.
28
30
  *
31
+ * When CMAC session is active:
32
+ * - Appends 8-byte CMAC to outgoing commands
33
+ * - Verifies and strips 8-byte CMAC from responses
34
+ * - Increments CmdCtr after each command-response pair
35
+ *
29
36
  * Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
30
37
  */
31
38
  export declare function transceive(command: number[], _timeoutMs?: number, retryCount?: number): Promise<number[]>;
@@ -53,6 +60,8 @@ export declare function releaseNfcTech(forceLongDelay?: boolean): Promise<void>;
53
60
  * @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
54
61
  */
55
62
  export declare function authenticate(key: Uint8Array): Promise<void>;
63
+ /** FAST_READ pages 0x08–0x27 (user memory). */
64
+ export declare function readUserMemory(): Promise<Uint8Array>;
56
65
  /**
57
66
  * Decrement the on-card retry counter by 1 (clamped to 0).
58
67
  * Must be called within an active NFC session (between requestNfcTech and releaseNfcTech).
package/dist/nfc-core.js CHANGED
@@ -53,10 +53,12 @@ exports.markNfcOperationCancelledByCleanup = markNfcOperationCancelledByCleanup;
53
53
  exports.consumeNfcOperationCancelledByCleanup = consumeNfcOperationCancelledByCleanup;
54
54
  exports.getNfcOperationCancelledByCleanupTimestamp = getNfcOperationCancelledByCleanupTimestamp;
55
55
  exports.clearNfcOperationCancelledByCleanup = clearNfcOperationCancelledByCleanup;
56
+ exports.clearCmacSession = clearCmacSession;
56
57
  exports.transceive = transceive;
57
58
  exports.requestNfcTech = requestNfcTech;
58
59
  exports.releaseNfcTech = releaseNfcTech;
59
60
  exports.authenticate = authenticate;
61
+ exports.readUserMemory = readUserMemory;
60
62
  exports.decrementRetryCountInSession = decrementRetryCountInSession;
61
63
  exports.writeRetryCountInSession = writeRetryCountInSession;
62
64
  const react_native_nfc_manager_1 = __importStar(require("react-native-nfc-manager"));
@@ -117,23 +119,93 @@ function clearNfcOperationCancelledByCleanup() {
117
119
  nfcCancelledByCleanupTimestamp = 0;
118
120
  }
119
121
  // ===========================================================================
122
+ // CMAC secure messaging session state
123
+ // ===========================================================================
124
+ let cmacSessionKey = null;
125
+ let cmacCmdCtr = 0;
126
+ /** Clear CMAC session state. Called on NFC release. */
127
+ function clearCmacSession() {
128
+ cmacSessionKey = null;
129
+ cmacCmdCtr = 0;
130
+ }
131
+ /**
132
+ * Compute the 8-byte truncated CMAC for an NFC command.
133
+ * Input: CmdCtr (2 bytes LE) || CmdCode (1 byte) || Arguments
134
+ */
135
+ function computeCommandCmac(sessionKey, cmdCtr, command) {
136
+ const ctrLo = cmdCtr & 0xff;
137
+ const ctrHi = (cmdCtr >> 8) & 0xff;
138
+ const input = new Uint8Array(2 + command.length);
139
+ input[0] = ctrLo;
140
+ input[1] = ctrHi;
141
+ input.set(command, 2);
142
+ const mac = (0, crypto_1.aesCmac)(sessionKey, input);
143
+ return mac.slice(0, 8); // truncate to 8 bytes
144
+ }
145
+ /**
146
+ * Compute the 8-byte truncated CMAC for an NFC response.
147
+ * Input: CmdCtr (2 bytes LE) || ResponseData
148
+ */
149
+ function computeResponseCmac(sessionKey, cmdCtr, responseData) {
150
+ const ctrLo = cmdCtr & 0xff;
151
+ const ctrHi = (cmdCtr >> 8) & 0xff;
152
+ const input = new Uint8Array(2 + responseData.length);
153
+ input[0] = ctrLo;
154
+ input[1] = ctrHi;
155
+ input.set(responseData, 2);
156
+ const mac = (0, crypto_1.aesCmac)(sessionKey, input);
157
+ return mac.slice(0, 8);
158
+ }
159
+ // ===========================================================================
120
160
  // Low-level NFC transceive
121
161
  // ===========================================================================
122
162
  /**
123
163
  * Send a command to the NFC tag and return the response.
124
164
  *
165
+ * When CMAC session is active:
166
+ * - Appends 8-byte CMAC to outgoing commands
167
+ * - Verifies and strips 8-byte CMAC from responses
168
+ * - Increments CmdCtr after each command-response pair
169
+ *
125
170
  * Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
126
171
  */
127
172
  async function transceive(command, _timeoutMs = 2000, retryCount = 0) {
173
+ // Build the actual command to send
174
+ let cmdToSend = command;
175
+ if (cmacSessionKey) {
176
+ const mac = computeCommandCmac(cmacSessionKey, cmacCmdCtr, command);
177
+ cmdToSend = [...command, ...Array.from(mac)];
178
+ }
128
179
  try {
129
180
  const result = react_native_1.Platform.OS === 'ios'
130
- ? await react_native_nfc_manager_1.default.sendMifareCommandIOS(command)
131
- : await react_native_nfc_manager_1.default.nfcAHandler.transceive(command);
181
+ ? await react_native_nfc_manager_1.default.sendMifareCommandIOS(cmdToSend)
182
+ : await react_native_nfc_manager_1.default.nfcAHandler.transceive(cmdToSend);
183
+ if (cmacSessionKey && result && result.length >= 8) {
184
+ // Response has CMAC appended: split data and MAC
185
+ const dataLen = result.length - 8;
186
+ const responseData = result.slice(0, dataLen);
187
+ const responseMac = result.slice(dataLen);
188
+ // Verify response CMAC (constant-time comparison)
189
+ const expectedMac = computeResponseCmac(cmacSessionKey, cmacCmdCtr, responseData);
190
+ let macValid = true;
191
+ for (let i = 0; i < 8; i++) {
192
+ macValid = macValid && (responseMac[i] === expectedMac[i]);
193
+ }
194
+ if (!macValid) {
195
+ throw new Error('CMAC_VERIFY_FAILED');
196
+ }
197
+ cmacCmdCtr++;
198
+ return responseData;
199
+ }
200
+ if (cmacSessionKey) {
201
+ // ACK-only response (e.g. WRITE command) — MAC replaces ACK
202
+ cmacCmdCtr++;
203
+ }
132
204
  return result;
133
205
  }
134
206
  catch (error) {
135
207
  const maxRetries = react_native_1.Platform.OS === 'ios' ? 1 : 2;
136
- if (retryCount < maxRetries) {
208
+ if (retryCount < maxRetries && error?.message !== 'CMAC_VERIFY_FAILED') {
137
209
  const wait = 200 * (retryCount + 1);
138
210
  await new Promise(r => setTimeout(r, wait));
139
211
  return transceive(command, _timeoutMs, retryCount + 1);
@@ -158,7 +230,7 @@ async function requestNfcTech() {
158
230
  await react_native_nfc_manager_1.default.requestTechnology(react_native_nfc_manager_1.NfcTech.NfcA);
159
231
  }
160
232
  if (react_native_1.Platform.OS === 'android') {
161
- await new Promise(r => setTimeout(r, 300));
233
+ await new Promise(r => setTimeout(r, constants_1.DELAY.ANDROID_POST_TECH));
162
234
  }
163
235
  else {
164
236
  // iOS: probe connection availability
@@ -175,11 +247,12 @@ async function requestNfcTech() {
175
247
  * @param forceLongDelay Use a longer post-release delay (e.g. after timeout / tag-lost).
176
248
  */
177
249
  async function releaseNfcTech(forceLongDelay = false) {
250
+ clearCmacSession();
178
251
  try {
179
252
  await react_native_nfc_manager_1.default.cancelTechnologyRequest();
180
253
  const delay = react_native_1.Platform.OS === 'ios'
181
- ? (forceLongDelay ? 300 : 100)
182
- : (forceLongDelay ? 1000 : 800);
254
+ ? (forceLongDelay ? constants_1.DELAY.IOS_POST_RELEASE_FORCED : constants_1.DELAY.IOS_POST_RELEASE)
255
+ : (forceLongDelay ? constants_1.DELAY.ANDROID_POST_RELEASE_FORCED : constants_1.DELAY.ANDROID_POST_RELEASE);
183
256
  await new Promise(r => setTimeout(r, delay));
184
257
  }
185
258
  catch (error) {
@@ -202,6 +275,9 @@ async function releaseNfcTech(forceLongDelay = false) {
202
275
  * @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
203
276
  */
204
277
  async function authenticate(key) {
278
+ // Clear any previous CMAC session before starting auth
279
+ // (auth commands themselves are never CMAC-protected)
280
+ clearCmacSession();
205
281
  const iv0 = new Uint8Array(16);
206
282
  // Step 1
207
283
  const response1 = await transceive([constants_1.CMD_AUTH_PART1, constants_1.KEY_NO_DATA_PROT]);
@@ -224,15 +300,14 @@ async function authenticate(key) {
224
300
  ekRndAB.set(ekPart1, 0);
225
301
  ekRndAB.set(ekPart2, 16);
226
302
  // Step 5
227
- let response2 = null;
303
+ let response2;
228
304
  try {
229
305
  response2 = await transceive([constants_1.CMD_AUTH_PART2, ...Array.from(ekRndAB)]);
230
306
  }
231
307
  catch (_) {
232
- // AUTH_PART2 transceive failure usually means wrong password
233
308
  throw new Error('AUTH_WRONG_PASSWORD');
234
309
  }
235
- if (!response2 || response2.length < 17 || response2[0] !== 0x00) {
310
+ if (response2.length < 17 || response2[0] !== 0x00) {
236
311
  throw new Error('AUTH_WRONG_PASSWORD');
237
312
  }
238
313
  // Step 6 – verify RndA'
@@ -242,6 +317,19 @@ async function authenticate(key) {
242
317
  if (!(0, crypto_1.arraysEqual)(rndARot, expectedRndARot)) {
243
318
  throw new Error('AUTH_VERIFY_FAILED');
244
319
  }
320
+ // Step 7 – derive CMAC session key and enable secure messaging
321
+ cmacSessionKey = (0, crypto_1.deriveSessionKey)(key, rndA, rndB);
322
+ cmacCmdCtr = 0;
323
+ }
324
+ // ===========================================================================
325
+ // Shared memory read helpers
326
+ // ===========================================================================
327
+ /** FAST_READ pages 0x08–0x27 (user memory). */
328
+ async function readUserMemory() {
329
+ const response = await transceive([constants_1.CMD_FAST_READ, constants_1.USER_PAGE_START, constants_1.USER_PAGE_END]);
330
+ if (!response || response.length < constants_1.USER_MEMORY_SIZE)
331
+ throw new Error('READ_FAILED');
332
+ return new Uint8Array(response.slice(0, constants_1.USER_MEMORY_SIZE));
245
333
  }
246
334
  // ===========================================================================
247
335
  // PIN retry-counter session helpers
package/dist/reader.d.ts CHANGED
@@ -12,9 +12,9 @@
12
12
  * Based on MIFARE Ultralight AES (MF0AES(H)20) datasheet.
13
13
  */
14
14
  import { DEFAULT_PIN_RETRY_COUNT } from './constants';
15
- import { ResultCode, type NfcResult } from './types';
15
+ import { NfcStatusCode, type NfcResult } from './types';
16
16
  import { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup } from './nfc-core';
17
- export { ResultCode, type NfcResult, isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, DEFAULT_PIN_RETRY_COUNT, };
17
+ export { NfcStatusCode, type NfcResult, isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, DEFAULT_PIN_RETRY_COUNT, };
18
18
  declare function parseCardInfo(data: Uint8Array): {
19
19
  version: number;
20
20
  cardType: number;
@@ -85,3 +85,14 @@ export declare function readUserNickname(password?: string, onCardIdentified?: (
85
85
  export declare function readMnemonicRetryCount(onCardIdentified?: () => void): Promise<NfcResult>;
86
86
  /** Reset the PIN retry counter to the default value (10). */
87
87
  export declare function resetRetryCountTo10(onCardIdentified?: () => void): Promise<NfcResult>;
88
+ /**
89
+ * Read card product version info (GET_VERSION command).
90
+ * No authentication required.
91
+ */
92
+ export declare function getCardVersion(onCardIdentified?: () => void): Promise<NfcResult>;
93
+ /**
94
+ * Read the ECC originality signature (READ_SIG command).
95
+ * Verifies the card is a genuine NXP product.
96
+ * No authentication required (unless Random ID is enabled).
97
+ */
98
+ export declare function readOriginality(onCardIdentified?: () => void): Promise<NfcResult>;