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

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/types.js CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.ResultCode = void 0;
15
+ exports.nfcResultRetryCountExhausted = nfcResultRetryCountExhausted;
15
16
  exports.errorToCode = errorToCode;
16
17
  const react_native_1 = require("react-native");
17
18
  // ---------------------------------------------------------------------------
@@ -30,6 +31,8 @@ exports.ResultCode = {
30
31
  UPDATE_PASSWORD_SUCCESS: 10204,
31
32
  WRITE_NICKNAME_SUCCESS: 10205,
32
33
  RESET_SUCCESS: 10206,
34
+ /** Card already has a valid mnemonic backup; write was skipped */
35
+ PRECHECK_HAS_BACKUP: 10207,
33
36
  // Failure (4xxxx – shared by reader & writer)
34
37
  NFC_CONNECT_FAILED: 40001,
35
38
  AUTH_WRONG_PASSWORD: 40002,
@@ -45,7 +48,20 @@ exports.ResultCode = {
45
48
  READ_TIMEOUT: 40012,
46
49
  NFC_LOCK_TIMEOUT: 40013,
47
50
  CRC16_CHECK_FAILED: 40014,
51
+ /** PIN retry counter is 0; no authentication attempts left */
52
+ RETRY_COUNT_EXHAUSTED: 40015,
48
53
  };
54
+ /**
55
+ * Unified failure result when the PIN retry counter has reached 0.
56
+ * Used by readMnemonic, updateCard, updatePassword, and resetCard.
57
+ */
58
+ function nfcResultRetryCountExhausted() {
59
+ return {
60
+ code: exports.ResultCode.RETRY_COUNT_EXHAUSTED,
61
+ success: false,
62
+ data: { retryCount: 0 },
63
+ };
64
+ }
49
65
  // ---------------------------------------------------------------------------
50
66
  // Error → code mapping (internal use)
51
67
  // ---------------------------------------------------------------------------
@@ -71,6 +87,7 @@ function errorToCode(error) {
71
87
  if (msg.includes('NFC_USER_CANCELED_SIGNAL'))
72
88
  return exports.ResultCode.NFC_USER_CANCELED;
73
89
  const keywords = [
90
+ ['RETRY_COUNT_EXHAUSTED', exports.ResultCode.RETRY_COUNT_EXHAUSTED],
74
91
  ['AUTH_WRONG_PASSWORD', exports.ResultCode.AUTH_WRONG_PASSWORD],
75
92
  ['AUTH_INVALID_RESPONSE', exports.ResultCode.AUTH_INVALID_RESPONSE],
76
93
  ['AUTH_VERIFY_FAILED', exports.ResultCode.AUTH_VERIFY_FAILED],
package/dist/utils.d.ts CHANGED
@@ -15,3 +15,13 @@ export declare function crc16ToBytes(crc16: number): Uint8Array;
15
15
  * @throws if buffer is shorter than 2 bytes.
16
16
  */
17
17
  export declare function extractCRC16(data: Uint8Array): number;
18
+ /**
19
+ * Validate whether raw card data contains a valid mnemonic payload.
20
+ * Checks type byte, length, and CRC16 — does NOT decode the mnemonic.
21
+ *
22
+ * @throws INVALID_CARD_DATA / EMPTY_CARD / CRC16_CHECK_FAILED
23
+ * @returns The mnemonic type description string.
24
+ */
25
+ export declare function validateMnemonicPayload(data: Uint8Array): {
26
+ type: string;
27
+ };
package/dist/utils.js CHANGED
@@ -8,6 +8,8 @@ exports.hexToBytes = hexToBytes;
8
8
  exports.calculateCRC16 = calculateCRC16;
9
9
  exports.crc16ToBytes = crc16ToBytes;
10
10
  exports.extractCRC16 = extractCRC16;
11
+ exports.validateMnemonicPayload = validateMnemonicPayload;
12
+ const constants_1 = require("./constants");
11
13
  // ---------------------------------------------------------------------------
12
14
  // Hex ↔ bytes
13
15
  // ---------------------------------------------------------------------------
@@ -61,3 +63,54 @@ function extractCRC16(data) {
61
63
  }
62
64
  return data[data.length - 2] | (data[data.length - 1] << 8);
63
65
  }
66
+ // ---------------------------------------------------------------------------
67
+ // Mnemonic payload validation
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Validate whether raw card data contains a valid mnemonic payload.
71
+ * Checks type byte, length, and CRC16 — does NOT decode the mnemonic.
72
+ *
73
+ * @throws INVALID_CARD_DATA / EMPTY_CARD / CRC16_CHECK_FAILED
74
+ * @returns The mnemonic type description string.
75
+ */
76
+ function validateMnemonicPayload(data) {
77
+ if (data.length < 19)
78
+ throw new Error('INVALID_CARD_DATA');
79
+ if (data.every(b => b === 0))
80
+ throw new Error('EMPTY_CARD');
81
+ const typeId = data[0];
82
+ let entropyLength;
83
+ let typeStr;
84
+ switch (typeId) {
85
+ case constants_1.MNEMONIC_TYPE_12:
86
+ entropyLength = 16;
87
+ typeStr = '12 words (128-bit)';
88
+ break;
89
+ case constants_1.MNEMONIC_TYPE_15:
90
+ entropyLength = 20;
91
+ typeStr = '15 words (160-bit)';
92
+ break;
93
+ case constants_1.MNEMONIC_TYPE_18:
94
+ entropyLength = 24;
95
+ typeStr = '18 words (192-bit)';
96
+ break;
97
+ case constants_1.MNEMONIC_TYPE_21:
98
+ entropyLength = 28;
99
+ typeStr = '21 words (224-bit)';
100
+ break;
101
+ case constants_1.MNEMONIC_TYPE_24:
102
+ entropyLength = 32;
103
+ typeStr = '24 words (256-bit)';
104
+ break;
105
+ default: throw new Error('INVALID_CARD_DATA');
106
+ }
107
+ const expectedTotal = 1 + entropyLength + 2;
108
+ if (data.length < expectedTotal)
109
+ throw new Error('INVALID_CARD_DATA');
110
+ const dataBlock = data.slice(0, 1 + entropyLength);
111
+ const storedCRC = extractCRC16(data.slice(1 + entropyLength, expectedTotal));
112
+ const calcCRC = calculateCRC16(dataBlock);
113
+ if (storedCRC !== calcCRC)
114
+ throw new Error('CRC16_CHECK_FAILED');
115
+ return { type: typeStr };
116
+ }
package/dist/writer.d.ts CHANGED
@@ -19,14 +19,23 @@ export { ResultCode, type NfcResult, isNfcOperationLocked, releaseNfcOperationLo
19
19
  export declare function initializeCard(mnemonic: string, password: string, onCardIdentified?: () => void): Promise<NfcResult>;
20
20
  /**
21
21
  * Update card: authenticate with old password, then write new mnemonic + new password.
22
+ *
23
+ * When `options.precheckExistingMnemonic` is true, the card is read after authentication.
24
+ * If a valid mnemonic backup already exists, the write is skipped and PRECHECK_HAS_BACKUP
25
+ * is returned (`success: true` — operation completed; distinguish outcome by `code`).
26
+ * Otherwise the normal write flow proceeds.
22
27
  */
23
- export declare function updateCard(oldPassword: string, newPassword: string, newMnemonic: string, onCardIdentified?: () => void): Promise<NfcResult>;
28
+ export declare function updateCard(oldPassword: string, newPassword: string, newMnemonic: string, onCardIdentified?: () => void, options?: {
29
+ precheckExistingMnemonic?: boolean;
30
+ }): Promise<NfcResult>;
24
31
  /** Change password only (old password required). */
25
32
  export declare function updatePassword(oldPassword: string, newPassword: string, onCardIdentified?: () => void): Promise<NfcResult>;
26
33
  /** Write a user nickname (password required for authentication). */
27
- export declare function writeUserNickname(password: string, nickname: string): Promise<NfcResult>;
34
+ export declare function writeUserNickname(password: string, nickname: string, onCardIdentified?: () => void): Promise<NfcResult>;
28
35
  /**
29
- * Reset card: wipe user data, set password to "000000".
36
+ * Reset card: wipe mnemonic data and set a new password.
30
37
  * @param password – current card password (required if protection is enabled).
38
+ * @param newPassword – password to set after reset.
39
+ * @param onCardIdentified – callback when card is identified.
31
40
  */
32
- export declare function resetCard(password?: string, onCardIdentified?: () => void): Promise<NfcResult>;
41
+ export declare function resetCard(password: string | undefined, newPassword: string, onCardIdentified?: () => void): Promise<NfcResult>;
package/dist/writer.js CHANGED
@@ -77,13 +77,14 @@ async function writePage(page, data) {
77
77
  await (0, nfc_core_1.transceive)([constants_1.CMD_WRITE, page, ...data]);
78
78
  }
79
79
  /**
80
- * Write data to user memory (pages 0x08–0x27).
80
+ * Write data to mnemonic area (pages 0x08–0x24).
81
+ * Does NOT touch the nickname area (pages 0x25–0x27).
81
82
  * iOS: inserts small delays and periodic keep-alive reads.
82
83
  */
83
84
  async function writeUserMemory(data) {
84
- const buffer = new Uint8Array(constants_1.USER_MEMORY_SIZE);
85
- buffer.set(data, 0);
86
- const totalPages = constants_1.USER_PAGE_END - constants_1.USER_PAGE_START + 1;
85
+ const buffer = new Uint8Array(constants_1.MNEMONIC_MEMORY_SIZE);
86
+ buffer.set(data.slice(0, constants_1.MNEMONIC_MEMORY_SIZE), 0);
87
+ const totalPages = constants_1.MNEMONIC_PAGE_END - constants_1.USER_PAGE_START + 1;
87
88
  for (let i = 0; i < totalPages; i++) {
88
89
  const page = constants_1.USER_PAGE_START + i;
89
90
  const offset = i * constants_1.PAGE_SIZE;
@@ -101,6 +102,13 @@ async function writeUserMemory(data) {
101
102
  }
102
103
  }
103
104
  }
105
+ /** FAST_READ pages 0x08–0x27 (user memory). */
106
+ async function readUserMemory() {
107
+ const response = await (0, nfc_core_1.transceive)([constants_1.CMD_FAST_READ, constants_1.USER_PAGE_START, constants_1.USER_PAGE_END]);
108
+ if (!response || response.length < constants_1.USER_MEMORY_SIZE)
109
+ throw new Error('READ_FAILED');
110
+ return new Uint8Array(response.slice(0, constants_1.USER_MEMORY_SIZE));
111
+ }
104
112
  /**
105
113
  * Write a 16-byte AES key to AES_KEY0 (pages 0x30–0x33).
106
114
  * Byte order is reversed per the datasheet.
@@ -273,8 +281,13 @@ async function initializeCard(mnemonic, password, onCardIdentified) {
273
281
  }
274
282
  /**
275
283
  * Update card: authenticate with old password, then write new mnemonic + new password.
284
+ *
285
+ * When `options.precheckExistingMnemonic` is true, the card is read after authentication.
286
+ * If a valid mnemonic backup already exists, the write is skipped and PRECHECK_HAS_BACKUP
287
+ * is returned (`success: true` — operation completed; distinguish outcome by `code`).
288
+ * Otherwise the normal write flow proceeds.
276
289
  */
277
- async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified) {
290
+ async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified, options) {
278
291
  try {
279
292
  await (0, nfc_core_1.acquireNfcLock)();
280
293
  }
@@ -291,22 +304,57 @@ async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentifie
291
304
  return { code: types_1.ResultCode.NFC_CONNECT_FAILED, success: false };
292
305
  }
293
306
  try {
294
- const entropyResult = mnemonicToEntropyWithCRC(newMnemonic);
295
307
  try {
296
308
  const n = await (0, nfc_core_1.decrementRetryCountInSession)();
297
309
  if (typeof n === 'number')
298
310
  retryCountAfterPreDecrement = n;
299
311
  }
300
- catch { /* non-fatal */ }
312
+ catch (error) {
313
+ const code = (0, types_1.errorToCode)(error);
314
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
315
+ await (0, nfc_core_1.releaseNfcTech)();
316
+ (0, nfc_core_1.releaseNfcLock)();
317
+ return (0, types_1.nfcResultRetryCountExhausted)();
318
+ }
319
+ // Communication failure — counter was not decremented, continue to auth
320
+ }
301
321
  const oldKey = (0, crypto_1.passwordToAesKey)(oldPassword);
302
322
  await (0, nfc_core_1.authenticate)(oldKey);
303
323
  try {
304
324
  await (0, nfc_core_1.writeRetryCountInSession)(constants_1.DEFAULT_PIN_RETRY_COUNT);
325
+ retryCountAfterPreDecrement = constants_1.DEFAULT_PIN_RETRY_COUNT;
305
326
  }
306
327
  catch { /* non-fatal */ }
307
328
  onCardIdentified?.();
308
- await disableAuth();
329
+ if (options?.precheckExistingMnemonic) {
330
+ try {
331
+ const data = await readUserMemory();
332
+ const decoded = (0, utils_1.validateMnemonicPayload)(data);
333
+ await (0, nfc_core_1.releaseNfcTech)();
334
+ (0, nfc_core_1.releaseNfcLock)();
335
+ return {
336
+ code: types_1.ResultCode.PRECHECK_HAS_BACKUP,
337
+ success: true,
338
+ data: { type: decoded.type, retryCount: constants_1.DEFAULT_PIN_RETRY_COUNT },
339
+ };
340
+ }
341
+ catch (e) {
342
+ const c = (0, types_1.errorToCode)(e);
343
+ if (c === types_1.ResultCode.CHECK_EMPTY ||
344
+ c === types_1.ResultCode.CRC16_CHECK_FAILED ||
345
+ c === types_1.ResultCode.INVALID_CARD_DATA) {
346
+ // No valid mnemonic on card — fall through to write
347
+ }
348
+ else {
349
+ await (0, nfc_core_1.releaseNfcTech)();
350
+ (0, nfc_core_1.releaseNfcLock)();
351
+ return { code: c, success: false };
352
+ }
353
+ }
354
+ }
355
+ const entropyResult = mnemonicToEntropyWithCRC(newMnemonic);
309
356
  await writeUserMemory(entropyResult.data);
357
+ await disableAuth();
310
358
  const newKey = (0, crypto_1.passwordToAesKey)(newPassword);
311
359
  const aesKeyHex = (0, utils_1.bytesToHex)(newKey);
312
360
  await writeAesKey(newKey);
@@ -366,11 +414,20 @@ async function updatePassword(oldPassword, newPassword, onCardIdentified) {
366
414
  if (typeof n === 'number')
367
415
  retryCountAfterPreDecrement = n;
368
416
  }
369
- catch { /* non-fatal */ }
417
+ catch (error) {
418
+ const code = (0, types_1.errorToCode)(error);
419
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
420
+ await (0, nfc_core_1.releaseNfcTech)();
421
+ (0, nfc_core_1.releaseNfcLock)();
422
+ return (0, types_1.nfcResultRetryCountExhausted)();
423
+ }
424
+ // Communication failure — counter was not decremented, continue to auth
425
+ }
370
426
  const oldKey = (0, crypto_1.passwordToAesKey)(oldPassword);
371
427
  await (0, nfc_core_1.authenticate)(oldKey);
372
428
  try {
373
429
  await (0, nfc_core_1.writeRetryCountInSession)(constants_1.DEFAULT_PIN_RETRY_COUNT);
430
+ retryCountAfterPreDecrement = constants_1.DEFAULT_PIN_RETRY_COUNT;
374
431
  }
375
432
  catch { /* non-fatal */ }
376
433
  onCardIdentified?.();
@@ -404,7 +461,7 @@ async function updatePassword(oldPassword, newPassword, onCardIdentified) {
404
461
  }
405
462
  }
