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

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/README.en.md CHANGED
@@ -55,36 +55,51 @@ import {
55
55
 
56
56
  ## API Reference
57
57
 
58
- ### `checkCard(onCardIdentified?)`
58
+ ### `checkCard(password?, onCardIdentified?)`
59
59
 
60
60
  Detect card status (empty / has data).
61
61
 
62
+ **Without password (quick probe):**
62
63
  ```typescript
63
64
  const result = await checkCard();
64
65
  if (result.code === ResultCode.CHECK_EMPTY) {
65
66
  // Empty card – ready to initialize
66
67
  } else if (result.code === ResultCode.CHECK_HAS_DATA) {
67
- // Has data need password to read or update
68
+ // Has data (or read-protected, cannot determine)
69
+ }
70
+ ```
71
+
72
+ **With password (authenticated deep check, for read-protected cards):**
73
+ ```typescript
74
+ const result = await checkCard('password');
75
+ if (result.code === ResultCode.CHECK_EMPTY) {
76
+ // Empty card – ready to write
77
+ } else if (result.code === ResultCode.CHECK_HAS_DATA) {
78
+ // Valid mnemonic backup exists, type: result.data?.type
79
+ } else if (result.code === ResultCode.AUTH_WRONG_PASSWORD) {
80
+ // Wrong password
68
81
  }
69
82
  ```
70
83
 
71
84
  **Parameters:**
72
85
  | Parameter | Type | Required | Description |
73
86
  |-----------|------|----------|-------------|
74
- | `onCardIdentified` | `() => void` | No | Callback after card is identified |
87
+ | `password` | `string` | No | Card protection password. If omitted, the library reads directly (suited for unencrypted cards). If provided, AES authentication runs first, then full data is read and CRC16 is verified (suited for read-protected cards; more accurate). |
88
+ | `onCardIdentified` | `() => void` | No | Called after the NFC session is established; use for UI such as “card detected”. |
75
89
 
76
90
  **Result codes:**
77
91
  | Code | Meaning |
78
92
  |------|---------|
79
- | `ResultCode.CHECK_EMPTY` (10104) | Empty card |
80
- | `ResultCode.CHECK_HAS_DATA` (10105) | Card has data |
81
- | `ResultCode.NFC_CONNECT_FAILED` (40001) | NFC connection failed |
93
+ | `ResultCode.CHECK_EMPTY` (10104) | Empty card – no mnemonic data |
94
+ | `ResultCode.CHECK_HAS_DATA` (10105) | Card has data. When a password is supplied, `data.type` contains the mnemonic type (e.g. `"12 words (128-bit)"`). |
95
+ | `ResultCode.AUTH_WRONG_PASSWORD` (40002) | Wrong password (only possible when a password is provided) |
96
+ | `ResultCode.NFC_CONNECT_FAILED` (40001) | NFC connection failed – card not tapped or device unsupported |
82
97
 
83
98
  ---
84
99
 
85
100
  ### `readMnemonic(password, onCardIdentified?)`
86
101
 
87
- Read mnemonic from a password-protected card.
102
+ Read the mnemonic (password authentication required). The retry counter is decremented before authentication and reset to 10 after a successful authentication.
88
103
 
89
104
  ```typescript
90
105
  const result = await readMnemonic('your-password');
@@ -99,24 +114,24 @@ if (result.success) {
99
114
  **Parameters:**
100
115
  | Parameter | Type | Required | Description |
101
116
  |-----------|------|----------|-------------|
102
- | `password` | `string` | Yes | Card protection password |
103
- | `onCardIdentified` | `() => void` | No | Callback after successful authentication |
117
+ | `password` | `string` | Yes | Card protection password for AES-128 authentication |
118
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication and before reading data; use for UI such as “reading”. |
104
119
 
105
120
  **Returned `data` fields:**
106
- | Field | Description |
107
- |-------|-------------|
108
- | `mnemonic` | BIP-39 mnemonic phrase |
109
- | `type` | Mnemonic type (e.g. "12 words (128-bit)") |
110
- | `entropyHex` | Entropy as hex string |
111
- | `rawBytes` | Raw data as hex string |
112
- | `nickname` | User nickname (if set) |
113
- | `retryCount` | Retry count after successful reset |
121
+ | Field | Type | Description |
122
+ |-------|------|-------------|
123
+ | `mnemonic` | `string` | BIP-39 mnemonic (e.g. `"abandon abandon ... about"`) |
124
+ | `type` | `string` | Mnemonic type (e.g. `"12 words (128-bit)"`, `"24 words (256-bit)"`) |
125
+ | `entropyHex` | `string` | Entropy as a hexadecimal string |
126
+ | `rawBytes` | `string` | Hex string of raw on-card data (type + entropy) |
127
+ | `nickname` | `string` | User nickname if set on the card |
128
+ | `retryCount` | `number` | Retry count after successful authentication (normally 10) |
114
129
 
115
130
  ---
116
131
 
117
132
  ### `initializeCard(mnemonic, password, onCardIdentified?)`
118
133
 
119
- Initialize a blank card: write mnemonic + set password protection.
134
+ Initialize a blank card: convert the mnemonic to BIP-39 entropy, write it to the card, enable AES password protection, and require read/write authentication.
120
135
 
121
136
  ```typescript
122
137
  const result = await initializeCard(
@@ -131,15 +146,15 @@ if (result.code === ResultCode.INIT_SUCCESS) {
131
146
  **Parameters:**
132
147
  | Parameter | Type | Required | Description |
133
148
  |-----------|------|----------|-------------|
134
- | `mnemonic` | `string` | Yes | BIP-39 mnemonic (12/15/18/21/24 words) |
135
- | `password` | `string` | Yes | Protection password to set |
136
- | `onCardIdentified` | `() => void` | No | Callback before writing begins |
149
+ | `mnemonic` | `string` | Yes | BIP-39 mnemonic; supports 12/15/18/21/24 words. The library converts to entropy + CRC16 and writes to the card. |
150
+ | `password` | `string` | Yes | Protection password to set; used to derive the AES-128 key written to the card. |
151
+ | `onCardIdentified` | `() => void` | No | Called after NFC connection is established and before writing begins; use for UI such as “writing”. |
137
152
 
138
153
  ---
139
154
 
140
- ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?)`
155
+ ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?, options?)`
141
156
 
142
- Update card: authenticate with old password, then write new mnemonic + new password.
157
+ Update the card: authenticate with the old password, then write the new mnemonic and new password. The retry counter is decremented automatically before authentication.
143
158
 
144
159
  ```typescript
145
160
  const result = await updateCard('old-password', 'new-password', 'new mnemonic words ...');
@@ -148,19 +163,34 @@ if (result.code === ResultCode.WRITE_SUCCESS) {
148
163
  }
149
164
  ```
150
165
 
166
+ **Pre-check for existing backup:**
167
+
168
+ ```typescript
169
+ const result = await updateCard('old-password', 'new-password', 'mnemonic ...', undefined, {
170
+ precheckExistingMnemonic: true,
171
+ });
172
+ if (result.code === ResultCode.PRECHECK_HAS_BACKUP) {
173
+ // Card already has a valid mnemonic backup; write was skipped
174
+ console.log('Backup exists, type:', result.data?.type);
175
+ } else if (result.code === ResultCode.WRITE_SUCCESS) {
176
+ console.log('Write successful');
177
+ }
178
+ ```
179
+
151
180
  **Parameters:**
152
181
  | Parameter | Type | Required | Description |
153
182
  |-----------|------|----------|-------------|
154
- | `oldPassword` | `string` | Yes | Current password |
155
- | `newPassword` | `string` | Yes | New password |
156
- | `newMnemonic` | `string` | Yes | New mnemonic |
157
- | `onCardIdentified` | `() => void` | No | Callback after successful authentication |
183
+ | `oldPassword` | `string` | Yes | Current card password for AES authentication |
184
+ | `newPassword` | `string` | Yes | New password; the card will be protected with this password after the write |
185
+ | `newMnemonic` | `string` | Yes | New BIP-39 mnemonic (12/15/18/21/24 words) |
186
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication; use for UI such as “writing”. |
187
+ | `options.precheckExistingMnemonic` | `boolean` | No | When `true`, after authentication the library reads card data and verifies CRC16. If a valid mnemonic already exists, returns `PRECHECK_HAS_BACKUP` and does not write. |
158
188
 
159
189
  ---
160
190
 
161
191
  ### `updatePassword(oldPassword, newPassword, onCardIdentified?)`
162
192
 
163
- Change password only, without modifying mnemonic data.
193
+ Change the password only; mnemonic data on the card is unchanged. The retry counter is decremented automatically before authentication.
164
194
 
165
195
  ```typescript
166
196
  const result = await updatePassword('old-password', 'new-password');
@@ -169,11 +199,18 @@ if (result.code === ResultCode.UPDATE_PASSWORD_SUCCESS) {
169
199
  }
170
200
  ```
171
201
 
202
+ **Parameters:**
203
+ | Parameter | Type | Required | Description |
204
+ |-----------|------|----------|-------------|
205
+ | `oldPassword` | `string` | Yes | Current card password for AES authentication |
206
+ | `newPassword` | `string` | Yes | New password; the card will use this password after the change |
207
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication |
208
+
172
209
  ---
173
210
 
174
- ### `writeUserNickname(password, nickname)`
211
+ ### `writeUserNickname(password, nickname, onCardIdentified?)`
175
212
 
176
- Write a user nickname to the card (max 12 bytes, UTF-8 encoded).
213
+ Write a user nickname to the card. The nickname is UTF-8 encoded, max 12 bytes (longer input is truncated).
177
214
 
178
215
  ```typescript
179
216
  const result = await writeUserNickname('your-password', 'MyCard');
@@ -182,11 +219,18 @@ if (result.code === ResultCode.WRITE_NICKNAME_SUCCESS) {
182
219
  }
183
220
  ```
184
221
 
222
+ **Parameters:**
223
+ | Parameter | Type | Required | Description |
224
+ |-----------|------|----------|-------------|
225
+ | `password` | `string` | Yes | Card protection password for AES authentication |
226
+ | `nickname` | `string` | Yes | User nickname; UTF-8 max 12 bytes (roughly 4 CJK characters or 12 Latin letters) |
227
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication |
228
+
185
229
  ---
186
230
 
187
- ### `readUserNickname(password?)`
231
+ ### `readUserNickname(password?, onCardIdentified?)`
188
232
 
189
- Read user nickname from the card. Supply password if read-protection is enabled.
233
+ Read the user nickname from the card.
190
234
 
191
235
  ```typescript
192
236
  const result = await readUserNickname('your-password');
@@ -195,11 +239,17 @@ if (result.success) {
195
239
  }
196
240
  ```
197
241
 
242
+ **Parameters:**
243
+ | Parameter | Type | Required | Description |
244
+ |-----------|------|----------|-------------|
245
+ | `password` | `string` | No | Card password. Required if read protection is enabled (`PROT=1`); otherwise optional. |
246
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication |
247
+
198
248
  ---
199
249
 
200
250
  ### `resetCard(password?, onCardIdentified?)`
201
251
 
202
- Reset card: wipe all user data, set password to `"000000"`.
252
+ Reset the card: wipe all user data (mnemonic, nickname), set password to `"000000"`, and disable read/write protection.
203
253
 
204
254
  ```typescript
205
255
  const result = await resetCard('your-password');
@@ -208,13 +258,19 @@ if (result.code === ResultCode.RESET_SUCCESS) {
208
258
  }
209
259
  ```
210
260
 
211
- > ⚠️ This operation is irreversible. Use with caution.
261
+ > ⚠️ This operation is irreversible; mnemonic data on the card is permanently erased.
262
+
263
+ **Parameters:**
264
+ | Parameter | Type | Required | Description |
265
+ |-----------|------|----------|-------------|
266
+ | `password` | `string` | No | Current card password. Required if the card is protected; otherwise optional. The retry counter is decremented automatically before authentication. |
267
+ | `onCardIdentified` | `() => void` | No | Called after successful authentication |
212
268
 
213
269
  ---
214
270
 
215
- ### `readMnemonicRetryCount()`
271
+ ### `readMnemonicRetryCount(onCardIdentified?)`
216
272
 
217
- Read the current PIN retry counter value.
273
+ Read the PIN retry counter on the card. No password authentication is required (the counter page is outside the protected area).
218
274
 
219
275
  ```typescript
220
276
  const result = await readMnemonicRetryCount();
@@ -223,16 +279,26 @@ if (result.success) {
223
279
  }
224
280
  ```
225
281
 
282
+ **Parameters:**
283
+ | Parameter | Type | Required | Description |
284
+ |-----------|------|----------|-------------|
285
+ | `onCardIdentified` | `() => void` | No | Called after the NFC session is established |
286
+
226
287
  ---
227
288
 
228
- ### `resetRetryCountTo10()`
289
+ ### `resetRetryCountTo10(onCardIdentified?)`
229
290
 
230
- Reset the PIN retry counter to its default value (10).
291
+ Reset the PIN retry counter to its default value (10). No password authentication is required.
231
292
 
232
293
  ```typescript
233
294
  const result = await resetRetryCountTo10();
234
295
  ```
235
296
 
297
+ **Parameters:**
298
+ | Parameter | Type | Required | Description |
299
+ |-----------|------|----------|-------------|
300
+ | `onCardIdentified` | `() => void` | No | Called after the NFC session is established |
301
+
236
302
  ---
237
303
 
238
304
  ### NFC Lock Management
@@ -299,6 +365,9 @@ switch (result.code) {
299
365
  case ResultCode.READ_TIMEOUT:
300
366
  alert('Read timeout – remove and re-tap the card');
301
367
  break;
368
+ case ResultCode.RETRY_COUNT_EXHAUSTED:
369
+ alert('Retry count exhausted – card is locked');
370
+ break;
302
371
  default:
303
372
  alert('Operation failed');
304
373
  }
@@ -320,6 +389,7 @@ switch (result.code) {
320
389
  | `UPDATE_PASSWORD_SUCCESS` | 10204 | Password change successful |
321
390
  | `WRITE_NICKNAME_SUCCESS` | 10205 | Nickname written |
322
391
  | `RESET_SUCCESS` | 10206 | Card reset successful |
392
+ | `PRECHECK_HAS_BACKUP` | 10207 | Card already has a valid backup; write was skipped |
323
393
 
324
394
  ### Error Codes
325
395
 
@@ -339,6 +409,7 @@ switch (result.code) {
339
409
  | `READ_TIMEOUT` | 40012 | Read timeout |
340
410
  | `NFC_LOCK_TIMEOUT` | 40013 | NFC lock timeout |
341
411
  | `CRC16_CHECK_FAILED` | 40014 | CRC16 check failed |
412
+ | `RETRY_COUNT_EXHAUSTED` | 40015 | PIN retry count exhausted, card locked |
342
413
 
343
414
  ## Storage Format
344
415
 
@@ -364,13 +435,6 @@ The card stores BIP-39 mnemonics using entropy compression:
364
435
  - **PIN retry counter**: auto-decrements on wrong password, resets to 10 on success
365
436
  - **Secure random**: authentication uses `crypto.getRandomValues()` (requires Hermes ≥ 0.72 or `react-native-get-random-values` polyfill)
366
437
 
367
- ## Platform Support
368
-
369
- | Platform | Technology | Requirements |
370
- |----------|-----------|--------------|
371
- | iOS | MifareIOS | iPhone 7 or later |
372
- | Android | NfcA | NFC-capable device |
373
-
374
438
  ## Project Structure
375
439
 
376
440
  ```
@@ -379,12 +443,19 @@ src/
379
443
  ├── constants.ts # Shared constants (page addresses, NFC commands, mnemonic types)
380
444
  ├── types.ts # Unified ResultCode, NfcResult interface, error mapping
381
445
  ├── crypto.ts # AES encrypt/decrypt, key derivation, secure random
382
- ├── utils.ts # CRC16, hex conversion
446
+ ├── utils.ts # CRC16, hex conversion, array utilities
383
447
  ├── nfc-core.ts # NFC lock, transceive, authentication, retry counter
384
448
  ├── reader.ts # Reader API
385
449
  └── writer.ts # Writer API
386
450
  ```
387
451
 
452
+ ## Platform Support
453
+
454
+ | Platform | Technology | Requirements |
455
+ |----------|-----------|--------------|
456
+ | iOS | MifareIOS | iPhone 7 or later |
457
+ | Android | NfcA | NFC-capable device |
458
+
388
459
  ## License
389
460
 
390
461
  MIT
package/README.md CHANGED
@@ -55,36 +55,51 @@ import {
55
55
 
56
56
  ## API 文档
57
57
 
58
- ### `checkCard(onCardIdentified?)`
58
+ ### `checkCard(password?, onCardIdentified?)`
59
59
 
60
60
  检测卡片状态(空卡 / 有数据)。
61
61
 
62
+ **不传密码(快速探测):**
62
63
  ```typescript
63
64
  const result = await checkCard();
64
65
  if (result.code === ResultCode.CHECK_EMPTY) {
65
66
  // 空卡,可以初始化
66
67
  } else if (result.code === ResultCode.CHECK_HAS_DATA) {
67
- // 有数据,需要密码读取或更新
68
+ // 有数据(或卡开了读保护无法确定)
69
+ }
70
+ ```
71
+
72
+ **传密码(认证后深度检查,适用于开了读保护的卡):**
73
+ ```typescript
74
+ const result = await checkCard('password');
75
+ if (result.code === ResultCode.CHECK_EMPTY) {
76
+ // 空卡,可以写入
77
+ } else if (result.code === ResultCode.CHECK_HAS_DATA) {
78
+ // 有合法助记词备份,类型: result.data?.type
79
+ } else if (result.code === ResultCode.AUTH_WRONG_PASSWORD) {
80
+ // 密码错误
68
81
  }
69
82
  ```
70
83
 
71
84
  **参数:**
72
85
  | 参数 | 类型 | 必填 | 说明 |
73
86
  |------|------|------|------|
74
- | `onCardIdentified` | `() => void` | 否 | 卡片识别成功后的回调 |
87
+ | `password` | `string` | 否 | 卡片保护密码。不传时直接尝试读取(适合未加密的卡);传入后先用密码进行 AES 认证再读取完整数据并校验 CRC16(适合开了读保护的卡,结果更准确) |
88
+ | `onCardIdentified` | `() => void` | 否 | NFC 连接建立后的回调,可用于 UI 提示"已识别到卡片" |
75
89
 
76
90
  **返回值 (`NfcResult`):**
77
91
  | code | 含义 |
78
92
  |------|------|
79
- | `ResultCode.CHECK_EMPTY` (10104) | 空卡 |
80
- | `ResultCode.CHECK_HAS_DATA` (10105) | 有数据 |
81
- | `ResultCode.NFC_CONNECT_FAILED` (40001) | NFC 连接失败 |
93
+ | `ResultCode.CHECK_EMPTY` (10104) | 空卡,没有助记词数据 |
94
+ | `ResultCode.CHECK_HAS_DATA` (10105) | 卡上有数据。传密码时 `data.type` 包含助记词类型(如 "12 words (128-bit)") |
95
+ | `ResultCode.AUTH_WRONG_PASSWORD` (40002) | 密码错误(仅传密码时可能返回) |
96
+ | `ResultCode.NFC_CONNECT_FAILED` (40001) | NFC 连接失败,卡片未贴近或设备不支持 |
82
97
 
83
98
  ---
84
99
 
85
100
  ### `readMnemonic(password, onCardIdentified?)`
86
101
 
87
- 读取助记词(需要密码认证)。
102
+ 读取助记词(需要密码认证)。认证前会自动递减重试计数器,认证成功后重置为 10。
88
103
 
89
104
  ```typescript
90
105
  const result = await readMnemonic('your-password');
@@ -99,24 +114,24 @@ if (result.success) {
99
114
  **参数:**
100
115
  | 参数 | 类型 | 必填 | 说明 |
101
116
  |------|------|------|------|
102
- | `password` | `string` | 是 | 卡片保护密码 |
103
- | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
117
+ | `password` | `string` | 是 | 卡片保护密码,用于 AES-128 认证 |
118
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后、开始读取数据前的回调,可用于 UI 提示"正在读取" |
104
119
 
105
120
  **返回 `data` 字段:**
106
- | 字段 | 说明 |
107
- |------|------|
108
- | `mnemonic` | BIP-39 助记词 |
109
- | `type` | 助记词类型(如 "12 words (128-bit)") |
110
- | `entropyHex` | 熵的十六进制 |
111
- | `rawBytes` | 原始数据十六进制 |
112
- | `nickname` | 用户昵称(如果有) |
113
- | `retryCount` | 认证成功后重置的重试次数 |
121
+ | 字段 | 类型 | 说明 |
122
+ |------|------|------|
123
+ | `mnemonic` | `string` | BIP-39 助记词(如 "abandon abandon ... about") |
124
+ | `type` | `string` | 助记词类型(如 "12 words (128-bit)"、"24 words (256-bit)") |
125
+ | `entropyHex` | `string` | 熵数据的十六进制字符串 |
126
+ | `rawBytes` | `string` | 卡上原始数据(类型+熵)的十六进制字符串 |
127
+ | `nickname` | `string` | 用户昵称(如果卡上有设置) |
128
+ | `retryCount` | `number` | 认证成功后重置的重试次数(正常为 10) |
114
129
 
115
130
  ---
116
131
 
117
132
  ### `initializeCard(mnemonic, password, onCardIdentified?)`
118
133
 
119
- 初始化空卡:写入助记词 + 设置密码保护。
134
+ 初始化空卡:将助记词转为 BIP-39 熵写入卡片,设置 AES 密码保护,开启读写认证。
120
135
 
121
136
  ```typescript
122
137
  const result = await initializeCard(
@@ -131,15 +146,15 @@ if (result.code === ResultCode.INIT_SUCCESS) {
131
146
  **参数:**
132
147
  | 参数 | 类型 | 必填 | 说明 |
133
148
  |------|------|------|------|
134
- | `mnemonic` | `string` | 是 | BIP-39 助记词(12/15/18/21/24 词) |
135
- | `password` | `string` | 是 | 要设置的保护密码 |
136
- | `onCardIdentified` | `() => void` | 否 | 开始写入前的回调 |
149
+ | `mnemonic` | `string` | 是 | BIP-39 助记词,支持 12/15/18/21/24 词。库会自动转为熵 + CRC16 写入卡片 |
150
+ | `password` | `string` | 是 | 要设置的保护密码,用于生成 AES-128 密钥并写入卡片 |
151
+ | `onCardIdentified` | `() => void` | 否 | NFC 连接建立后、开始写入前的回调,可用于 UI 提示"正在写入" |
137
152
 
138
153
  ---
139
154
 
140
- ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?)`
155
+ ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?, options?)`
141
156
 
142
- 更新卡片内容:使用旧密码认证后写入新助记词和新密码。
157
+ 更新卡片:用旧密码认证后,写入新助记词和新密码。认证前自动递减重试计数器。
143
158
 
144
159
  ```typescript
145
160
  const result = await updateCard('old-password', 'new-password', 'new mnemonic words ...');
@@ -148,19 +163,33 @@ if (result.code === ResultCode.WRITE_SUCCESS) {
148
163
  }
149
164
  ```
150
165
 
166
+ **写入前检查是否已有备份:**
167
+
168
+ ```typescript
169
+ const result = await updateCard('old-password', 'new-password', 'mnemonic ...', undefined, {
170
+ precheckExistingMnemonic: true,
171
+ });
172
+ if (result.code === ResultCode.PRECHECK_HAS_BACKUP) {
173
+ console.log('卡上已有备份,未写入。类型:', result.data?.type);
174
+ } else if (result.code === ResultCode.WRITE_SUCCESS) {
175
+ console.log('写入成功');
176
+ }
177
+ ```
178
+
151
179
  **参数:**
152
180
  | 参数 | 类型 | 必填 | 说明 |
153
181
  |------|------|------|------|
154
- | `oldPassword` | `string` | 是 | 当前密码 |
155
- | `newPassword` | `string` | 是 | 新密码 |
156
- | `newMnemonic` | `string` | 是 | 新助记词 |
157
- | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
182
+ | `oldPassword` | `string` | 是 | 当前卡片密码,用于 AES 认证 |
183
+ | `newPassword` | `string` | 是 | 新密码,写入后卡片将使用此密码保护 |
184
+ | `newMnemonic` | `string` | 是 | 新的 BIP-39 助记词(12/15/18/21/24 词) |
185
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调,可用于 UI 提示"正在写入" |
186
+ | `options.precheckExistingMnemonic` | `boolean` | 否 | 设为 `true` 时,认证后先读取卡片数据并校验 CRC16。如果已有合法助记词,返回 `PRECHECK_HAS_BACKUP` 不执行写入 |
158
187
 
159
188
  ---
160
189
 
161
190
  ### `updatePassword(oldPassword, newPassword, onCardIdentified?)`
162
191
 
163
- 仅修改密码,不更改助记词数据。
192
+ 仅修改密码,不更改卡上的助记词数据。认证前自动递减重试计数器。
164
193
 
165
194
  ```typescript
166
195
  const result = await updatePassword('old-password', 'new-password');
@@ -169,11 +198,18 @@ if (result.code === ResultCode.UPDATE_PASSWORD_SUCCESS) {
169
198
  }
170
199
  ```
171
200
 
201
+ **参数:**
202
+ | 参数 | 类型 | 必填 | 说明 |
203
+ |------|------|------|------|
204
+ | `oldPassword` | `string` | 是 | 当前卡片密码,用于 AES 认证 |
205
+ | `newPassword` | `string` | 是 | 新密码,修改后卡片将使用此密码保护 |
206
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
207
+
172
208
  ---
173
209
 
174
- ### `writeUserNickname(password, nickname)`
210
+ ### `writeUserNickname(password, nickname, onCardIdentified?)`
175
211
 
176
- 写入用户昵称到卡片(最长 12 字节,UTF-8 编码)。
212
+ 写入用户昵称到卡片。昵称使用 UTF-8 编码,最长 12 字节(超出自动截断)。
177
213
 
178
214
  ```typescript
179
215
  const result = await writeUserNickname('your-password', 'MyCard');
@@ -182,11 +218,18 @@ if (result.code === ResultCode.WRITE_NICKNAME_SUCCESS) {
182
218
  }
183
219
  ```
184
220
 
221
+ **参数:**
222
+ | 参数 | 类型 | 必填 | 说明 |
223
+ |------|------|------|------|
224
+ | `password` | `string` | 是 | 卡片保护密码,用于 AES 认证 |
225
+ | `nickname` | `string` | 是 | 用户昵称,UTF-8 编码最长 12 字节。中文约 4 个字,英文 12 个字符 |
226
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
227
+
185
228
  ---
186
229
 
187
- ### `readUserNickname(password?)`
230
+ ### `readUserNickname(password?, onCardIdentified?)`
188
231
 
189
- 读取卡片上的用户昵称。如果卡片开启了读保护,需要传密码。
232
+ 读取卡片上的用户昵称。
190
233
 
191
234
  ```typescript
192
235
  const result = await readUserNickname('your-password');
@@ -195,11 +238,17 @@ if (result.success) {
195
238
  }
196
239
  ```
197
240
 
241
+ **参数:**
242
+ | 参数 | 类型 | 必填 | 说明 |
243
+ |------|------|------|------|
244
+ | `password` | `string` | 否 | 卡片密码。如果卡开了读保护(PROT=1)则必须传入,否则可省略 |
245
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
246
+
198
247
  ---
199
248
 
200
249
  ### `resetCard(password?, onCardIdentified?)`
201
250
 
202
- 重置卡片:清空所有用户数据,密码设为 `"000000"`。
251
+ 重置卡片:清空所有用户数据(助记词、昵称),密码重置为 `"000000"`,关闭读写保护。
203
252
 
204
253
  ```typescript
205
254
  const result = await resetCard('your-password');
@@ -208,13 +257,19 @@ if (result.code === ResultCode.RESET_SUCCESS) {
208
257
  }
209
258
  ```
210
259
 
211
- > ⚠️ 重置操作不可逆,请谨慎使用。
260
+ > ⚠️ 重置操作不可逆,卡上的助记词数据将被永久清除。
261
+
262
+ **参数:**
263
+ | 参数 | 类型 | 必填 | 说明 |
264
+ |------|------|------|------|
265
+ | `password` | `string` | 否 | 当前卡片密码。如果卡开了保护则必须传入,否则可省略。认证前自动递减重试计数器 |
266
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
212
267
 
213
268
  ---
214
269
 
215
- ### `readMnemonicRetryCount()`
270
+ ### `readMnemonicRetryCount(onCardIdentified?)`
216
271
 
217
- 读取当前 PIN 重试计数器的值。
272
+ 读取卡片上的 PIN 重试计数器值。不需要密码认证(计数器页不在保护区域内)。
218
273
 
219
274
  ```typescript
220
275
  const result = await readMnemonicRetryCount();
@@ -223,16 +278,26 @@ if (result.success) {
223
278
  }
224
279
  ```
225
280
 
281
+ **参数:**
282
+ | 参数 | 类型 | 必填 | 说明 |
283
+ |------|------|------|------|
284
+ | `onCardIdentified` | `() => void` | 否 | NFC 连接建立后的回调 |
285
+
226
286
  ---
227
287
 
228
- ### `resetRetryCountTo10()`
288
+ ### `resetRetryCountTo10(onCardIdentified?)`
229
289
 
230
- 将 PIN 重试计数器重置为默认值 10
290
+ 将 PIN 重试计数器重置为默认值 10。不需要密码认证。
231
291
 
232
292
  ```typescript
233
293
  const result = await resetRetryCountTo10();
234
294
  ```
235
295
 
296
+ **参数:**
297
+ | 参数 | 类型 | 必填 | 说明 |
298
+ |------|------|------|------|
299
+ | `onCardIdentified` | `() => void` | 否 | NFC 连接建立后的回调 |
300
+
236
301
  ---
237
302
 
238
303
  ### NFC 锁管理
@@ -299,6 +364,9 @@ switch (result.code) {
299
364
  case ResultCode.READ_TIMEOUT:
300
365
  alert('读取超时,请将卡片移开后重新贴近');
301
366
  break;
367
+ case ResultCode.RETRY_COUNT_EXHAUSTED:
368
+ alert('重试次数已用完,卡片已锁定');
369
+ break;
302
370
  default:
303
371
  alert('操作失败');
304
372
  }
@@ -320,6 +388,7 @@ switch (result.code) {
320
388
  | `UPDATE_PASSWORD_SUCCESS` | 10204 | 修改密码成功 |
321
389
  | `WRITE_NICKNAME_SUCCESS` | 10205 | 写入昵称成功 |
322
390
  | `RESET_SUCCESS` | 10206 | 重置卡片成功 |
391
+ | `PRECHECK_HAS_BACKUP` | 10207 | 卡上已有合法备份,写入被跳过 |
323
392
 
324
393
  ### 错误码
325
394
 
@@ -339,6 +408,7 @@ switch (result.code) {
339
408
  | `READ_TIMEOUT` | 40012 | 读取超时 |
340
409
  | `NFC_LOCK_TIMEOUT` | 40013 | NFC 锁超时 |
341
410
  | `CRC16_CHECK_FAILED` | 40014 | CRC16 校验失败 |
411
+ | `RETRY_COUNT_EXHAUSTED` | 40015 | PIN 重试次数已用完,卡片锁定 |
342
412
 
343
413
  ## 存储格式
344
414
 
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
5
5
  */
6
- export { ResultCode, type NfcResult } from './types';
6
+ export { ResultCode, type NfcResult, nfcResultRetryCountExhausted, } from './types';
7
7
  export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, } from './reader';
8
8
  export { initializeCard, updateCard, updatePassword, writeUserNickname, resetCard, } from './writer';
9
9
  export { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, } from './nfc-core';
package/dist/index.js CHANGED
@@ -5,12 +5,13 @@
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.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.cardInfoToJson = exports.resetRetryCountTo10 = exports.readMnemonicRetryCount = exports.readUserNickname = exports.readMnemonic = exports.checkCard = exports.nfcResultRetryCountExhausted = exports.ResultCode = void 0;
9
9
  // ---------------------------------------------------------------------------
10
10
  // Unified types & constants (consumers can use a single ResultCode)
11
11
  // ---------------------------------------------------------------------------
12
12
  var types_1 = require("./types");
13
13
  Object.defineProperty(exports, "ResultCode", { enumerable: true, get: function () { return types_1.ResultCode; } });
14
+ Object.defineProperty(exports, "nfcResultRetryCountExhausted", { enumerable: true, get: function () { return types_1.nfcResultRetryCountExhausted; } });
14
15
  // ---------------------------------------------------------------------------
15
16
  // Reader API
16
17
  // ---------------------------------------------------------------------------
package/dist/nfc-core.js CHANGED
@@ -257,7 +257,9 @@ async function decrementRetryCountInSession() {
257
257
  const page = pageBlock.slice(0, constants_1.PAGE_SIZE);
258
258
  const raw = page[constants_1.RETRY_COUNTER_OFFSET];
259
259
  const current = typeof raw === 'number' ? raw & 0xff : 0;
260
- const next = current > 0 ? current - 1 : 0;
260
+ if (current <= 0)
261
+ throw new Error('RETRY_COUNT_EXHAUSTED');
262
+ const next = current - 1;
261
263
  page[constants_1.RETRY_COUNTER_OFFSET] = next & 0xff;
262
264
  await transceive([constants_1.CMD_WRITE, constants_1.RETRY_COUNTER_PAGE, ...page]);
263
265
  return next;
package/dist/reader.d.ts CHANGED
@@ -58,12 +58,17 @@ export declare function cardInfoToJson(cardInfo: ReturnType<typeof parseCardInfo
58
58
  /**
59
59
  * Detect whether the card is empty or already contains data.
60
60
  *
61
- * Logic:
61
+ * Without password: tries to read without auth.
62
62
  * - Read succeeds & first byte is a valid mnemonic type → HAS_DATA
63
63
  * - Read succeeds & first byte is other → EMPTY
64
- * - Read fails (auth required) → HAS_DATA (read-protection is on)
64
+ * - Read fails (auth required) → HAS_DATA (read-protection is on, cannot determine)
65
+ *
66
+ * With password: authenticates first, then reads and validates with CRC16.
67
+ * - Valid mnemonic payload → HAS_DATA (with type info)
68
+ * - Empty or invalid data → EMPTY
69
+ * - Auth failure → AUTH_WRONG_PASSWORD
65
70
  */
66
- export declare function checkCard(onCardIdentified?: () => void): Promise<NfcResult>;
71
+ export declare function checkCard(password?: string, onCardIdentified?: () => void): Promise<NfcResult>;
67
72
  /**
68
73
  * Read the mnemonic from a password-protected card.
69
74
  *
@@ -75,8 +80,8 @@ export declare function readMnemonic(password: string, onCardIdentified?: () =>
75
80
  * Read the user nickname from the card.
76
81
  * @param password – supply if the card has read-protection enabled.
77
82
  */
78
- export declare function readUserNickname(password?: string): Promise<NfcResult>;
83
+ export declare function readUserNickname(password?: string, onCardIdentified?: () => void): Promise<NfcResult>;
79
84
  /** Read the current PIN retry counter from the card. */
80
- export declare function readMnemonicRetryCount(): Promise<NfcResult>;
85
+ export declare function readMnemonicRetryCount(onCardIdentified?: () => void): Promise<NfcResult>;
81
86
  /** Reset the PIN retry counter to the default value (10). */
82
- export declare function resetRetryCountTo10(): Promise<NfcResult>;
87
+ export declare function resetRetryCountTo10(onCardIdentified?: () => void): Promise<NfcResult>;
package/dist/reader.js CHANGED
@@ -228,12 +228,17 @@ function cardInfoToJson(cardInfo, pretty = true) {
228
228
  /**
229
229
  * Detect whether the card is empty or already contains data.
230
230
  *
231
- * Logic:
231
+ * Without password: tries to read without auth.
232
232
  * - Read succeeds & first byte is a valid mnemonic type → HAS_DATA
233
233
  * - Read succeeds & first byte is other → EMPTY
234
- * - Read fails (auth required) → HAS_DATA (read-protection is on)
234
+ * - Read fails (auth required) → HAS_DATA (read-protection is on, cannot determine)
235
+ *
236
+ * With password: authenticates first, then reads and validates with CRC16.
237
+ * - Valid mnemonic payload → HAS_DATA (with type info)
238
+ * - Empty or invalid data → EMPTY
239
+ * - Auth failure → AUTH_WRONG_PASSWORD
235
240
  */
236
- async function checkCard(onCardIdentified) {
241
+ async function checkCard(password, onCardIdentified) {
237
242
  try {
238
243
  await (0, nfc_core_1.acquireNfcLock)();
239
244
  }
@@ -248,25 +253,54 @@ async function checkCard(onCardIdentified) {
248
253
  (0, nfc_core_1.releaseNfcLock)();
249
254
  return { code: types_1.ResultCode.NFC_CONNECT_FAILED, success: false };
250
255
  }
251
- onCardIdentified?.();
252
- let response = null;
253
256
  try {
254
- response = await (0, nfc_core_1.transceive)([constants_1.CMD_READ, constants_1.USER_PAGE_START]);
257
+ if (password) {
258
+ // Authenticated check: authenticate → read full memory → validate CRC16
259
+ const aesKey = (0, crypto_1.passwordToAesKey)(password);
260
+ await (0, nfc_core_1.authenticate)(aesKey);
261
+ onCardIdentified?.();
262
+ const data = await readUserMemory();
263
+ try {
264
+ const decoded = (0, utils_1.validateMnemonicPayload)(data);
265
+ await (0, nfc_core_1.releaseNfcTech)();
266
+ (0, nfc_core_1.releaseNfcLock)();
267
+ return { code: types_1.ResultCode.CHECK_HAS_DATA, success: true, data: { type: decoded.type } };
268
+ }
269
+ catch {
270
+ await (0, nfc_core_1.releaseNfcTech)();
271
+ (0, nfc_core_1.releaseNfcLock)();
272
+ return { code: types_1.ResultCode.CHECK_EMPTY, success: true };
273
+ }
274
+ }
275
+ else {
276
+ // Unauthenticated check: try to read first page directly
277
+ onCardIdentified?.();
278
+ let response = null;
279
+ try {
280
+ response = await (0, nfc_core_1.transceive)([constants_1.CMD_READ, constants_1.USER_PAGE_START]);
281
+ }
282
+ catch { /* read-protected */ }
283
+ await (0, nfc_core_1.releaseNfcTech)();
284
+ (0, nfc_core_1.releaseNfcLock)();
285
+ if (response && response.length >= 4) {
286
+ const first = response[0];
287
+ const hasData = first === constants_1.MNEMONIC_TYPE_12 ||
288
+ first === constants_1.MNEMONIC_TYPE_15 ||
289
+ first === constants_1.MNEMONIC_TYPE_18 ||
290
+ first === constants_1.MNEMONIC_TYPE_21 ||
291
+ first === constants_1.MNEMONIC_TYPE_24;
292
+ return { code: hasData ? types_1.ResultCode.CHECK_HAS_DATA : types_1.ResultCode.CHECK_EMPTY, success: true };
293
+ }
294
+ // Read failed = protection is on, assume has data
295
+ return { code: types_1.ResultCode.CHECK_HAS_DATA, success: true };
296
+ }
255
297
  }
256
- catch { /* read-protected */ }
257
- await (0, nfc_core_1.releaseNfcTech)();
258
- (0, nfc_core_1.releaseNfcLock)();
259
- if (response && response.length >= 4) {
260
- const first = response[0];
261
- const hasData = first === constants_1.MNEMONIC_TYPE_12 ||
262
- first === constants_1.MNEMONIC_TYPE_15 ||
263
- first === constants_1.MNEMONIC_TYPE_18 ||
264
- first === constants_1.MNEMONIC_TYPE_21 ||
265
- first === constants_1.MNEMONIC_TYPE_24;
266
- const code = hasData ? types_1.ResultCode.CHECK_HAS_DATA : types_1.ResultCode.CHECK_EMPTY;
267
- return { code, success: true };
298
+ catch (error) {
299
+ await (0, nfc_core_1.releaseNfcTech)();
300
+ (0, nfc_core_1.releaseNfcLock)();
301
+ const code = (0, types_1.errorToCode)(error);
302
+ return { code, success: false };
268
303
  }
269
- return { code: types_1.ResultCode.CHECK_HAS_DATA, success: true };
270
304
  }
271
305
  catch (error) {
272
306
  (0, nfc_core_1.releaseNfcLock)();
@@ -302,7 +336,14 @@ async function readMnemonic(password, onCardIdentified) {
302
336
  if (typeof next === 'number')
303
337
  retryCountAfterPreDecrement = next;
304
338
  }
305
- catch { /* non-fatal */ }
339
+ catch (error) {
340
+ const code = (0, types_1.errorToCode)(error);
341
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
342
+ await (0, nfc_core_1.releaseNfcTech)();
343
+ (0, nfc_core_1.releaseNfcLock)();
344
+ return (0, types_1.nfcResultRetryCountExhausted)();
345
+ }
346
+ }
306
347
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
307
348
  await (0, nfc_core_1.authenticate)(aesKey);
308
349
  onCardIdentified?.();
@@ -359,7 +400,7 @@ async function readMnemonic(password, onCardIdentified) {
359
400
  * Read the user nickname from the card.
360
401
  * @param password – supply if the card has read-protection enabled.
361
402
  */
362
- async function readUserNickname(password) {
403
+ async function readUserNickname(password, onCardIdentified) {
363
404
  try {
364
405
  await (0, nfc_core_1.acquireNfcLock)();
365
406
  }
@@ -379,6 +420,7 @@ async function readUserNickname(password) {
379
420
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
380
421
  await (0, nfc_core_1.authenticate)(aesKey);
381
422
  }
423
+ onCardIdentified?.();
382
424
  const nickname = await readUserNicknameInternal();
383
425
  await (0, nfc_core_1.releaseNfcTech)();
384
426
  (0, nfc_core_1.releaseNfcLock)();
@@ -401,7 +443,7 @@ async function readUserNickname(password) {
401
443
  }
402
444
  }
403
445
  /** Read the current PIN retry counter from the card. */
404
- async function readMnemonicRetryCount() {
446
+ async function readMnemonicRetryCount(onCardIdentified) {
405
447
  try {
406
448
  await (0, nfc_core_1.acquireNfcLock)();
407
449
  }
@@ -417,6 +459,7 @@ async function readMnemonicRetryCount() {
417
459
  return { code: types_1.ResultCode.NFC_CONNECT_FAILED, success: false };
418
460
  }
419
461
  try {
462
+ onCardIdentified?.();
420
463
  const pageBlock = await (0, nfc_core_1.transceive)([constants_1.CMD_READ, constants_1.RETRY_COUNTER_PAGE]);
421
464
  if (!pageBlock || pageBlock.length < constants_1.PAGE_SIZE) {
422
465
  await (0, nfc_core_1.releaseNfcTech)();
@@ -449,7 +492,7 @@ async function readMnemonicRetryCount() {
449
492
  }
450
493
  }
451
494
  /** Reset the PIN retry counter to the default value (10). */
452
- async function resetRetryCountTo10() {
495
+ async function resetRetryCountTo10(onCardIdentified) {
453
496
  try {
454
497
  await (0, nfc_core_1.acquireNfcLock)();
455
498
  }
@@ -465,6 +508,7 @@ async function resetRetryCountTo10() {
465
508
  return { code: types_1.ResultCode.NFC_CONNECT_FAILED, success: false };
466
509
  }
467
510
  try {
511
+ onCardIdentified?.();
468
512
  await (0, nfc_core_1.writeRetryCountInSession)(constants_1.DEFAULT_PIN_RETRY_COUNT);
469
513
  await (0, nfc_core_1.releaseNfcTech)();
470
514
  (0, nfc_core_1.releaseNfcLock)();
package/dist/types.d.ts CHANGED
@@ -20,6 +20,8 @@ export declare const ResultCode: {
20
20
  readonly UPDATE_PASSWORD_SUCCESS: 10204;
21
21
  readonly WRITE_NICKNAME_SUCCESS: 10205;
22
22
  readonly RESET_SUCCESS: 10206;
23
+ /** Card already has a valid mnemonic backup; write was skipped */
24
+ readonly PRECHECK_HAS_BACKUP: 10207;
23
25
  readonly NFC_CONNECT_FAILED: 40001;
24
26
  readonly AUTH_WRONG_PASSWORD: 40002;
25
27
  readonly AUTH_INVALID_RESPONSE: 40003;
@@ -34,6 +36,8 @@ export declare const ResultCode: {
34
36
  readonly READ_TIMEOUT: 40012;
35
37
  readonly NFC_LOCK_TIMEOUT: 40013;
36
38
  readonly CRC16_CHECK_FAILED: 40014;
39
+ /** PIN retry counter is 0; no authentication attempts left */
40
+ readonly RETRY_COUNT_EXHAUSTED: 40015;
37
41
  };
38
42
  export interface NfcResult {
39
43
  /** Numeric result code – compare against ResultCode constants */
@@ -52,6 +56,11 @@ export interface NfcResult {
52
56
  crc16?: number;
53
57
  };
54
58
  }
59
+ /**
60
+ * Unified failure result when the PIN retry counter has reached 0.
61
+ * Used by readMnemonic, updateCard, updatePassword, and resetCard.
62
+ */
63
+ export declare function nfcResultRetryCountExhausted(): NfcResult;
55
64
  /**
56
65
  * Derive a ResultCode from an error thrown during NFC operations.
57
66
  * Handles iOS-specific cancel detection, known error strings, and fallback.
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,12 +19,19 @@ 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
36
  * Reset card: wipe user data, set password to "000000".
30
37
  * @param password – current card password (required if protection is enabled).
package/dist/writer.js CHANGED
@@ -101,6 +101,13 @@ async function writeUserMemory(data) {
101
101
  }
102
102
  }
103
103
  }
104
+ /** FAST_READ pages 0x08–0x27 (user memory). */
105
+ async function readUserMemory() {
106
+ const response = await (0, nfc_core_1.transceive)([constants_1.CMD_FAST_READ, constants_1.USER_PAGE_START, constants_1.USER_PAGE_END]);
107
+ if (!response || response.length < constants_1.USER_MEMORY_SIZE)
108
+ throw new Error('READ_FAILED');
109
+ return new Uint8Array(response.slice(0, constants_1.USER_MEMORY_SIZE));
110
+ }
104
111
  /**
105
112
  * Write a 16-byte AES key to AES_KEY0 (pages 0x30–0x33).
106
113
  * Byte order is reversed per the datasheet.
@@ -273,8 +280,13 @@ async function initializeCard(mnemonic, password, onCardIdentified) {
273
280
  }
274
281
  /**
275
282
  * Update card: authenticate with old password, then write new mnemonic + new password.
283
+ *
284
+ * When `options.precheckExistingMnemonic` is true, the card is read after authentication.
285
+ * If a valid mnemonic backup already exists, the write is skipped and PRECHECK_HAS_BACKUP
286
+ * is returned (`success: true` — operation completed; distinguish outcome by `code`).
287
+ * Otherwise the normal write flow proceeds.
276
288
  */
277
- async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified) {
289
+ async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified, options) {
278
290
  try {
279
291
  await (0, nfc_core_1.acquireNfcLock)();
280
292
  }
@@ -297,7 +309,14 @@ async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentifie
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
+ }
301
320
  const oldKey = (0, crypto_1.passwordToAesKey)(oldPassword);
302
321
  await (0, nfc_core_1.authenticate)(oldKey);
303
322
  try {
@@ -305,6 +324,32 @@ async function updateCard(oldPassword, newPassword, newMnemonic, onCardIdentifie
305
324
  }
306
325
  catch { /* non-fatal */ }
307
326
  onCardIdentified?.();
327
+ if (options?.precheckExistingMnemonic) {
328
+ try {
329
+ const data = await readUserMemory();
330
+ const decoded = (0, utils_1.validateMnemonicPayload)(data);
331
+ await (0, nfc_core_1.releaseNfcTech)();
332
+ (0, nfc_core_1.releaseNfcLock)();
333
+ return {
334
+ code: types_1.ResultCode.PRECHECK_HAS_BACKUP,
335
+ success: true,
336
+ data: { type: decoded.type, retryCount: constants_1.DEFAULT_PIN_RETRY_COUNT },
337
+ };
338
+ }
339
+ catch (e) {
340
+ const c = (0, types_1.errorToCode)(e);
341
+ if (c === types_1.ResultCode.CHECK_EMPTY ||
342
+ c === types_1.ResultCode.CRC16_CHECK_FAILED ||
343
+ c === types_1.ResultCode.INVALID_CARD_DATA) {
344
+ // No valid mnemonic on card — fall through to write
345
+ }
346
+ else {
347
+ await (0, nfc_core_1.releaseNfcTech)();
348
+ (0, nfc_core_1.releaseNfcLock)();
349
+ return { code: c, success: false };
350
+ }
351
+ }
352
+ }
308
353
  await disableAuth();
309
354
  await writeUserMemory(entropyResult.data);
310
355
  const newKey = (0, crypto_1.passwordToAesKey)(newPassword);
@@ -366,7 +411,14 @@ async function updatePassword(oldPassword, newPassword, onCardIdentified) {
366
411
  if (typeof n === 'number')
367
412
  retryCountAfterPreDecrement = n;
368
413
  }
369
- catch { /* non-fatal */ }
414
+ catch (error) {
415
+ const code = (0, types_1.errorToCode)(error);
416
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
417
+ await (0, nfc_core_1.releaseNfcTech)();
418
+ (0, nfc_core_1.releaseNfcLock)();
419
+ return (0, types_1.nfcResultRetryCountExhausted)();
420
+ }
421
+ }
370
422
  const oldKey = (0, crypto_1.passwordToAesKey)(oldPassword);
371
423
  await (0, nfc_core_1.authenticate)(oldKey);
372
424
  try {
@@ -404,7 +456,7 @@ async function updatePassword(oldPassword, newPassword, onCardIdentified) {
404
456
  }
405
457
  }
406
458
  /** Write a user nickname (password required for authentication). */
407
- async function writeUserNickname(password, nickname) {
459
+ async function writeUserNickname(password, nickname, onCardIdentified) {
408
460
  try {
409
461
  await (0, nfc_core_1.acquireNfcLock)();
410
462
  }
@@ -422,6 +474,7 @@ async function writeUserNickname(password, nickname) {
422
474
  try {
423
475
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
424
476
  await (0, nfc_core_1.authenticate)(aesKey);
477
+ onCardIdentified?.();
425
478
  await disableAuth();
426
479
  await writeNicknameToCard(nickname);
427
480
  await configureAuth();
@@ -471,7 +524,14 @@ async function resetCard(password, onCardIdentified) {
471
524
  if (typeof n === 'number')
472
525
  retryCountAfterPreDecrement = n;
473
526
  }
474
- catch { /* non-fatal */ }
527
+ catch (error) {
528
+ const code = (0, types_1.errorToCode)(error);
529
+ if (code === types_1.ResultCode.RETRY_COUNT_EXHAUSTED) {
530
+ await (0, nfc_core_1.releaseNfcTech)();
531
+ (0, nfc_core_1.releaseNfcLock)();
532
+ return (0, types_1.nfcResultRetryCountExhausted)();
533
+ }
534
+ }
475
535
  const aesKey = (0, crypto_1.passwordToAesKey)(password);
476
536
  try {
477
537
  await (0, nfc_core_1.authenticate)(aesKey);
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.1",
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.en.md"
31
32
  ]
32
33
  }