@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/README.md +250 -178
- package/{README.en.md → README.zh.md} +122 -50
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/nfc-core.js +3 -1
- package/dist/reader.d.ts +11 -6
- package/dist/reader.js +67 -23
- package/dist/types.d.ts +9 -0
- package/dist/types.js +17 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +53 -0
- package/dist/writer.d.ts +13 -4
- package/dist/writer.js +87 -18
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ukeyfe/react-native-nfc-litecard
|
|
2
2
|
|
|
3
|
-
English
|
|
3
|
+
[English](./README.md) | 中文
|
|
4
4
|
|
|
5
5
|
React Native NFC read/write library for **MIFARE Ultralight AES** (MF0AES(H)20), designed for LiteCard mnemonic storage.
|
|
6
6
|
|
|
@@ -19,7 +19,7 @@ React Native NFC read/write library for **MIFARE Ultralight AES** (MF0AES(H)20),
|
|
|
19
19
|
| Update card | `updateCard()` | Update mnemonic & password (old password required) |
|
|
20
20
|
| Change password | `updatePassword()` | Change password only (old password required) |
|
|
21
21
|
| Write nickname | `writeUserNickname()` | Write user nickname to card |
|
|
22
|
-
| Reset card | `resetCard()` | Wipe data,
|
|
22
|
+
| Reset card | `resetCard()` | Wipe mnemonic data, set a new password |
|
|
23
23
|
|
|
24
24
|
## Installation
|
|
25
25
|
|
|
@@ -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
|
|
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
|
-
| `
|
|
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.
|
|
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
|
|
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 |
|
|
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
|
|
109
|
-
| `type` | Mnemonic type (e.g. "12 words (128-bit)") |
|
|
110
|
-
| `entropyHex` | Entropy as
|
|
111
|
-
| `rawBytes` |
|
|
112
|
-
| `nickname` | User nickname
|
|
113
|
-
| `retryCount` | Retry count after successful
|
|
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:
|
|
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
|
|
135
|
-
| `password` | `string` | Yes | Protection password to set |
|
|
136
|
-
| `onCardIdentified` | `() => void` | No |
|
|
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
|
|
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 |
|
|
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
|
|
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
|
|
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.
|
|
233
|
+
Read the user nickname from the card.
|
|
190
234
|
|
|
191
235
|
```typescript
|
|
192
236
|
const result = await readUserNickname('your-password');
|
|
@@ -195,26 +239,39 @@ 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
|
-
### `resetCard(password
|
|
250
|
+
### `resetCard(password, newPassword, onCardIdentified?)`
|
|
201
251
|
|
|
202
|
-
Reset card: wipe
|
|
252
|
+
Reset the card: wipe mnemonic data, set a new password, and disable read/write protection. Nickname is preserved.
|
|
203
253
|
|
|
204
254
|
```typescript
|
|
205
|
-
const result = await resetCard('
|
|
255
|
+
const result = await resetCard('old-password', 'new-password');
|
|
206
256
|
if (result.code === ResultCode.RESET_SUCCESS) {
|
|
207
257
|
console.log('Reset successful');
|
|
208
258
|
}
|
|
209
259
|
```
|
|
210
260
|
|
|
211
|
-
> ⚠️ This operation is irreversible
|
|
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 \| undefined` | No | Current card password. Required if the card is protected; pass `undefined` otherwise. The retry counter is decremented automatically before authentication. |
|
|
267
|
+
| `newPassword` | `string` | Yes | Password to set after reset. |
|
|
268
|
+
| `onCardIdentified` | `() => void` | No | Called after successful authentication |
|
|
212
269
|
|
|
213
270
|
---
|
|
214
271
|
|
|
215
|
-
### `readMnemonicRetryCount()`
|
|
272
|
+
### `readMnemonicRetryCount(onCardIdentified?)`
|
|
216
273
|
|
|
217
|
-
Read the
|
|
274
|
+
Read the PIN retry counter on the card. No password authentication is required (the counter page is outside the protected area).
|
|
218
275
|
|
|
219
276
|
```typescript
|
|
220
277
|
const result = await readMnemonicRetryCount();
|
|
@@ -223,16 +280,26 @@ if (result.success) {
|
|
|
223
280
|
}
|
|
224
281
|
```
|
|
225
282
|
|
|
283
|
+
**Parameters:**
|
|
284
|
+
| Parameter | Type | Required | Description |
|
|
285
|
+
|-----------|------|----------|-------------|
|
|
286
|
+
| `onCardIdentified` | `() => void` | No | Called after the NFC session is established |
|
|
287
|
+
|
|
226
288
|
---
|
|
227
289
|
|
|
228
|
-
### `resetRetryCountTo10()`
|
|
290
|
+
### `resetRetryCountTo10(onCardIdentified?)`
|
|
229
291
|
|
|
230
|
-
Reset the PIN retry counter to its default value (10).
|
|
292
|
+
Reset the PIN retry counter to its default value (10). No password authentication is required.
|
|
231
293
|
|
|
232
294
|
```typescript
|
|
233
295
|
const result = await resetRetryCountTo10();
|
|
234
296
|
```
|
|
235
297
|
|
|
298
|
+
**Parameters:**
|
|
299
|
+
| Parameter | Type | Required | Description |
|
|
300
|
+
|-----------|------|----------|-------------|
|
|
301
|
+
| `onCardIdentified` | `() => void` | No | Called after the NFC session is established |
|
|
302
|
+
|
|
236
303
|
---
|
|
237
304
|
|
|
238
305
|
### NFC Lock Management
|
|
@@ -299,6 +366,9 @@ switch (result.code) {
|
|
|
299
366
|
case ResultCode.READ_TIMEOUT:
|
|
300
367
|
alert('Read timeout – remove and re-tap the card');
|
|
301
368
|
break;
|
|
369
|
+
case ResultCode.RETRY_COUNT_EXHAUSTED:
|
|
370
|
+
alert('Retry count exhausted – card is locked');
|
|
371
|
+
break;
|
|
302
372
|
default:
|
|
303
373
|
alert('Operation failed');
|
|
304
374
|
}
|
|
@@ -320,6 +390,7 @@ switch (result.code) {
|
|
|
320
390
|
| `UPDATE_PASSWORD_SUCCESS` | 10204 | Password change successful |
|
|
321
391
|
| `WRITE_NICKNAME_SUCCESS` | 10205 | Nickname written |
|
|
322
392
|
| `RESET_SUCCESS` | 10206 | Card reset successful |
|
|
393
|
+
| `PRECHECK_HAS_BACKUP` | 10207 | Card already has a valid backup; write was skipped |
|
|
323
394
|
|
|
324
395
|
### Error Codes
|
|
325
396
|
|
|
@@ -339,6 +410,7 @@ switch (result.code) {
|
|
|
339
410
|
| `READ_TIMEOUT` | 40012 | Read timeout |
|
|
340
411
|
| `NFC_LOCK_TIMEOUT` | 40013 | NFC lock timeout |
|
|
341
412
|
| `CRC16_CHECK_FAILED` | 40014 | CRC16 check failed |
|
|
413
|
+
| `RETRY_COUNT_EXHAUSTED` | 40015 | PIN retry count exhausted, card locked |
|
|
342
414
|
|
|
343
415
|
## Storage Format
|
|
344
416
|
|
|
@@ -364,13 +436,6 @@ The card stores BIP-39 mnemonics using entropy compression:
|
|
|
364
436
|
- **PIN retry counter**: auto-decrements on wrong password, resets to 10 on success
|
|
365
437
|
- **Secure random**: authentication uses `crypto.getRandomValues()` (requires Hermes ≥ 0.72 or `react-native-get-random-values` polyfill)
|
|
366
438
|
|
|
367
|
-
## Platform Support
|
|
368
|
-
|
|
369
|
-
| Platform | Technology | Requirements |
|
|
370
|
-
|----------|-----------|--------------|
|
|
371
|
-
| iOS | MifareIOS | iPhone 7 or later |
|
|
372
|
-
| Android | NfcA | NFC-capable device |
|
|
373
|
-
|
|
374
439
|
## Project Structure
|
|
375
440
|
|
|
376
441
|
```
|
|
@@ -379,12 +444,19 @@ src/
|
|
|
379
444
|
├── constants.ts # Shared constants (page addresses, NFC commands, mnemonic types)
|
|
380
445
|
├── types.ts # Unified ResultCode, NfcResult interface, error mapping
|
|
381
446
|
├── crypto.ts # AES encrypt/decrypt, key derivation, secure random
|
|
382
|
-
├── utils.ts # CRC16, hex conversion
|
|
447
|
+
├── utils.ts # CRC16, hex conversion, array utilities
|
|
383
448
|
├── nfc-core.ts # NFC lock, transceive, authentication, retry counter
|
|
384
449
|
├── reader.ts # Reader API
|
|
385
450
|
└── writer.ts # Writer API
|
|
386
451
|
```
|
|
387
452
|
|
|
453
|
+
## Platform Support
|
|
454
|
+
|
|
455
|
+
| Platform | Technology | Requirements |
|
|
456
|
+
|----------|-----------|--------------|
|
|
457
|
+
| iOS | MifareIOS | iPhone 7 or later |
|
|
458
|
+
| Android | NfcA | NFC-capable device |
|
|
459
|
+
|
|
388
460
|
## License
|
|
389
461
|
|
|
390
462
|
MIT
|
package/dist/constants.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export declare const MNEMONIC_TYPE_18 = 3;
|
|
|
43
43
|
export declare const MNEMONIC_TYPE_21 = 4;
|
|
44
44
|
/** 24-word mnemonic (256-bit entropy, 32 bytes) */
|
|
45
45
|
export declare const MNEMONIC_TYPE_24 = 5;
|
|
46
|
+
/** Mnemonic data end page (stops before nickname area) */
|
|
47
|
+
export declare const MNEMONIC_PAGE_END: number;
|
|
48
|
+
/** Mnemonic data size: (0x24 - 0x08 + 1) * 4 = 116 bytes */
|
|
49
|
+
export declare const MNEMONIC_MEMORY_SIZE: number;
|
|
46
50
|
/** Nickname start page */
|
|
47
51
|
export declare const USER_NICKNAME_PAGE_START: number;
|
|
48
52
|
/** Nickname end page */
|
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_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.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;
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
// NFC command codes
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +59,13 @@ exports.MNEMONIC_TYPE_21 = 0x04;
|
|
|
59
59
|
/** 24-word mnemonic (256-bit entropy, 32 bytes) */
|
|
60
60
|
exports.MNEMONIC_TYPE_24 = 0x05;
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
+
// Mnemonic data area (excludes nickname pages)
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
/** Mnemonic data end page (stops before nickname area) */
|
|
65
|
+
exports.MNEMONIC_PAGE_END = exports.USER_PAGE_END - 3; // 0x24
|
|
66
|
+
/** Mnemonic data size: (0x24 - 0x08 + 1) * 4 = 116 bytes */
|
|
67
|
+
exports.MNEMONIC_MEMORY_SIZE = (exports.MNEMONIC_PAGE_END - exports.USER_PAGE_START + 1) * exports.PAGE_SIZE;
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
62
69
|
// User nickname area (last 3 pages of user memory)
|
|
63
70
|
// ---------------------------------------------------------------------------
|
|
64
71
|
/** Nickname start page */
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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 {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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 {
|
|
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.
|