@ukeyfe/react-native-nfc-litecard 1.0.0

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 ADDED
@@ -0,0 +1,390 @@
1
+ # @ukeyfe/react-native-nfc-litecard
2
+
3
+ English | [中文](./README.md)
4
+
5
+ React Native NFC read/write library for **MIFARE Ultralight AES** (MF0AES(H)20), designed for LiteCard mnemonic storage.
6
+
7
+ > **Design principle**: The library only returns status codes (`code`) and data (`data`). It does **not** provide user-facing messages. The caller should map `ResultCode` to their own localised strings.
8
+
9
+ ## Features
10
+
11
+ | Feature | Method | Description |
12
+ |---------|--------|-------------|
13
+ | Check card | `checkCard()` | Detect whether the card is empty or contains data |
14
+ | Read mnemonic | `readMnemonic()` | Read BIP-39 mnemonic (password required) |
15
+ | Read nickname | `readUserNickname()` | Read user nickname from card |
16
+ | Read retry count | `readMnemonicRetryCount()` | Read PIN retry counter |
17
+ | Reset retry count | `resetRetryCountTo10()` | Reset PIN retry counter to default (10) |
18
+ | Initialize card | `initializeCard()` | Write mnemonic + set password on a blank card |
19
+ | Update card | `updateCard()` | Update mnemonic & password (old password required) |
20
+ | Change password | `updatePassword()` | Change password only (old password required) |
21
+ | Write nickname | `writeUserNickname()` | Write user nickname to card |
22
+ | Reset card | `resetCard()` | Wipe data, reset password to "000000" |
23
+
24
+ ## Installation
25
+
26
+ ```bash
27
+ npm install @ukeyfe/react-native-nfc-litecard
28
+ ```
29
+
30
+ ### Peer Dependencies
31
+
32
+ This library requires the following peer dependencies:
33
+
34
+ ```bash
35
+ npm install react-native react-native-nfc-manager
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```typescript
41
+ import {
42
+ ResultCode,
43
+ checkCard,
44
+ readMnemonic,
45
+ initializeCard,
46
+ updateCard,
47
+ updatePassword,
48
+ writeUserNickname,
49
+ readUserNickname,
50
+ resetCard,
51
+ readMnemonicRetryCount,
52
+ resetRetryCountTo10,
53
+ } from '@ukeyfe/react-native-nfc-litecard';
54
+ ```
55
+
56
+ ## API Reference
57
+
58
+ ### `checkCard(onCardIdentified?)`
59
+
60
+ Detect card status (empty / has data).
61
+
62
+ ```typescript
63
+ const result = await checkCard();
64
+ if (result.code === ResultCode.CHECK_EMPTY) {
65
+ // Empty card – ready to initialize
66
+ } else if (result.code === ResultCode.CHECK_HAS_DATA) {
67
+ // Has data – need password to read or update
68
+ }
69
+ ```
70
+
71
+ **Parameters:**
72
+ | Parameter | Type | Required | Description |
73
+ |-----------|------|----------|-------------|
74
+ | `onCardIdentified` | `() => void` | No | Callback after card is identified |
75
+
76
+ **Result codes:**
77
+ | Code | Meaning |
78
+ |------|---------|
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 |
82
+
83
+ ---
84
+
85
+ ### `readMnemonic(password, onCardIdentified?)`
86
+
87
+ Read mnemonic from a password-protected card.
88
+
89
+ ```typescript
90
+ const result = await readMnemonic('your-password');
91
+ if (result.success) {
92
+ console.log('Mnemonic:', result.data?.mnemonic);
93
+ console.log('Type:', result.data?.type); // "12 words (128-bit)"
94
+ console.log('Nickname:', result.data?.nickname);
95
+ console.log('Retry count:', result.data?.retryCount);
96
+ }
97
+ ```
98
+
99
+ **Parameters:**
100
+ | Parameter | Type | Required | Description |
101
+ |-----------|------|----------|-------------|
102
+ | `password` | `string` | Yes | Card protection password |
103
+ | `onCardIdentified` | `() => void` | No | Callback after successful authentication |
104
+
105
+ **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 |
114
+
115
+ ---
116
+
117
+ ### `initializeCard(mnemonic, password, onCardIdentified?)`
118
+
119
+ Initialize a blank card: write mnemonic + set password protection.
120
+
121
+ ```typescript
122
+ const result = await initializeCard(
123
+ 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
124
+ 'your-password'
125
+ );
126
+ if (result.code === ResultCode.INIT_SUCCESS) {
127
+ console.log('Initialization successful');
128
+ }
129
+ ```
130
+
131
+ **Parameters:**
132
+ | Parameter | Type | Required | Description |
133
+ |-----------|------|----------|-------------|
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 |
137
+
138
+ ---
139
+
140
+ ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?)`
141
+
142
+ Update card: authenticate with old password, then write new mnemonic + new password.
143
+
144
+ ```typescript
145
+ const result = await updateCard('old-password', 'new-password', 'new mnemonic words ...');
146
+ if (result.code === ResultCode.WRITE_SUCCESS) {
147
+ console.log('Update successful');
148
+ }
149
+ ```
150
+
151
+ **Parameters:**
152
+ | Parameter | Type | Required | Description |
153
+ |-----------|------|----------|-------------|
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 |
158
+
159
+ ---
160
+
161
+ ### `updatePassword(oldPassword, newPassword, onCardIdentified?)`
162
+
163
+ Change password only, without modifying mnemonic data.
164
+
165
+ ```typescript
166
+ const result = await updatePassword('old-password', 'new-password');
167
+ if (result.code === ResultCode.UPDATE_PASSWORD_SUCCESS) {
168
+ console.log('Password updated');
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ### `writeUserNickname(password, nickname)`
175
+
176
+ Write a user nickname to the card (max 12 bytes, UTF-8 encoded).
177
+
178
+ ```typescript
179
+ const result = await writeUserNickname('your-password', 'MyCard');
180
+ if (result.code === ResultCode.WRITE_NICKNAME_SUCCESS) {
181
+ console.log('Nickname written');
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ### `readUserNickname(password?)`
188
+
189
+ Read user nickname from the card. Supply password if read-protection is enabled.
190
+
191
+ ```typescript
192
+ const result = await readUserNickname('your-password');
193
+ if (result.success) {
194
+ console.log('Nickname:', result.data?.nickname);
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ### `resetCard(password?, onCardIdentified?)`
201
+
202
+ Reset card: wipe all user data, set password to `"000000"`.
203
+
204
+ ```typescript
205
+ const result = await resetCard('your-password');
206
+ if (result.code === ResultCode.RESET_SUCCESS) {
207
+ console.log('Reset successful');
208
+ }
209
+ ```
210
+
211
+ > ⚠️ This operation is irreversible. Use with caution.
212
+
213
+ ---
214
+
215
+ ### `readMnemonicRetryCount()`
216
+
217
+ Read the current PIN retry counter value.
218
+
219
+ ```typescript
220
+ const result = await readMnemonicRetryCount();
221
+ if (result.success) {
222
+ console.log('Remaining retries:', result.data?.retryCount);
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ### `resetRetryCountTo10()`
229
+
230
+ Reset the PIN retry counter to its default value (10).
231
+
232
+ ```typescript
233
+ const result = await resetRetryCountTo10();
234
+ ```
235
+
236
+ ---
237
+
238
+ ### NFC Lock Management
239
+
240
+ For app-level NFC session lifecycle management:
241
+
242
+ ```typescript
243
+ import {
244
+ isNfcOperationLocked,
245
+ releaseNfcOperationLock,
246
+ markNfcOperationCancelledByCleanup,
247
+ consumeNfcOperationCancelledByCleanup,
248
+ } from '@ukeyfe/react-native-nfc-litecard';
249
+ ```
250
+
251
+ | Method | Description |
252
+ |--------|-------------|
253
+ | `isNfcOperationLocked()` | Check if the NFC operation lock is held |
254
+ | `releaseNfcOperationLock()` | Force-release the lock (use on page unmount) |
255
+ | `markNfcOperationCancelledByCleanup()` | Mark current operation as interrupted by cleanup |
256
+ | `consumeNfcOperationCancelledByCleanup()` | Consume the cleanup flag (returns whether it was set) |
257
+
258
+ ## NfcResult Structure
259
+
260
+ All APIs return a unified `NfcResult`:
261
+
262
+ ```typescript
263
+ interface NfcResult {
264
+ code: number; // Status code – compare against ResultCode
265
+ success: boolean; // Whether the operation succeeded
266
+ data?: { // Optional data, only present for some operations
267
+ mnemonic?: string;
268
+ type?: string;
269
+ entropyHex?: string;
270
+ rawBytes?: string;
271
+ nickname?: string;
272
+ retryCount?: number;
273
+ aesKeyHex?: string;
274
+ crc16?: number;
275
+ };
276
+ }
277
+ ```
278
+
279
+ ## Error Handling Example
280
+
281
+ ```typescript
282
+ import { ResultCode, readMnemonic } from '@ukeyfe/react-native-nfc-litecard';
283
+
284
+ const result = await readMnemonic('password');
285
+
286
+ switch (result.code) {
287
+ case ResultCode.READ_SUCCESS:
288
+ console.log('Read successful:', result.data?.mnemonic);
289
+ break;
290
+ case ResultCode.AUTH_WRONG_PASSWORD:
291
+ alert('Wrong password');
292
+ break;
293
+ case ResultCode.NFC_CONNECT_FAILED:
294
+ alert('NFC connection failed, please re-tap the card');
295
+ break;
296
+ case ResultCode.NFC_USER_CANCELED:
297
+ // iOS user cancelled – handle silently
298
+ break;
299
+ case ResultCode.READ_TIMEOUT:
300
+ alert('Read timeout – remove and re-tap the card');
301
+ break;
302
+ default:
303
+ alert('Operation failed');
304
+ }
305
+ ```
306
+
307
+ ## Result Codes
308
+
309
+ ### Success Codes
310
+
311
+ | Constant | Value | Description |
312
+ |----------|-------|-------------|
313
+ | `READ_SUCCESS` | 10102 | Mnemonic read successful |
314
+ | `READ_NICKNAME_SUCCESS` | 10103 | Nickname read successful |
315
+ | `CHECK_EMPTY` | 10104 | Empty card |
316
+ | `CHECK_HAS_DATA` | 10105 | Card has data |
317
+ | `READ_RETRY_COUNT_SUCCESS` | 10106 | Retry count read successful |
318
+ | `INIT_SUCCESS` | 10201 | Initialization successful |
319
+ | `WRITE_SUCCESS` | 10203 | Write/update successful |
320
+ | `UPDATE_PASSWORD_SUCCESS` | 10204 | Password change successful |
321
+ | `WRITE_NICKNAME_SUCCESS` | 10205 | Nickname written |
322
+ | `RESET_SUCCESS` | 10206 | Card reset successful |
323
+
324
+ ### Error Codes
325
+
326
+ | Constant | Value | Description |
327
+ |----------|-------|-------------|
328
+ | `NFC_CONNECT_FAILED` | 40001 | NFC connection failed |
329
+ | `AUTH_WRONG_PASSWORD` | 40002 | Wrong password |
330
+ | `AUTH_INVALID_RESPONSE` | 40003 | Invalid authentication response |
331
+ | `AUTH_VERIFY_FAILED` | 40004 | Authentication verification failed |
332
+ | `READ_FAILED` | 40005 | Read failed |
333
+ | `WRITE_FAILED` | 40006 | Write failed |
334
+ | `INVALID_MNEMONIC` | 40007 | Invalid mnemonic |
335
+ | `UNSUPPORTED_MNEMONIC_LENGTH` | 40008 | Unsupported mnemonic length |
336
+ | `INVALID_CARD_DATA` | 40009 | Invalid card data |
337
+ | `UNKNOWN_ERROR` | 40010 | Unknown error |
338
+ | `NFC_USER_CANCELED` | 40011 | User cancelled NFC scan (iOS) |
339
+ | `READ_TIMEOUT` | 40012 | Read timeout |
340
+ | `NFC_LOCK_TIMEOUT` | 40013 | NFC lock timeout |
341
+ | `CRC16_CHECK_FAILED` | 40014 | CRC16 check failed |
342
+
343
+ ## Storage Format
344
+
345
+ The card stores BIP-39 mnemonics using entropy compression:
346
+
347
+ ```
348
+ [type 1B] [entropy 16-32B] [CRC16 2B]
349
+ ```
350
+
351
+ | Type | Mnemonic Length | Entropy Size |
352
+ |------|----------------|--------------|
353
+ | 0x01 | 12 words | 16 bytes (128-bit) |
354
+ | 0x02 | 15 words | 20 bytes (160-bit) |
355
+ | 0x03 | 18 words | 24 bytes (192-bit) |
356
+ | 0x04 | 21 words | 28 bytes (224-bit) |
357
+ | 0x05 | 24 words | 32 bytes (256-bit) |
358
+
359
+ ## Security
360
+
361
+ - **AES-128 hardware-level mutual authentication** (3-pass)
362
+ - **SHA-256 key derivation**: password → SHA-256 → first 16 bytes as AES key
363
+ - **CRC16-Modbus checksum**: data integrity verification
364
+ - **PIN retry counter**: auto-decrements on wrong password, resets to 10 on success
365
+ - **Secure random**: authentication uses `crypto.getRandomValues()` (requires Hermes ≥ 0.72 or `react-native-get-random-values` polyfill)
366
+
367
+ ## Platform Support
368
+
369
+ | Platform | Technology | Requirements |
370
+ |----------|-----------|--------------|
371
+ | iOS | MifareIOS | iPhone 7 or later |
372
+ | Android | NfcA | NFC-capable device |
373
+
374
+ ## Project Structure
375
+
376
+ ```
377
+ src/
378
+ ├── index.ts # Public API exports
379
+ ├── constants.ts # Shared constants (page addresses, NFC commands, mnemonic types)
380
+ ├── types.ts # Unified ResultCode, NfcResult interface, error mapping
381
+ ├── crypto.ts # AES encrypt/decrypt, key derivation, secure random
382
+ ├── utils.ts # CRC16, hex conversion
383
+ ├── nfc-core.ts # NFC lock, transceive, authentication, retry counter
384
+ ├── reader.ts # Reader API
385
+ └── writer.ts # Writer API
386
+ ```
387
+
388
+ ## License
389
+
390
+ MIT