@ukeyfe/react-native-nfc-litecard 1.0.2 → 1.0.4

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.
@@ -11,6 +11,10 @@ export declare const CMD_FAST_READ = 58;
11
11
  export declare const CMD_AUTH_PART1 = 26;
12
12
  /** AES authentication part 2 */
13
13
  export declare const CMD_AUTH_PART2 = 175;
14
+ /** GET_VERSION command - retrieve product version info */
15
+ export declare const CMD_GET_VERSION = 96;
16
+ /** READ_SIG command - read ECC originality signature */
17
+ export declare const CMD_READ_SIG = 60;
14
18
  /** Data protection key slot (Key0) */
15
19
  export declare const KEY_NO_DATA_PROT = 0;
16
20
  /** Bytes per page */
@@ -59,3 +63,25 @@ export declare const RETRY_COUNTER_PAGE: number;
59
63
  export declare const RETRY_COUNTER_OFFSET: number;
60
64
  /** Default retry limit, restored after a successful authentication */
61
65
  export declare const DEFAULT_PIN_RETRY_COUNT = 10;
66
+ /** SEC_MSG_ACT bit mask in CFG0 byte 0 (bit 1) */
67
+ export declare const SEC_MSG_ACT_MASK = 2;
68
+ export declare const DELAY: {
69
+ /** Delay after requestNfcTech on Android */
70
+ readonly ANDROID_POST_TECH: 300;
71
+ /** Delay after releaseNfcTech (normal) */
72
+ readonly IOS_POST_RELEASE: 100;
73
+ readonly ANDROID_POST_RELEASE: 800;
74
+ /** Delay after releaseNfcTech (forced, e.g. after timeout) */
75
+ readonly IOS_POST_RELEASE_FORCED: 300;
76
+ readonly ANDROID_POST_RELEASE_FORCED: 1000;
77
+ /** Delay between page writes on iOS */
78
+ readonly IOS_PAGE_WRITE: 20;
79
+ /** Delay between AES key page writes on iOS */
80
+ readonly IOS_KEY_WRITE: 10;
81
+ /** Delay between nickname page writes on iOS */
82
+ readonly IOS_NICKNAME_WRITE: 100;
83
+ /** Delay between config operations on iOS */
84
+ readonly IOS_CONFIG: 80;
85
+ /** Keep-alive frequency (every N pages) during writes */
86
+ readonly KEEPALIVE_FREQ: 4;
87
+ };
package/dist/constants.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Shared constants for MIFARE Ultralight AES (MF0AES(H)20) NFC operations.
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.DEFAULT_PIN_RETRY_COUNT = exports.RETRY_COUNTER_OFFSET = exports.RETRY_COUNTER_PAGE = exports.USER_NICKNAME_MAX_LENGTH = exports.USER_NICKNAME_PAGE_END = exports.USER_NICKNAME_PAGE_START = exports.MNEMONIC_MEMORY_SIZE = exports.MNEMONIC_PAGE_END = exports.MNEMONIC_TYPE_24 = exports.MNEMONIC_TYPE_21 = exports.MNEMONIC_TYPE_18 = exports.MNEMONIC_TYPE_15 = exports.MNEMONIC_TYPE_12 = exports.USER_CARD_INFO_SIZE = exports.USER_MEMORY_SIZE = exports.PAGE_AES_KEY0_START = exports.PAGE_CFG1 = exports.PAGE_CFG0 = exports.USER_CARD_INFO_PAGE_END = exports.USER_CARD_INFO_PAGE_START = exports.USER_PAGE_END = exports.USER_PAGE_START = exports.PAGE_SIZE = exports.KEY_NO_DATA_PROT = exports.CMD_AUTH_PART2 = exports.CMD_AUTH_PART1 = exports.CMD_FAST_READ = exports.CMD_WRITE = exports.CMD_READ = void 0;
6
+ exports.DELAY = exports.SEC_MSG_ACT_MASK = exports.DEFAULT_PIN_RETRY_COUNT = exports.RETRY_COUNTER_OFFSET = exports.RETRY_COUNTER_PAGE = exports.USER_NICKNAME_MAX_LENGTH = exports.USER_NICKNAME_PAGE_END = exports.USER_NICKNAME_PAGE_START = exports.MNEMONIC_MEMORY_SIZE = exports.MNEMONIC_PAGE_END = exports.MNEMONIC_TYPE_24 = exports.MNEMONIC_TYPE_21 = exports.MNEMONIC_TYPE_18 = exports.MNEMONIC_TYPE_15 = exports.MNEMONIC_TYPE_12 = exports.USER_CARD_INFO_SIZE = exports.USER_MEMORY_SIZE = exports.PAGE_AES_KEY0_START = exports.PAGE_CFG1 = exports.PAGE_CFG0 = exports.USER_CARD_INFO_PAGE_END = exports.USER_CARD_INFO_PAGE_START = exports.USER_PAGE_END = exports.USER_PAGE_START = exports.PAGE_SIZE = exports.KEY_NO_DATA_PROT = exports.CMD_READ_SIG = exports.CMD_GET_VERSION = exports.CMD_AUTH_PART2 = exports.CMD_AUTH_PART1 = exports.CMD_FAST_READ = exports.CMD_WRITE = exports.CMD_READ = void 0;
7
7
  // ---------------------------------------------------------------------------
