@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.md ADDED
@@ -0,0 +1,390 @@
1
+ # @ukeyfe/react-native-nfc-litecard
2
+
3
+ [English](./README.en.md) | 中文
4
+
5
+ 基于 **MIFARE Ultralight AES** (MF0AES(H)20) 的 React Native NFC 读写库,用于 LiteCard 助记词存储。
6
+
7
+ > **设计原则**:库只返回状态码 (`code`) 和数据 (`data`),**不提供用户提示文案**。调用方根据 `ResultCode` 自行处理本地化提示。
8
+
9
+ ## 功能概览
10
+
11
+ | 功能 | 方法 | 说明 |
12
+ |------|------|------|
13
+ | 检测卡片 | `checkCard()` | 判断卡片是空卡还是已有数据 |
14
+ | 读取助记词 | `readMnemonic()` | 密码认证后读取 BIP-39 助记词 |
15
+ | 读取昵称 | `readUserNickname()` | 读取卡片上的用户昵称 |
16
+ | 读取重试次数 | `readMnemonicRetryCount()` | 读取 PIN 重试计数器 |
17
+ | 重置重试次数 | `resetRetryCountTo10()` | 重置 PIN 重试计数器为默认值 10 |
18
+ | 初始化卡片 | `initializeCard()` | 向空卡写入助记词并设置密码保护 |
19
+ | 更新卡片 | `updateCard()` | 更新助记词和密码(需要旧密码) |
20
+ | 修改密码 | `updatePassword()` | 仅修改密码(需要旧密码) |
21
+ | 写入昵称 | `writeUserNickname()` | 写入用户昵称到卡片 |
22
+ | 重置卡片 | `resetCard()` | 清空数据,密码重置为 "000000" |
23
+
24
+ ## 安装
25
+
26
+ ```bash
27
+ npm install @ukeyfe/react-native-nfc-litecard
28
+ ```
29
+
30
+ ### 前置依赖
31
+
32
+ 本库需要以下 peer dependencies,请确保项目中已安装:
33
+
34
+ ```bash
35
+ npm install react-native react-native-nfc-manager
36
+ ```
37
+
38
+ ## 快速上手
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 文档
57
+
58
+ ### `checkCard(onCardIdentified?)`
59
+
60
+ 检测卡片状态(空卡 / 有数据)。
61
+
62
+ ```typescript
63
+ const result = await checkCard();
64
+ if (result.code === ResultCode.CHECK_EMPTY) {
65
+ // 空卡,可以初始化
66
+ } else if (result.code === ResultCode.CHECK_HAS_DATA) {
67
+ // 有数据,需要密码读取或更新
68
+ }
69
+ ```
70
+
71
+ **参数:**
72
+ | 参数 | 类型 | 必填 | 说明 |
73
+ |------|------|------|------|
74
+ | `onCardIdentified` | `() => void` | 否 | 卡片识别成功后的回调 |
75
+
76
+ **返回值 (`NfcResult`):**
77
+ | code | 含义 |
78
+ |------|------|
79
+ | `ResultCode.CHECK_EMPTY` (10104) | 空卡 |
80
+ | `ResultCode.CHECK_HAS_DATA` (10105) | 有数据 |
81
+ | `ResultCode.NFC_CONNECT_FAILED` (40001) | NFC 连接失败 |
82
+
83
+ ---
84
+
85
+ ### `readMnemonic(password, onCardIdentified?)`
86
+
87
+ 读取助记词(需要密码认证)。
88
+
89
+ ```typescript
90
+ const result = await readMnemonic('your-password');
91
+ if (result.success) {
92
+ console.log('助记词:', result.data?.mnemonic);
93
+ console.log('类型:', result.data?.type); // "12 words (128-bit)"
94
+ console.log('昵称:', result.data?.nickname);
95
+ console.log('剩余重试:', result.data?.retryCount);
96
+ }
97
+ ```
98
+
99
+ **参数:**
100
+ | 参数 | 类型 | 必填 | 说明 |
101
+ |------|------|------|------|
102
+ | `password` | `string` | 是 | 卡片保护密码 |
103
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
104
+
105
+ **返回 `data` 字段:**
106
+ | 字段 | 说明 |
107
+ |------|------|
108
+ | `mnemonic` | BIP-39 助记词 |
109
+ | `type` | 助记词类型(如 "12 words (128-bit)") |
110
+ | `entropyHex` | 熵的十六进制 |
111
+ | `rawBytes` | 原始数据十六进制 |
112
+ | `nickname` | 用户昵称(如果有) |
113
+ | `retryCount` | 认证成功后重置的重试次数 |
114
+
115
+ ---
116
+
117
+ ### `initializeCard(mnemonic, password, onCardIdentified?)`
118
+
119
+ 初始化空卡:写入助记词 + 设置密码保护。
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('初始化成功');
128
+ }
129
+ ```
130
+
131
+ **参数:**
132
+ | 参数 | 类型 | 必填 | 说明 |
133
+ |------|------|------|------|
134
+ | `mnemonic` | `string` | 是 | BIP-39 助记词(12/15/18/21/24 词) |
135
+ | `password` | `string` | 是 | 要设置的保护密码 |
136
+ | `onCardIdentified` | `() => void` | 否 | 开始写入前的回调 |
137
+
138
+ ---
139
+
140
+ ### `updateCard(oldPassword, newPassword, newMnemonic, onCardIdentified?)`
141
+
142
+ 更新卡片内容:使用旧密码认证后写入新助记词和新密码。
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('更新成功');
148
+ }
149
+ ```
150
+
151
+ **参数:**
152
+ | 参数 | 类型 | 必填 | 说明 |
153
+ |------|------|------|------|
154
+ | `oldPassword` | `string` | 是 | 当前密码 |
155
+ | `newPassword` | `string` | 是 | 新密码 |
156
+ | `newMnemonic` | `string` | 是 | 新助记词 |
157
+ | `onCardIdentified` | `() => void` | 否 | 认证成功后的回调 |
158
+
159
+ ---
160
+
161
+ ### `updatePassword(oldPassword, newPassword, onCardIdentified?)`
162
+
163
+ 仅修改密码,不更改助记词数据。
164
+
165
+ ```typescript
166
+ const result = await updatePassword('old-password', 'new-password');
167
+ if (result.code === ResultCode.UPDATE_PASSWORD_SUCCESS) {
168
+ console.log('密码修改成功');
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ### `writeUserNickname(password, nickname)`
175
+
176
+ 写入用户昵称到卡片(最长 12 字节,UTF-8 编码)。
177
+
178
+ ```typescript
179
+ const result = await writeUserNickname('your-password', 'MyCard');
180
+ if (result.code === ResultCode.WRITE_NICKNAME_SUCCESS) {
181
+ console.log('昵称写入成功');
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ### `readUserNickname(password?)`
188
+
189
+ 读取卡片上的用户昵称。如果卡片开启了读保护,需要传密码。
190
+
191
+ ```typescript
192
+ const result = await readUserNickname('your-password');
193
+ if (result.success) {
194
+ console.log('昵称:', result.data?.nickname);
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ### `resetCard(password?, onCardIdentified?)`
201
+
202
+ 重置卡片:清空所有用户数据,密码设为 `"000000"`。
203
+
204
+ ```typescript
205
+ const result = await resetCard('your-password');
206
+ if (result.code === ResultCode.RESET_SUCCESS) {
207
+ console.log('重置成功');
208
+ }
209
+ ```
210
+
211
+ > ⚠️ 重置操作不可逆,请谨慎使用。
212
+
213
+ ---
214
+
215
+ ### `readMnemonicRetryCount()`
216
+
217
+ 读取当前 PIN 重试计数器的值。
218
+
219
+ ```typescript
220
+ const result = await readMnemonicRetryCount();
221
+ if (result.success) {
222
+ console.log('剩余重试次数:', result.data?.retryCount);
223
+ }
224
+ ```
225
+
226
+ ---
227
+
228
+ ### `resetRetryCountTo10()`
229
+
230
+ 将 PIN 重试计数器重置为默认值 10。
231
+
232
+ ```typescript
233
+ const result = await resetRetryCountTo10();
234
+ ```
235
+
236
+ ---
237
+
238
+ ### NFC 锁管理
239
+
240
+ 用于 App 页面生命周期中的 NFC 会话管理:
241
+
242
+ ```typescript
243
+ import {
244
+ isNfcOperationLocked,
245
+ releaseNfcOperationLock,
246
+ markNfcOperationCancelledByCleanup,
247
+ consumeNfcOperationCancelledByCleanup,
248
+ } from '@ukeyfe/react-native-nfc-litecard';
249
+ ```
250
+
251
+ | 方法 | 说明 |
252
+ |------|------|
253
+ | `isNfcOperationLocked()` | 检查 NFC 操作锁是否被持有 |
254
+ | `releaseNfcOperationLock()` | 强制释放锁(页面关闭时使用) |
255
+ | `markNfcOperationCancelledByCleanup()` | 标记当前操作被页面清理中断 |
256
+ | `consumeNfcOperationCancelledByCleanup()` | 消费清理标记(返回是否被中断) |
257
+
258
+ ## NfcResult 返回结构
259
+
260
+ 所有 API 统一返回 `NfcResult`:
261
+
262
+ ```typescript
263
+ interface NfcResult {
264
+ code: number; // 状态码,与 ResultCode 常量比较
265
+ success: boolean; // 操作是否成功
266
+ data?: { // 可选数据,仅部分操作返回
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
+ ## 错误处理示例
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('读取成功:', result.data?.mnemonic);
289
+ break;
290
+ case ResultCode.AUTH_WRONG_PASSWORD:
291
+ alert('密码错误');
292
+ break;
293
+ case ResultCode.NFC_CONNECT_FAILED:
294
+ alert('NFC 连接失败,请重新贴卡');
295
+ break;
296
+ case ResultCode.NFC_USER_CANCELED:
297
+ // iOS 用户取消,静默处理
298
+ break;
299
+ case ResultCode.READ_TIMEOUT:
300
+ alert('读取超时,请将卡片移开后重新贴近');
301
+ break;
302
+ default:
303
+ alert('操作失败');
304
+ }
305
+ ```
306
+
307
+ ## 返回码一览
308
+
309
+ ### 成功码
310
+
311
+ | 常量 | 值 | 说明 |
312
+ |------|------|------|
313
+ | `READ_SUCCESS` | 10102 | 读取助记词成功 |
314
+ | `READ_NICKNAME_SUCCESS` | 10103 | 读取昵称成功 |
315
+ | `CHECK_EMPTY` | 10104 | 空卡 |
316
+ | `CHECK_HAS_DATA` | 10105 | 卡片有数据 |
317
+ | `READ_RETRY_COUNT_SUCCESS` | 10106 | 读取重试次数成功 |
318
+ | `INIT_SUCCESS` | 10201 | 初始化成功 |
319
+ | `WRITE_SUCCESS` | 10203 | 写入/更新成功 |
320
+ | `UPDATE_PASSWORD_SUCCESS` | 10204 | 修改密码成功 |
321
+ | `WRITE_NICKNAME_SUCCESS` | 10205 | 写入昵称成功 |
322
+ | `RESET_SUCCESS` | 10206 | 重置卡片成功 |
323
+
324
+ ### 错误码
325
+
326
+ | 常量 | 值 | 说明 |
327
+ |------|------|------|
328
+ | `NFC_CONNECT_FAILED` | 40001 | NFC 连接失败 |
329
+ | `AUTH_WRONG_PASSWORD` | 40002 | 密码错误 |
330
+ | `AUTH_INVALID_RESPONSE` | 40003 | 认证响应无效 |
331
+ | `AUTH_VERIFY_FAILED` | 40004 | 认证验证失败 |
332
+ | `READ_FAILED` | 40005 | 读取失败 |
333
+ | `WRITE_FAILED` | 40006 | 写入失败 |
334
+ | `INVALID_MNEMONIC` | 40007 | 无效的助记词 |
335
+ | `UNSUPPORTED_MNEMONIC_LENGTH` | 40008 | 不支持的助记词长度 |
336
+ | `INVALID_CARD_DATA` | 40009 | 卡片数据无效 |
337
+ | `UNKNOWN_ERROR` | 40010 | 未知错误 |
338
+ | `NFC_USER_CANCELED` | 40011 | 用户取消 NFC 扫描 (iOS) |
339
+ | `READ_TIMEOUT` | 40012 | 读取超时 |
340
+ | `NFC_LOCK_TIMEOUT` | 40013 | NFC 锁超时 |
341
+ | `CRC16_CHECK_FAILED` | 40014 | CRC16 校验失败 |
342
+
343
+ ## 存储格式
344
+
345
+ 卡片使用 BIP-39 熵压缩存储助记词:
346
+
347
+ ```
348
+ [类型 1字节] [熵数据 16-32字节] [CRC16 2字节]
349
+ ```
350
+
351
+ | 类型值 | 助记词长度 | 熵长度 |
352
+ |--------|-----------|--------|
353
+ | 0x01 | 12 词 | 16 字节 (128-bit) |
354
+ | 0x02 | 15 词 | 20 字节 (160-bit) |
355
+ | 0x03 | 18 词 | 24 字节 (192-bit) |
356
+ | 0x04 | 21 词 | 28 字节 (224-bit) |
357
+ | 0x05 | 24 词 | 32 字节 (256-bit) |
358
+
359
+ ## 安全机制
360
+
361
+ - **AES-128 硬件级双向认证**(3-pass mutual authentication)
362
+ - **SHA-256 密钥派生**:用户密码 → SHA-256 → 取前 16 字节作为 AES 密钥
363
+ - **CRC16-Modbus 校验**:数据完整性验证
364
+ - **PIN 重试计数器**:密码错误自动递减,成功后恢复为 10
365
+ - **安全随机数**:认证使用 `crypto.getRandomValues()`(需 Hermes ≥ 0.72 或 `react-native-get-random-values` polyfill)
366
+
367
+ ## 项目结构
368
+
369
+ ```
370
+ src/
371
+ ├── index.ts # 公共 API 导出
372
+ ├── constants.ts # 共享常量(页地址、NFC 命令、助记词类型)
373
+ ├── types.ts # 统一 ResultCode、NfcResult 接口、错误映射
374
+ ├── crypto.ts # AES 加解密、密钥派生、安全随机数生成
375
+ ├── utils.ts # CRC16、hex 转换、数组工具
376
+ ├── nfc-core.ts # NFC 锁、transceive、认证、重试计数器
377
+ ├── reader.ts # 读卡 API
378
+ └── writer.ts # 写卡 API
379
+ ```
380
+
381
+ ## 平台支持
382
+
383
+ | 平台 | 技术 | 说明 |
384
+ |------|------|------|
385
+ | iOS | MifareIOS | 需要 iPhone 7 及以上 |
386
+ | Android | NfcA | 需要设备支持 NFC |
387
+
388
+ ## License
389
+
390
+ MIT
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Shared constants for MIFARE Ultralight AES (MF0AES(H)20) NFC operations.
3
+ */
4
+ /** READ command - reads 4 pages (16 bytes) starting from the specified page */
5
+ export declare const CMD_READ = 48;
6
+ /** WRITE command - writes a single page (4 bytes) */
7
+ export declare const CMD_WRITE = 162;
8
+ /** FAST_READ command - reads a range of pages in one shot */
9
+ export declare const CMD_FAST_READ = 58;
10
+ /** AES authentication part 1 */
11
+ export declare const CMD_AUTH_PART1 = 26;
12
+ /** AES authentication part 2 */
13
+ export declare const CMD_AUTH_PART2 = 175;
14
+ /** Data protection key slot (Key0) */
15
+ export declare const KEY_NO_DATA_PROT = 0;
16
+ /** Bytes per page */
17
+ export declare const PAGE_SIZE = 4;
18
+ /** User memory start page (page 8) */
19
+ export declare const USER_PAGE_START = 8;
20
+ /** User memory end page (page 39) */
21
+ export declare const USER_PAGE_END = 39;
22
+ /** Card info start page – first usable page in user memory */
23
+ export declare const USER_CARD_INFO_PAGE_START = 4;
24
+ /** Card info end page (4 pages, 16 bytes) */
25
+ export declare const USER_CARD_INFO_PAGE_END = 7;
26
+ /** Configuration page 0 – contains AUTH0 */
27
+ export declare const PAGE_CFG0 = 41;
28
+ /** Configuration page 1 – contains PROT bit */
29
+ export declare const PAGE_CFG1 = 42;
30
+ /** AES Key0 start page */
31
+ export declare const PAGE_AES_KEY0_START = 48;
32
+ /** Total user memory: (0x27 - 0x08 + 1) * 4 = 128 bytes */
33
+ export declare const USER_MEMORY_SIZE: number;
34
+ /** Card info area size: (0x07 - 0x04 + 1) * 4 = 16 bytes */
35
+ export declare const USER_CARD_INFO_SIZE: number;
36
+ /** 12-word mnemonic (128-bit entropy, 16 bytes) */
37
+ export declare const MNEMONIC_TYPE_12 = 1;
38
+ /** 15-word mnemonic (160-bit entropy, 20 bytes) */
39
+ export declare const MNEMONIC_TYPE_15 = 2;
40
+ /** 18-word mnemonic (192-bit entropy, 24 bytes) */
41
+ export declare const MNEMONIC_TYPE_18 = 3;
42
+ /** 21-word mnemonic (224-bit entropy, 28 bytes) */
43
+ export declare const MNEMONIC_TYPE_21 = 4;
44
+ /** 24-word mnemonic (256-bit entropy, 32 bytes) */
45
+ export declare const MNEMONIC_TYPE_24 = 5;
46
+ /** Nickname start page */
47
+ export declare const USER_NICKNAME_PAGE_START: number;
48
+ /** Nickname end page */
49
+ export declare const USER_NICKNAME_PAGE_END = 39;
50
+ /** Max nickname length in bytes (3 pages × 4 bytes) */
51
+ export declare const USER_NICKNAME_MAX_LENGTH = 12;
52
+ /** Retry counter page (one page before mnemonic data) */
53
+ export declare const RETRY_COUNTER_PAGE: number;
54
+ /** Byte offset of the counter within the page (last byte) */
55
+ export declare const RETRY_COUNTER_OFFSET: number;
56
+ /** Default retry limit, restored after a successful authentication */
57
+ export declare const DEFAULT_PIN_RETRY_COUNT = 10;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Shared constants for MIFARE Ultralight AES (MF0AES(H)20) NFC operations.
4
+ */
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;
7
+ // ---------------------------------------------------------------------------
8
+ // NFC command codes
9
+ // ---------------------------------------------------------------------------
10
+ /** READ command - reads 4 pages (16 bytes) starting from the specified page */
11
+ exports.CMD_READ = 0x30;
12
+ /** WRITE command - writes a single page (4 bytes) */
13
+ exports.CMD_WRITE = 0xa2;
14
+ /** FAST_READ command - reads a range of pages in one shot */
15
+ exports.CMD_FAST_READ = 0x3a;
16
+ /** AES authentication part 1 */
17
+ exports.CMD_AUTH_PART1 = 0x1a;
18
+ /** AES authentication part 2 */
19
+ exports.CMD_AUTH_PART2 = 0xaf;
20
+ /** Data protection key slot (Key0) */
21
+ exports.KEY_NO_DATA_PROT = 0x00;
22
+ // ---------------------------------------------------------------------------
23
+ // Page addresses
24
+ // ---------------------------------------------------------------------------
25
+ /** Bytes per page */
26
+ exports.PAGE_SIZE = 4;
27
+ /** User memory start page (page 8) */
28
+ exports.USER_PAGE_START = 0x08;
29
+ /** User memory end page (page 39) */
30
+ exports.USER_PAGE_END = 0x27;
31
+ /** Card info start page – first usable page in user memory */
32
+ exports.USER_CARD_INFO_PAGE_START = 0x04;
33
+ /** Card info end page (4 pages, 16 bytes) */
34
+ exports.USER_CARD_INFO_PAGE_END = 0x07;
35
+ /** Configuration page 0 – contains AUTH0 */
36
+ exports.PAGE_CFG0 = 0x29;
37
+ /** Configuration page 1 – contains PROT bit */
38
+ exports.PAGE_CFG1 = 0x2a;
39
+ /** AES Key0 start page */
40
+ exports.PAGE_AES_KEY0_START = 0x30;
41
+ // ---------------------------------------------------------------------------
42
+ // Computed sizes
43
+ // ---------------------------------------------------------------------------
44
+ /** Total user memory: (0x27 - 0x08 + 1) * 4 = 128 bytes */
45
+ exports.USER_MEMORY_SIZE = (exports.USER_PAGE_END - exports.USER_PAGE_START + 1) * exports.PAGE_SIZE;
46
+ /** Card info area size: (0x07 - 0x04 + 1) * 4 = 16 bytes */
47
+ exports.USER_CARD_INFO_SIZE = (exports.USER_CARD_INFO_PAGE_END - exports.USER_CARD_INFO_PAGE_START + 1) * exports.PAGE_SIZE;
48
+ // ---------------------------------------------------------------------------
49
+ // Mnemonic type identifiers
50
+ // ---------------------------------------------------------------------------
51
+ /** 12-word mnemonic (128-bit entropy, 16 bytes) */
52
+ exports.MNEMONIC_TYPE_12 = 0x01;
53
+ /** 15-word mnemonic (160-bit entropy, 20 bytes) */
54
+ exports.MNEMONIC_TYPE_15 = 0x02;
55
+ /** 18-word mnemonic (192-bit entropy, 24 bytes) */
56
+ exports.MNEMONIC_TYPE_18 = 0x03;
57
+ /** 21-word mnemonic (224-bit entropy, 28 bytes) */
58
+ exports.MNEMONIC_TYPE_21 = 0x04;
59
+ /** 24-word mnemonic (256-bit entropy, 32 bytes) */
60
+ exports.MNEMONIC_TYPE_24 = 0x05;
61
+ // ---------------------------------------------------------------------------
62
+ // User nickname area (last 3 pages of user memory)
63
+ // ---------------------------------------------------------------------------
64
+ /** Nickname start page */
65
+ exports.USER_NICKNAME_PAGE_START = exports.USER_PAGE_END - 2; // 0x25
66
+ /** Nickname end page */
67
+ exports.USER_NICKNAME_PAGE_END = exports.USER_PAGE_END; // 0x27
68
+ /** Max nickname length in bytes (3 pages × 4 bytes) */
69
+ exports.USER_NICKNAME_MAX_LENGTH = 12;
70
+ // ---------------------------------------------------------------------------
71
+ // PIN retry counter
72
+ // ---------------------------------------------------------------------------
73
+ /** Retry counter page (one page before mnemonic data) */
74
+ exports.RETRY_COUNTER_PAGE = exports.USER_PAGE_START - 1; // 0x07
75
+ /** Byte offset of the counter within the page (last byte) */
76
+ exports.RETRY_COUNTER_OFFSET = exports.PAGE_SIZE - 1;
77
+ /** Default retry limit, restored after a successful authentication */
78
+ exports.DEFAULT_PIN_RETRY_COUNT = 10;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Cryptographic helpers for MIFARE Ultralight AES authentication.
3
+ *
4
+ * - AES-128 CBC encrypt / decrypt (via crypto-js)
5
+ * - Password → AES key derivation (SHA-256, first 16 bytes)
6
+ * - Cryptographically-secure 16-byte random generation
7
+ * - Byte-array comparison & rotation
8
+ */
9
+ /**
10
+ * Derive a 16-byte AES-128 key from a password string.
11
+ * SHA-256 the password, then take the first 16 bytes.
12
+ */
13
+ export declare function passwordToAesKey(password: string): Uint8Array;
14
+ /** AES-128 CBC decrypt (no padding). Both key, data, iv must be 16 bytes. */
15
+ export declare function aesDecrypt(key: Uint8Array, data: Uint8Array, iv: Uint8Array): Uint8Array;
16
+ /** AES-128 CBC encrypt (no padding). Both key, data, iv must be 16 bytes. */
17
+ export declare function aesEncrypt(key: Uint8Array, data: Uint8Array, iv: Uint8Array): Uint8Array;
18
+ /**
19
+ * Left-rotate a byte array by 8 bits (1 byte).
20
+ * e.g. [1,2,3,4] → [2,3,4,1]
21
+ * Used in AES 3-pass auth to compute RndB' and verify RndA'.
22
+ */
23
+ export declare function rotateLeft8(data: Uint8Array): Uint8Array;
24
+ /** Compare two Uint8Arrays for equality. */
25
+ export declare function arraysEqual(a: Uint8Array, b: Uint8Array): boolean;
26
+ /**
27
+ * Generate a 16-byte cryptographically-secure random value.
28
+ *
29
+ * Uses `crypto.getRandomValues` when available (Hermes ≥ 0.72, or
30
+ * the `react-native-get-random-values` polyfill). Falls back to
31
+ * `Math.random()` with a console warning if the API is missing.
32
+ */
33
+ export declare function generateRandom16(): Uint8Array;
package/dist/crypto.js ADDED
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Cryptographic helpers for MIFARE Ultralight AES authentication.
4
+ *
5
+ * - AES-128 CBC encrypt / decrypt (via crypto-js)
6
+ * - Password → AES key derivation (SHA-256, first 16 bytes)
7
+ * - Cryptographically-secure 16-byte random generation
8
+ * - Byte-array comparison & rotation
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.passwordToAesKey = passwordToAesKey;
15
+ exports.aesDecrypt = aesDecrypt;
16
+ exports.aesEncrypt = aesEncrypt;
17
+ exports.rotateLeft8 = rotateLeft8;
18
+ exports.arraysEqual = arraysEqual;
19
+ exports.generateRandom16 = generateRandom16;
20
+ const crypto_js_1 = __importDefault(require("crypto-js"));
21
+ // ---------------------------------------------------------------------------
22
+ // Key derivation
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Derive a 16-byte AES-128 key from a password string.
26
+ * SHA-256 the password, then take the first 16 bytes.
27
+ */
28
+ function passwordToAesKey(password) {
29
+ const safePassword = password || '';
30
+ const hash = crypto_js_1.default.SHA256(String(safePassword)).toString();
31
+ const key = new Uint8Array(16);
32
+ for (let i = 0; i < 16; i++) {
33
+ key[i] = parseInt(hash.substr(i * 2, 2), 16);
34
+ }
35
+ return key;
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // AES-128 CBC
39
+ // ---------------------------------------------------------------------------
40
+ /** AES-128 CBC decrypt (no padding). Both key, data, iv must be 16 bytes. */
41
+ function aesDecrypt(key, data, iv) {
42
+ const keyWords = crypto_js_1.default.lib.WordArray.create(key);
43
+ const ivWords = crypto_js_1.default.lib.WordArray.create(iv);
44
+ const dataWords = crypto_js_1.default.lib.WordArray.create(data);
45
+ const decrypted = crypto_js_1.default.AES.decrypt({ ciphertext: dataWords }, keyWords, { iv: ivWords, mode: crypto_js_1.default.mode.CBC, padding: crypto_js_1.default.pad.NoPadding });
46
+ const hex = decrypted.toString();
47
+ const result = new Uint8Array(16);
48
+ for (let i = 0; i < 16; i++) {
49
+ result[i] = parseInt(hex.substr(i * 2, 2), 16);
50
+ }
51
+ return result;
52
+ }
53
+ /** AES-128 CBC encrypt (no padding). Both key, data, iv must be 16 bytes. */
54
+ function aesEncrypt(key, data, iv) {
55
+ const keyWords = crypto_js_1.default.lib.WordArray.create(key);
56
+ const ivWords = crypto_js_1.default.lib.WordArray.create(iv);
57
+ const dataWords = crypto_js_1.default.lib.WordArray.create(data);
58
+ const encrypted = crypto_js_1.default.AES.encrypt(dataWords, keyWords, {
59
+ iv: ivWords,
60
+ mode: crypto_js_1.default.mode.CBC,
61
+ padding: crypto_js_1.default.pad.NoPadding,
62
+ });
63
+ const hex = encrypted.ciphertext.toString();
64
+ const result = new Uint8Array(hex.length / 2);
65
+ for (let i = 0; i < result.length; i++) {
66
+ result[i] = parseInt(hex.substr(i * 2, 2), 16);
67
+ }
68
+ return result;
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Byte-array helpers
72
+ // ---------------------------------------------------------------------------
73
+ /**
74
+ * Left-rotate a byte array by 8 bits (1 byte).
75
+ * e.g. [1,2,3,4] → [2,3,4,1]
76
+ * Used in AES 3-pass auth to compute RndB' and verify RndA'.
77
+ */
78
+ function rotateLeft8(data) {
79
+ const result = new Uint8Array(data.length);
80
+ for (let i = 0; i < data.length - 1; i++) {
81
+ result[i] = data[i + 1];
82
+ }
83
+ result[data.length - 1] = data[0];
84
+ return result;
85
+ }
86
+ /** Compare two Uint8Arrays for equality. */
87
+ function arraysEqual(a, b) {
88
+ if (a.length !== b.length)
89
+ return false;
90
+ for (let i = 0; i < a.length; i++) {
91
+ if (a[i] !== b[i])
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Secure random
98
+ // ---------------------------------------------------------------------------
99
+ /**
100
+ * Generate a 16-byte cryptographically-secure random value.
101
+ *
102
+ * Uses `crypto.getRandomValues` when available (Hermes ≥ 0.72, or
103
+ * the `react-native-get-random-values` polyfill). Falls back to
104
+ * `Math.random()` with a console warning if the API is missing.
105
+ */
106
+ function generateRandom16() {
107
+ const arr = new Uint8Array(16);
108
+ if (typeof globalThis.crypto !== 'undefined' &&
109
+ typeof globalThis.crypto.getRandomValues === 'function') {
110
+ globalThis.crypto.getRandomValues(arr);
111
+ }
112
+ else {
113
+ console.warn('[nfc-litecard] crypto.getRandomValues is not available. ' +
114
+ 'Falling back to Math.random() which is NOT cryptographically secure. ' +
115
+ 'Consider adding the react-native-get-random-values polyfill.');
116
+ for (let i = 0; i < 16; i++) {
117
+ arr[i] = Math.floor(Math.random() * 256);
118
+ }
119
+ }
120
+ return arr;
121
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @ukeyfe/react-native-nfc-litecard
3
+ *
4
+ * NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
5
+ */
6
+ export { ResultCode, type NfcResult } from './types';
7
+ export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, } from './reader';
8
+ export { initializeCard, updateCard, updatePassword, writeUserNickname, resetCard, } from './writer';
9
+ export { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, } from './nfc-core';
10
+ export { DEFAULT_PIN_RETRY_COUNT } from './constants';