406
463
  /** Write a user nickname (password required for authentication). */
407
- async function writeUserNickname(password, nickname) {
464
+ async function writeUserNickname(password, nickname, onCardIdentified) {
408
465
  try {
409
466
  await (0, nfc_core_1.acquireNfcLock)();
410
467
  }
@@ -422,6 +479,7 @@ async function writeUserNickname(password, nickname) {
422
479
  try {
423
480
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
424
481
  await (0, nfc_core_1.authenticate)(aesKey);
482
+ onCardIdentified?.();
425
483
  await disableAuth();
426
484
  await writeNicknameToCard(nickname);
427
485
  await configureAuth();
@@ -445,10 +503,12 @@ async function writeUserNickname(password, nickname) {
445
503
  }
446
504
  }
447
505
  /**
448
- * Reset card: wipe user data, set password to "000000".
506
+ * Reset card: wipe mnemonic data and set a new password.
449
507
  * @param password – current card password (required if protection is enabled).
508
+ * @param newPassword – password to set after reset.
509
+ * @param onCardIdentified – callback when card is identified.
450
510
  */
451
- async function resetCard(password, onCardIdentified) {
511
+ async function resetCard(password, newPassword, onCardIdentified) {
452
512
  try {
453
513
  await (0, nfc_core_1.acquireNfcLock)();
454
514
  }
@@ -471,12 +531,21 @@ async function resetCard(password, onCardIdentified) {
471
531
  if (typeof n === 'number')
472
532
  retryCountAfterPreDecrement = n;
473
533
  }
474
- catch { /* non-fatal */ }
534
+ catch (error) {
535
+ const code = (0, types_1.errorToCode)(error);
536
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
537
+ await (0, nfc_core_1.releaseNfcTech)();
538
+ (0, nfc_core_1.releaseNfcLock)();
539
+ return (0, types_1.nfcResultRetryCountExhausted)();
540
+ }
541
+ // Communication failure — counter was not decremented, continue to auth
542
+ }
475
543
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
476
544
  try {
477
545
  await (0, nfc_core_1.authenticate)(aesKey);
478
546
  try {
479
547
  await (0, nfc_core_1.writeRetryCountInSession)(constants_1.DEFAULT_PIN_RETRY_COUNT);
548
+ retryCountAfterPreDecrement = constants_1.DEFAULT_PIN_RETRY_COUNT;
480
549
  }
481
550
  catch { /* non-fatal */ }
482
551
  }
@@ -507,11 +576,11 @@ async function resetCard(password, onCardIdentified) {
507
576
  }
508
577
  onCardIdentified?.();
509
578
  }
510
- // Wipe user memory
511
- await writeUserMemory(new Uint8Array(constants_1.USER_MEMORY_SIZE));
512
- // Set default password "000000"
513
- const defaultKey = (0, crypto_1.passwordToAesKey)('000000');
514
- await writeAesKey(defaultKey);
579
+ // Wipe mnemonic data (nickname is preserved)
580
+ await writeUserMemory(new Uint8Array(constants_1.MNEMONIC_MEMORY_SIZE));
581
+ // Set new password
582
+ const resetKey = (0, crypto_1.passwordToAesKey)(newPassword);
583
+ await writeAesKey(resetKey);
515
584
  // Ensure protection is disabled
516
585
  try {
517
586
  await disableAuth();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ukeyfe/react-native-nfc-litecard",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "NFC read/write for MIFARE Ultralight AES (LiteCard mnemonic storage)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "files": [
29
29
  "dist",
30
- "README.md"
30
+ "README.md",
31
+ "README.zh.md"
31
32
  ]
32
33
  }