8
8
  // NFC command codes
9
9
  // ---------------------------------------------------------------------------
@@ -17,6 +17,10 @@ exports.CMD_FAST_READ = 0x3a;
17
17
  exports.CMD_AUTH_PART1 = 0x1a;
18
18
  /** AES authentication part 2 */
19
19
  exports.CMD_AUTH_PART2 = 0xaf;
20
+ /** GET_VERSION command - retrieve product version info */
21
+ exports.CMD_GET_VERSION = 0x60;
22
+ /** READ_SIG command - read ECC originality signature */
23
+ exports.CMD_READ_SIG = 0x3c;
20
24
  /** Data protection key slot (Key0) */
21
25
  exports.KEY_NO_DATA_PROT = 0x00;
22
26
  // ---------------------------------------------------------------------------
@@ -83,3 +87,31 @@ exports.RETRY_COUNTER_PAGE = exports.USER_PAGE_START - 1; // 0x07
83
87
  exports.RETRY_COUNTER_OFFSET = exports.PAGE_SIZE - 1;
84
88
  /** Default retry limit, restored after a successful authentication */
85
89
  exports.DEFAULT_PIN_RETRY_COUNT = 10;
90
+ // ---------------------------------------------------------------------------
91
+ // Secure messaging (CMAC)
92
+ // ---------------------------------------------------------------------------
93
+ /** SEC_MSG_ACT bit mask in CFG0 byte 0 (bit 1) */
94
+ exports.SEC_MSG_ACT_MASK = 0x02;
95
+ // ---------------------------------------------------------------------------
96
+ // Platform-specific delays (ms)
97
+ // ---------------------------------------------------------------------------
98
+ exports.DELAY = {
99
+ /** Delay after requestNfcTech on Android */
100
+ ANDROID_POST_TECH: 300,
101
+ /** Delay after releaseNfcTech (normal) */
102
+ IOS_POST_RELEASE: 100,
103
+ ANDROID_POST_RELEASE: 800,
104
+ /** Delay after releaseNfcTech (forced, e.g. after timeout) */
105
+ IOS_POST_RELEASE_FORCED: 300,
106
+ ANDROID_POST_RELEASE_FORCED: 1000,
107
+ /** Delay between page writes on iOS */
108
+ IOS_PAGE_WRITE: 20,
109
+ /** Delay between AES key page writes on iOS */
110
+ IOS_KEY_WRITE: 10,
111
+ /** Delay between nickname page writes on iOS */
112
+ IOS_NICKNAME_WRITE: 100,
113
+ /** Delay between config operations on iOS */
114
+ IOS_CONFIG: 80,
115
+ /** Keep-alive frequency (every N pages) during writes */
116
+ KEEPALIVE_FREQ: 4,
117
+ };
package/dist/crypto.d.ts CHANGED
@@ -23,6 +23,28 @@ export declare function aesEncrypt(key: Uint8Array, data: Uint8Array, iv: Uint8A
23
23
  export declare function rotateLeft8(data: Uint8Array): Uint8Array;
24
24
  /** Compare two Uint8Arrays for equality. */
25
25
  export declare function arraysEqual(a: Uint8Array, b: Uint8Array): boolean;
26
+ /** AES-128 ECB encrypt a single 16-byte block. */
27
+ export declare function aesEcbEncrypt(key: Uint8Array, data: Uint8Array): Uint8Array;
28
+ /**
29
+ * Generate CMAC subkeys K1 and K2 (NIST 800-38B §6.1).
30
+ */
31
+ export declare function generateCmacSubkeys(key: Uint8Array): {
32
+ k1: Uint8Array;
33
+ k2: Uint8Array;
34
+ };
35
+ /**
36
+ * Compute AES-CMAC (NIST 800-38B §6.2).
37
+ * Returns full 16-byte MAC.
38
+ */
39
+ export declare function aesCmac(key: Uint8Array, message: Uint8Array): Uint8Array;
40
+ /**
41
+ * Derive SesAuthMACKey from authentication key and random numbers.
42
+ * (MF0AES(H)20 §8.8.1)
43
+ *
44
+ * SV2 = 5Ah||A5h||00h||01h||00h||80h||RndA[15..14]||(RndA[13..8] XOR RndB[15..10])||RndB[9..0]||RndA[7..0]
45
+ * SesAuthMACKey = AES-CMAC(Kx, SV2)
46
+ */
47
+ export declare function deriveSessionKey(authKey: Uint8Array, rndA: Uint8Array, rndB: Uint8Array): Uint8Array;
26
48
  /**
27
49
  * Generate a 16-byte cryptographically-secure random value.
28
50
  *
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,101 @@ 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 truncateCmac(mac);
144
+ }
145
+ /**
146
+ * Truncate a 16-byte CMAC to 8 bytes using the MIFARE convention:
147
+ * select even-indexed bytes: MAC[0], MAC[2], MAC[4], ..., MAC[14].
148
+ * (First introduced in MIFARE Plus, see NXP AN13452.)
149
+ */
150
+ function truncateCmac(mac) {
151
+ return new Uint8Array([mac[0], mac[2], mac[4], mac[6], mac[8], mac[10], mac[12], mac[14]]);
152
+ }
153
+ /**
154
+ * Compute the 8-byte truncated CMAC for an NFC response.
155
+ * Input: CmdCtr (2 bytes LE) || ResponseData
156
+ */
157
+ function computeResponseCmac(sessionKey, cmdCtr, responseData) {
158
+ const ctrLo = cmdCtr & 0xff;
159
+ const ctrHi = (cmdCtr >> 8) & 0xff;
160
+ const input = new Uint8Array(2 + responseData.length);
161
+ input[0] = ctrLo;
162
+ input[1] = ctrHi;
163
+ input.set(responseData, 2);
164
+ const mac = (0, crypto_1.aesCmac)(sessionKey, input);
165
+ return truncateCmac(mac);
166
+ }
167
+ // ===========================================================================
120
168
  // Low-level NFC transceive
121
169
  // ===========================================================================
122
170
  /**
123
171
  * Send a command to the NFC tag and return the response.
124
172
  *
173
+ * When CMAC session is active:
174
+ * - Appends 8-byte CMAC to outgoing commands
175
+ * - Verifies and strips 8-byte CMAC from responses
176
+ * - Increments CmdCtr after each command-response pair
177
+ *
125
178
  * Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
126
179
  */
127
180
  async function transceive(command, _timeoutMs = 2000, retryCount = 0) {
181
+ // Build the actual command to send
182
+ let cmdToSend = command;
183
+ if (cmacSessionKey) {
184
+ const mac = computeCommandCmac(cmacSessionKey, cmacCmdCtr, command);
185
+ cmdToSend = [...command, ...Array.from(mac)];
186
+ }
128
187
  try {
129
188
  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);
189
+ ? await react_native_nfc_manager_1.default.sendMifareCommandIOS(cmdToSend)
190
+ : await react_native_nfc_manager_1.default.nfcAHandler.transceive(cmdToSend);
191
+ if (cmacSessionKey && result && result.length >= 8) {
192
+ // Response has CMAC appended: split data and MAC
193
+ const dataLen = result.length - 8;
194
+ const responseData = result.slice(0, dataLen);
195
+ const responseMac = result.slice(dataLen);
196
+ // Verify response CMAC (constant-time comparison)
197
+ const expectedMac = computeResponseCmac(cmacSessionKey, cmacCmdCtr, responseData);
198
+ let macValid = true;
199
+ for (let i = 0; i < 8; i++) {
200
+ macValid = macValid && (responseMac[i] === expectedMac[i]);
201
+ }
202
+ if (!macValid) {
203
+ throw new Error('CMAC_VERIFY_FAILED');
204
+ }
205
+ cmacCmdCtr++;
206
+ return responseData;
207
+ }
208
+ if (cmacSessionKey) {
209
+ // ACK-only response (e.g. WRITE command) — MAC replaces ACK
210
+ cmacCmdCtr++;
211
+ }
132
212
  return result;
133
213
  }
134
214
  catch (error) {
135
215
  const maxRetries = react_native_1.Platform.OS === 'ios' ? 1 : 2;
136
- if (retryCount < maxRetries) {
216
+ if (retryCount < maxRetries && error?.message !== 'CMAC_VERIFY_FAILED') {
137
217
  const wait = 200 * (retryCount + 1);
138
218
  await new Promise(r => setTimeout(r, wait));
139
219
  return transceive(command, _timeoutMs, retryCount + 1);
@@ -158,7 +238,7 @@ async function requestNfcTech() {
158
238
  await react_native_nfc_manager_1.default.requestTechnology(react_native_nfc_manager_1.NfcTech.NfcA);
159
239
  }
160
240
  if (react_native_1.Platform.OS === 'android') {
161
- await new Promise(r => setTimeout(r, 300));
241
+ await new Promise(r => setTimeout(r, constants_1.DELAY.ANDROID_POST_TECH));
162
242
  }
163
243
  else {
164
244
  // iOS: probe connection availability
@@ -175,11 +255,12 @@ async function requestNfcTech() {
175
255
  * @param forceLongDelay Use a longer post-release delay (e.g. after timeout / tag-lost).
176
256
  */
177
257
  async function releaseNfcTech(forceLongDelay = false) {
258
+ clearCmacSession();
178
259
  try {
179
260
  await react_native_nfc_manager_1.default.cancelTechnologyRequest();
180
261
  const delay = react_native_1.Platform.OS === 'ios'
181
- ? (forceLongDelay ? 300 : 100)
182
- : (forceLongDelay ? 1000 : 800);
262
+ ? (forceLongDelay ? constants_1.DELAY.IOS_POST_RELEASE_FORCED : constants_1.DELAY.IOS_POST_RELEASE)
263
+ : (forceLongDelay ? constants_1.DELAY.ANDROID_POST_RELEASE_FORCED : constants_1.DELAY.ANDROID_POST_RELEASE);
183
264
  await new Promise(r => setTimeout(r, delay));
184
265
  }
185
266
  catch (error) {
@@ -202,6 +283,9 @@ async function releaseNfcTech(forceLongDelay = false) {
202
283
  * @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
203
284
  */
204
285
  async function authenticate(key) {
286
+ // Clear any previous CMAC session before starting auth
287
+ // (auth commands themselves are never CMAC-protected)
288
+ clearCmacSession();
205
289
  const iv0 = new Uint8Array(16);
206
290
  // Step 1
207
291
  const response1 = await transceive([constants_1.CMD_AUTH_PART1, constants_1.KEY_NO_DATA_PROT]);
@@ -224,15 +308,14 @@ async function authenticate(key) {
224
308
  ekRndAB.set(ekPart1, 0);
225
309
  ekRndAB.set(ekPart2, 16);
226
310
  // Step 5
227
- let response2 = null;
311
+ let response2;
228
312
  try {
229
313
  response2 = await transceive([constants_1.CMD_AUTH_PART2, ...Array.from(ekRndAB)]);
230
314
  }
231
315
  catch (_) {
232
- // AUTH_PART2 transceive failure usually means wrong password
233
316
  throw new Error('AUTH_WRONG_PASSWORD');
234
317
  }
235
- if (!response2 || response2.length < 17 || response2[0] !== 0x00) {
318
+ if (response2.length < 17 || response2[0] !== 0x00) {
236
319
  throw new Error('AUTH_WRONG_PASSWORD');
237
320
  }
238
321
  // Step 6 – verify RndA'
@@ -242,6 +325,19 @@ async function authenticate(key) {
242
325
  if (!(0, crypto_1.arraysEqual)(rndARot, expectedRndARot)) {
243
326
  throw new Error('AUTH_VERIFY_FAILED');
244
327
  }
328
+ // Step 7 – derive CMAC session key and enable secure messaging
329
+ cmacSessionKey = (0, crypto_1.deriveSessionKey)(key, rndA, rndB);
330
+ cmacCmdCtr = 0;
331
+ }
332
+ // ===========================================================================
333
+ // Shared memory read helpers
334
+ // ===========================================================================
335
+ /** FAST_READ pages 0x08–0x27 (user memory). */
336
+ async function readUserMemory() {
337
+ const response = await transceive([constants_1.CMD_FAST_READ, constants_1.USER_PAGE_START, constants_1.USER_PAGE_END]);
338
+ if (!response || response.length < constants_1.USER_MEMORY_SIZE)
339
+ throw new Error('READ_FAILED');
340
+ return new Uint8Array(response.slice(0, constants_1.USER_MEMORY_SIZE));
245
341
  }
246
342
  // ===========================================================================
247
343
  // 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>;