@ukeyfe/react-native-nfc-litecard 1.0.2 → 1.0.3
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 +71 -34
- package/README.zh.md +275 -238
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +33 -1
- package/dist/crypto.d.ts +22 -0
- package/dist/crypto.js +165 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -3
- package/dist/nfc-core.d.ts +9 -0
- package/dist/nfc-core.js +97 -9
- package/dist/reader.d.ts +13 -2
- package/dist/reader.js +153 -66
- package/dist/types.d.ts +16 -4
- package/dist/types.js +26 -24
- package/dist/utils.d.ts +16 -0
- package/dist/utils.js +36 -28
- package/dist/writer.d.ts +9 -4
- package/dist/writer.js +95 -82
- package/package.json +1 -1
package/dist/constants.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export declare const CMD_FAST_READ = 58;
|
|
|
11
11
|
export declare const CMD_AUTH_PART1 = 26;
|
|
12
12
|
/** AES authentication part 2 */
|
|
13
13
|
export declare const CMD_AUTH_PART2 = 175;
|
|
14
|
+
/** GET_VERSION command - retrieve product version info */
|
|
15
|
+
export declare const CMD_GET_VERSION = 96;
|
|
16
|
+
/** READ_SIG command - read ECC originality signature */
|
|
17
|
+
export declare const CMD_READ_SIG = 60;
|
|
14
18
|
/** Data protection key slot (Key0) */
|
|
15
19
|
export declare const KEY_NO_DATA_PROT = 0;
|
|
16
20
|
/** Bytes per page */
|
|
@@ -59,3 +63,25 @@ export declare const RETRY_COUNTER_PAGE: number;
|
|
|
59
63
|
export declare const RETRY_COUNTER_OFFSET: number;
|
|
60
64
|
/** Default retry limit, restored after a successful authentication */
|
|
61
65
|
export declare const DEFAULT_PIN_RETRY_COUNT = 10;
|
|
66
|
+
/** SEC_MSG_ACT bit mask in CFG0 byte 0 (bit 1) */
|
|
67
|
+
export declare const SEC_MSG_ACT_MASK = 2;
|
|
68
|
+
export declare const DELAY: {
|
|
69
|
+
/** Delay after requestNfcTech on Android */
|
|
70
|
+
readonly ANDROID_POST_TECH: 300;
|
|
71
|
+
/** Delay after releaseNfcTech (normal) */
|
|
72
|
+
readonly IOS_POST_RELEASE: 100;
|
|
73
|
+
readonly ANDROID_POST_RELEASE: 800;
|
|
74
|
+
/** Delay after releaseNfcTech (forced, e.g. after timeout) */
|
|
75
|
+
readonly IOS_POST_RELEASE_FORCED: 300;
|
|
76
|
+
readonly ANDROID_POST_RELEASE_FORCED: 1000;
|
|
77
|
+
/** Delay between page writes on iOS */
|
|
78
|
+
readonly IOS_PAGE_WRITE: 20;
|
|
79
|
+
/** Delay between AES key page writes on iOS */
|
|
80
|
+
readonly IOS_KEY_WRITE: 10;
|
|
81
|
+
/** Delay between nickname page writes on iOS */
|
|
82
|
+
readonly IOS_NICKNAME_WRITE: 100;
|
|
83
|
+
/** Delay between config operations on iOS */
|
|
84
|
+
readonly IOS_CONFIG: 80;
|
|
85
|
+
/** Keep-alive frequency (every N pages) during writes */
|
|
86
|
+
readonly KEEPALIVE_FREQ: 4;
|
|
87
|
+
};
|
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_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;
|
|
6
|
+
exports.DELAY = exports.SEC_MSG_ACT_MASK = 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_READ_SIG = exports.CMD_GET_VERSION = 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
|
// ---------------------------------------------------------------------------
|
|
@@ -17,6 +17,10 @@ exports.CMD_FAST_READ = 0x3a;
|
|
|
17
17
|
exports.CMD_AUTH_PART1 = 0x1a;
|
|
18
18
|
/** AES authentication part 2 */
|
|
19
19
|
exports.CMD_AUTH_PART2 = 0xaf;
|
|
20
|
+
/** GET_VERSION command - retrieve product version info */
|
|
21
|
+
exports.CMD_GET_VERSION = 0x60;
|
|
22
|
+
/** READ_SIG command - read ECC originality signature */
|
|
23
|
+
exports.CMD_READ_SIG = 0x3c;
|
|
20
24
|
/** Data protection key slot (Key0) */
|
|
21
25
|
exports.KEY_NO_DATA_PROT = 0x00;
|
|
22
26
|
// ---------------------------------------------------------------------------
|
|
@@ -83,3 +87,31 @@ exports.RETRY_COUNTER_PAGE = exports.USER_PAGE_START - 1; // 0x07
|
|
|
83
87
|
exports.RETRY_COUNTER_OFFSET = exports.PAGE_SIZE - 1;
|
|
84
88
|
/** Default retry limit, restored after a successful authentication */
|
|
85
89
|
exports.DEFAULT_PIN_RETRY_COUNT = 10;
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Secure messaging (CMAC)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/** SEC_MSG_ACT bit mask in CFG0 byte 0 (bit 1) */
|
|
94
|
+
exports.SEC_MSG_ACT_MASK = 0x02;
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Platform-specific delays (ms)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
exports.DELAY = {
|
|
99
|
+
/** Delay after requestNfcTech on Android */
|
|
100
|
+
ANDROID_POST_TECH: 300,
|
|
101
|
+
/** Delay after releaseNfcTech (normal) */
|
|
102
|
+
IOS_POST_RELEASE: 100,
|
|
103
|
+
ANDROID_POST_RELEASE: 800,
|
|
104
|
+
/** Delay after releaseNfcTech (forced, e.g. after timeout) */
|
|
105
|
+
IOS_POST_RELEASE_FORCED: 300,
|
|
106
|
+
ANDROID_POST_RELEASE_FORCED: 1000,
|
|
107
|
+
/** Delay between page writes on iOS */
|
|
108
|
+
IOS_PAGE_WRITE: 20,
|
|
109
|
+
/** Delay between AES key page writes on iOS */
|
|
110
|
+
IOS_KEY_WRITE: 10,
|
|
111
|
+
/** Delay between nickname page writes on iOS */
|
|
112
|
+
IOS_NICKNAME_WRITE: 100,
|
|
113
|
+
/** Delay between config operations on iOS */
|
|
114
|
+
IOS_CONFIG: 80,
|
|
115
|
+
/** Keep-alive frequency (every N pages) during writes */
|
|
116
|
+
KEEPALIVE_FREQ: 4,
|
|
117
|
+
};
|
package/dist/crypto.d.ts
CHANGED
|
@@ -23,6 +23,28 @@ export declare function aesEncrypt(key: Uint8Array, data: Uint8Array, iv: Uint8A
|
|
|
23
23
|
export declare function rotateLeft8(data: Uint8Array): Uint8Array;
|
|
24
24
|
/** Compare two Uint8Arrays for equality. */
|
|
25
25
|
export declare function arraysEqual(a: Uint8Array, b: Uint8Array): boolean;
|
|
26
|
+
/** AES-128 ECB encrypt a single 16-byte block. */
|
|
27
|
+
export declare function aesEcbEncrypt(key: Uint8Array, data: Uint8Array): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Generate CMAC subkeys K1 and K2 (NIST 800-38B §6.1).
|
|
30
|
+
*/
|
|
31
|
+
export declare function generateCmacSubkeys(key: Uint8Array): {
|
|
32
|
+
k1: Uint8Array;
|
|
33
|
+
k2: Uint8Array;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Compute AES-CMAC (NIST 800-38B §6.2).
|
|
37
|
+
* Returns full 16-byte MAC.
|
|
38
|
+
*/
|
|
39
|
+
export declare function aesCmac(key: Uint8Array, message: Uint8Array): Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* Derive SesAuthMACKey from authentication key and random numbers.
|
|
42
|
+
* (MF0AES(H)20 §8.8.1)
|
|
43
|
+
*
|
|
44
|
+
* SV2 = 5Ah||A5h||00h||01h||00h||80h||RndA[15..14]||(RndA[13..8] XOR RndB[15..10])||RndB[9..0]||RndA[7..0]
|
|
45
|
+
* SesAuthMACKey = AES-CMAC(Kx, SV2)
|
|
46
|
+
*/
|
|
47
|
+
export declare function deriveSessionKey(authKey: Uint8Array, rndA: Uint8Array, rndB: Uint8Array): Uint8Array;
|
|
26
48
|
/**
|
|
27
49
|
* Generate a 16-byte cryptographically-secure random value.
|
|
28
50
|
*
|
package/dist/crypto.js
CHANGED
|
@@ -16,6 +16,10 @@ exports.aesDecrypt = aesDecrypt;
|
|
|
16
16
|
exports.aesEncrypt = aesEncrypt;
|
|
17
17
|
exports.rotateLeft8 = rotateLeft8;
|
|
18
18
|
exports.arraysEqual = arraysEqual;
|
|
19
|
+
exports.aesEcbEncrypt = aesEcbEncrypt;
|
|
20
|
+
exports.generateCmacSubkeys = generateCmacSubkeys;
|
|
21
|
+
exports.aesCmac = aesCmac;
|
|
22
|
+
exports.deriveSessionKey = deriveSessionKey;
|
|
19
23
|
exports.generateRandom16 = generateRandom16;
|
|
20
24
|
const crypto_js_1 = __importDefault(require("crypto-js"));
|
|
21
25
|
// ---------------------------------------------------------------------------
|
|
@@ -30,7 +34,7 @@ function passwordToAesKey(password) {
|
|
|
30
34
|
const hash = crypto_js_1.default.SHA256(String(safePassword)).toString();
|
|
31
35
|
const key = new Uint8Array(16);
|
|
32
36
|
for (let i = 0; i < 16; i++) {
|
|
33
|
-
key[i] = parseInt(hash.
|
|
37
|
+
key[i] = parseInt(hash.substring(i * 2, i * 2 + 2), 16);
|
|
34
38
|
}
|
|
35
39
|
return key;
|
|
36
40
|
}
|
|
@@ -46,7 +50,7 @@ function aesDecrypt(key, data, iv) {
|
|
|
46
50
|
const hex = decrypted.toString();
|
|
47
51
|
const result = new Uint8Array(16);
|
|
48
52
|
for (let i = 0; i < 16; i++) {
|
|
49
|
-
result[i] = parseInt(hex.
|
|
53
|
+
result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
50
54
|
}
|
|
51
55
|
return result;
|
|
52
56
|
}
|
|
@@ -63,7 +67,7 @@ function aesEncrypt(key, data, iv) {
|
|
|
63
67
|
const hex = encrypted.ciphertext.toString();
|
|
64
68
|
const result = new Uint8Array(hex.length / 2);
|
|
65
69
|
for (let i = 0; i < result.length; i++) {
|
|
66
|
-
result[i] = parseInt(hex.
|
|
70
|
+
result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
67
71
|
}
|
|
68
72
|
return result;
|
|
69
73
|
}
|
|
@@ -94,6 +98,158 @@ function arraysEqual(a, b) {
|
|
|
94
98
|
return true;
|
|
95
99
|
}
|
|
96
100
|
// ---------------------------------------------------------------------------
|
|
101
|
+
// AES-128 ECB (single block, for CMAC subkey generation)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/** AES-128 ECB encrypt a single 16-byte block. */
|
|
104
|
+
function aesEcbEncrypt(key, data) {
|
|
105
|
+
const keyWords = crypto_js_1.default.lib.WordArray.create(key);
|
|
106
|
+
const dataWords = crypto_js_1.default.lib.WordArray.create(data);
|
|
107
|
+
const encrypted = crypto_js_1.default.AES.encrypt(dataWords, keyWords, {
|
|
108
|
+
mode: crypto_js_1.default.mode.ECB,
|
|
109
|
+
padding: crypto_js_1.default.pad.NoPadding,
|
|
110
|
+
});
|
|
111
|
+
const hex = encrypted.ciphertext.toString();
|
|
112
|
+
const result = new Uint8Array(16);
|
|
113
|
+
for (let i = 0; i < 16; i++) {
|
|
114
|
+
result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// AES-CMAC (NIST SP 800-38B)
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
const CONST_RB = 0x87; // constant for 128-bit block cipher
|
|
122
|
+
/** Left-shift a 16-byte array by 1 bit. */
|
|
123
|
+
function shiftLeft1(data) {
|
|
124
|
+
const result = new Uint8Array(16);
|
|
125
|
+
let carry = 0;
|
|
126
|
+
for (let i = 15; i >= 0; i--) {
|
|
127
|
+
const shifted = (data[i] << 1) | carry;
|
|
128
|
+
result[i] = shifted & 0xff;
|
|
129
|
+
carry = (data[i] & 0x80) ? 1 : 0;
|
|
130
|
+
}
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
/** XOR two 16-byte arrays. */
|
|
134
|
+
function xor16(a, b) {
|
|
135
|
+
const result = new Uint8Array(16);
|
|
136
|
+
for (let i = 0; i < 16; i++) {
|
|
137
|
+
result[i] = a[i] ^ b[i];
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Generate CMAC subkeys K1 and K2 (NIST 800-38B §6.1).
|
|
143
|
+
*/
|
|
144
|
+
function generateCmacSubkeys(key) {
|
|
145
|
+
const zero = new Uint8Array(16);
|
|
146
|
+
const L = aesEcbEncrypt(key, zero);
|
|
147
|
+
let k1 = shiftLeft1(L);
|
|
148
|
+
if (L[0] & 0x80) {
|
|
149
|
+
k1[15] ^= CONST_RB;
|
|
150
|
+
}
|
|
151
|
+
let k2 = shiftLeft1(k1);
|
|
152
|
+
if (k1[0] & 0x80) {
|
|
153
|
+
k2[15] ^= CONST_RB;
|
|
154
|
+
}
|
|
155
|
+
return { k1, k2 };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Compute AES-CMAC (NIST 800-38B §6.2).
|
|
159
|
+
* Returns full 16-byte MAC.
|
|
160
|
+
*/
|
|
161
|
+
function aesCmac(key, message) {
|
|
162
|
+
const { k1, k2 } = generateCmacSubkeys(key);
|
|
163
|
+
const n = message.length === 0 ? 1 : Math.ceil(message.length / 16);
|
|
164
|
+
const lastBlockComplete = message.length > 0 && message.length % 16 === 0;
|
|
165
|
+
// Prepare last block
|
|
166
|
+
const lastBlock = new Uint8Array(16);
|
|
167
|
+
if (lastBlockComplete) {
|
|
168
|
+
// Complete: XOR with K1
|
|
169
|
+
const offset = (n - 1) * 16;
|
|
170
|
+
for (let i = 0; i < 16; i++) {
|
|
171
|
+
lastBlock[i] = message[offset + i] ^ k1[i];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// Incomplete: pad with 10* then XOR with K2
|
|
176
|
+
const offset = (n - 1) * 16;
|
|
177
|
+
const remaining = message.length - offset;
|
|
178
|
+
for (let i = 0; i < 16; i++) {
|
|
179
|
+
if (i < remaining) {
|
|
180
|
+
lastBlock[i] = message[offset + i];
|
|
181
|
+
}
|
|
182
|
+
else if (i === remaining) {
|
|
183
|
+
lastBlock[i] = 0x80;
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
lastBlock[i] = 0x00;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < 16; i++) {
|
|
190
|
+
lastBlock[i] ^= k2[i];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// CBC-MAC
|
|
194
|
+
const iv0 = new Uint8Array(16);
|
|
195
|
+
let x = iv0;
|
|
196
|
+
for (let i = 0; i < n - 1; i++) {
|
|
197
|
+
const block = new Uint8Array(message.slice(i * 16, (i + 1) * 16));
|
|
198
|
+
const y = xor16(x, block);
|
|
199
|
+
x = aesEcbEncrypt(key, y);
|
|
200
|
+
}
|
|
201
|
+
const y = xor16(x, lastBlock);
|
|
202
|
+
return aesEcbEncrypt(key, y);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Derive SesAuthMACKey from authentication key and random numbers.
|
|
206
|
+
* (MF0AES(H)20 §8.8.1)
|
|
207
|
+
*
|
|
208
|
+
* SV2 = 5Ah||A5h||00h||01h||00h||80h||RndA[15..14]||(RndA[13..8] XOR RndB[15..10])||RndB[9..0]||RndA[7..0]
|
|
209
|
+
* SesAuthMACKey = AES-CMAC(Kx, SV2)
|
|
210
|
+
*/
|
|
211
|
+
function deriveSessionKey(authKey, rndA, rndB) {
|
|
212
|
+
const sv2 = new Uint8Array(32);
|
|
213
|
+
// Header: 5Ah A5h 00h 01h 00h 80h
|
|
214
|
+
sv2[0] = 0x5a;
|
|
215
|
+
sv2[1] = 0xa5;
|
|
216
|
+
sv2[2] = 0x00;
|
|
217
|
+
sv2[3] = 0x01;
|
|
218
|
+
sv2[4] = 0x00;
|
|
219
|
+
sv2[5] = 0x80;
|
|
220
|
+
// RndA[15..14] (2 bytes)
|
|
221
|
+
sv2[6] = rndA[15];
|
|
222
|
+
sv2[7] = rndA[14];
|
|
223
|
+
// RndA[13..8] XOR RndB[15..10] (6 bytes)
|
|
224
|
+
sv2[8] = rndA[13] ^ rndB[15];
|
|
225
|
+
sv2[9] = rndA[12] ^ rndB[14];
|
|
226
|
+
sv2[10] = rndA[11] ^ rndB[13];
|
|
227
|
+
sv2[11] = rndA[10] ^ rndB[12];
|
|
228
|
+
sv2[12] = rndA[9] ^ rndB[11];
|
|
229
|
+
sv2[13] = rndA[8] ^ rndB[10];
|
|
230
|
+
// RndB[9..0] (10 bytes)
|
|
231
|
+
sv2[14] = rndB[9];
|
|
232
|
+
sv2[15] = rndB[8];
|
|
233
|
+
sv2[16] = rndB[7];
|
|
234
|
+
sv2[17] = rndB[6];
|
|
235
|
+
sv2[18] = rndB[5];
|
|
236
|
+
sv2[19] = rndB[4];
|
|
237
|
+
sv2[20] = rndB[3];
|
|
238
|
+
sv2[21] = rndB[2];
|
|
239
|
+
sv2[22] = rndB[1];
|
|
240
|
+
sv2[23] = rndB[0];
|
|
241
|
+
// RndA[7..0] (8 bytes)
|
|
242
|
+
sv2[24] = rndA[7];
|
|
243
|
+
sv2[25] = rndA[6];
|
|
244
|
+
sv2[26] = rndA[5];
|
|
245
|
+
sv2[27] = rndA[4];
|
|
246
|
+
sv2[28] = rndA[3];
|
|
247
|
+
sv2[29] = rndA[2];
|
|
248
|
+
sv2[30] = rndA[1];
|
|
249
|
+
sv2[31] = rndA[0];
|
|
250
|
+
return aesCmac(authKey, sv2);
|
|
251
|
+
}
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
97
253
|
// Secure random
|
|
98
254
|
// ---------------------------------------------------------------------------
|
|
99
255
|
/**
|
|
@@ -104,18 +260,12 @@ function arraysEqual(a, b) {
|
|
|
104
260
|
* `Math.random()` with a console warning if the API is missing.
|
|
105
261
|
*/
|
|
106
262
|
function generateRandom16() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
263
|
+
if (typeof globalThis.crypto === 'undefined' ||
|
|
264
|
+
typeof globalThis.crypto.getRandomValues !== 'function') {
|
|
265
|
+
throw new Error('[nfc-litecard] crypto.getRandomValues is not available. ' +
|
|
266
|
+
'Please add the react-native-get-random-values polyfill or use Hermes >= 0.72.');
|
|
119
267
|
}
|
|
268
|
+
const arr = new Uint8Array(16);
|
|
269
|
+
globalThis.crypto.getRandomValues(arr);
|
|
120
270
|
return arr;
|
|
121
271
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
|
|
5
5
|
*/
|
|
6
|
-
export {
|
|
7
|
-
export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, } from './reader';
|
|
6
|
+
export { NfcStatusCode, type NfcResult, nfcResultRetryCountExhausted, } from './types';
|
|
7
|
+
export { checkCard, readMnemonic, readUserNickname, readMnemonicRetryCount, resetRetryCountTo10, cardInfoToJson, getCardVersion, readOriginality, } from './reader';
|
|
8
8
|
export { initializeCard, updateCard, updatePassword, writeUserNickname, resetCard, } from './writer';
|
|
9
9
|
export { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, } from './nfc-core';
|
|
10
10
|
export { DEFAULT_PIN_RETRY_COUNT } from './constants';
|
package/dist/index.js
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
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.nfcResultRetryCountExhausted = exports.
|
|
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.readOriginality = exports.getCardVersion = exports.cardInfoToJson = exports.resetRetryCountTo10 = exports.readMnemonicRetryCount = exports.readUserNickname = exports.readMnemonic = exports.checkCard = exports.nfcResultRetryCountExhausted = exports.NfcStatusCode = void 0;
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
// Unified types & constants (consumers can use a single
|
|
10
|
+
// Unified types & constants (consumers can use a single NfcStatusCode)
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
var types_1 = require("./types");
|
|
13
|
-
Object.defineProperty(exports, "
|
|
13
|
+
Object.defineProperty(exports, "NfcStatusCode", { enumerable: true, get: function () { return types_1.NfcStatusCode; } });
|
|
14
14
|
Object.defineProperty(exports, "nfcResultRetryCountExhausted", { enumerable: true, get: function () { return types_1.nfcResultRetryCountExhausted; } });
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Reader API
|
|
@@ -22,6 +22,8 @@ Object.defineProperty(exports, "readUserNickname", { enumerable: true, get: func
|
|
|
22
22
|
Object.defineProperty(exports, "readMnemonicRetryCount", { enumerable: true, get: function () { return reader_1.readMnemonicRetryCount; } });
|
|
23
23
|
Object.defineProperty(exports, "resetRetryCountTo10", { enumerable: true, get: function () { return reader_1.resetRetryCountTo10; } });
|
|
24
24
|
Object.defineProperty(exports, "cardInfoToJson", { enumerable: true, get: function () { return reader_1.cardInfoToJson; } });
|
|
25
|
+
Object.defineProperty(exports, "getCardVersion", { enumerable: true, get: function () { return reader_1.getCardVersion; } });
|
|
26
|
+
Object.defineProperty(exports, "readOriginality", { enumerable: true, get: function () { return reader_1.readOriginality; } });
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Writer API
|
|
27
29
|
// ---------------------------------------------------------------------------
|
package/dist/nfc-core.d.ts
CHANGED
|
@@ -23,9 +23,16 @@ export declare function markNfcOperationCancelledByCleanup(): void;
|
|
|
23
23
|
export declare function consumeNfcOperationCancelledByCleanup(): boolean;
|
|
24
24
|
export declare function getNfcOperationCancelledByCleanupTimestamp(): number;
|
|
25
25
|
export declare function clearNfcOperationCancelledByCleanup(): void;
|
|
26
|
+
/** Clear CMAC session state. Called on NFC release. */
|
|
27
|
+
export declare function clearCmacSession(): void;
|
|
26
28
|
/**
|
|
27
29
|
* Send a command to the NFC tag and return the response.
|
|
28
30
|
*
|
|
31
|
+
* When CMAC session is active:
|
|
32
|
+
* - Appends 8-byte CMAC to outgoing commands
|
|
33
|
+
* - Verifies and strips 8-byte CMAC from responses
|
|
34
|
+
* - Increments CmdCtr after each command-response pair
|
|
35
|
+
*
|
|
29
36
|
* Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
|
|
30
37
|
*/
|
|
31
38
|
export declare function transceive(command: number[], _timeoutMs?: number, retryCount?: number): Promise<number[]>;
|
|
@@ -53,6 +60,8 @@ export declare function releaseNfcTech(forceLongDelay?: boolean): Promise<void>;
|
|
|
53
60
|
* @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
|
|
54
61
|
*/
|
|
55
62
|
export declare function authenticate(key: Uint8Array): Promise<void>;
|
|
63
|
+
/** FAST_READ pages 0x08–0x27 (user memory). */
|
|
64
|
+
export declare function readUserMemory(): Promise<Uint8Array>;
|
|
56
65
|
/**
|
|
57
66
|
* Decrement the on-card retry counter by 1 (clamped to 0).
|
|
58
67
|
* Must be called within an active NFC session (between requestNfcTech and releaseNfcTech).
|
package/dist/nfc-core.js
CHANGED
|
@@ -53,10 +53,12 @@ exports.markNfcOperationCancelledByCleanup = markNfcOperationCancelledByCleanup;
|
|
|
53
53
|
exports.consumeNfcOperationCancelledByCleanup = consumeNfcOperationCancelledByCleanup;
|
|
54
54
|
exports.getNfcOperationCancelledByCleanupTimestamp = getNfcOperationCancelledByCleanupTimestamp;
|
|
55
55
|
exports.clearNfcOperationCancelledByCleanup = clearNfcOperationCancelledByCleanup;
|
|
56
|
+
exports.clearCmacSession = clearCmacSession;
|
|
56
57
|
exports.transceive = transceive;
|
|
57
58
|
exports.requestNfcTech = requestNfcTech;
|
|
58
59
|
exports.releaseNfcTech = releaseNfcTech;
|
|
59
60
|
exports.authenticate = authenticate;
|
|
61
|
+
exports.readUserMemory = readUserMemory;
|
|
60
62
|
exports.decrementRetryCountInSession = decrementRetryCountInSession;
|
|
61
63
|
exports.writeRetryCountInSession = writeRetryCountInSession;
|
|
62
64
|
const react_native_nfc_manager_1 = __importStar(require("react-native-nfc-manager"));
|
|
@@ -117,23 +119,93 @@ function clearNfcOperationCancelledByCleanup() {
|
|
|
117
119
|
nfcCancelledByCleanupTimestamp = 0;
|
|
118
120
|
}
|
|
119
121
|
// ===========================================================================
|
|
122
|
+
// CMAC secure messaging session state
|
|
123
|
+
// ===========================================================================
|
|
124
|
+
let cmacSessionKey = null;
|
|
125
|
+
let cmacCmdCtr = 0;
|
|
126
|
+
/** Clear CMAC session state. Called on NFC release. */
|
|
127
|
+
function clearCmacSession() {
|
|
128
|
+
cmacSessionKey = null;
|
|
129
|
+
cmacCmdCtr = 0;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Compute the 8-byte truncated CMAC for an NFC command.
|
|
133
|
+
* Input: CmdCtr (2 bytes LE) || CmdCode (1 byte) || Arguments
|
|
134
|
+
*/
|
|
135
|
+
function computeCommandCmac(sessionKey, cmdCtr, command) {
|
|
136
|
+
const ctrLo = cmdCtr & 0xff;
|
|
137
|
+
const ctrHi = (cmdCtr >> 8) & 0xff;
|
|
138
|
+
const input = new Uint8Array(2 + command.length);
|
|
139
|
+
input[0] = ctrLo;
|
|
140
|
+
input[1] = ctrHi;
|
|
141
|
+
input.set(command, 2);
|
|
142
|
+
const mac = (0, crypto_1.aesCmac)(sessionKey, input);
|
|
143
|
+
return mac.slice(0, 8); // truncate to 8 bytes
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Compute the 8-byte truncated CMAC for an NFC response.
|
|
147
|
+
* Input: CmdCtr (2 bytes LE) || ResponseData
|
|
148
|
+
*/
|
|
149
|
+
function computeResponseCmac(sessionKey, cmdCtr, responseData) {
|
|
150
|
+
const ctrLo = cmdCtr & 0xff;
|
|
151
|
+
const ctrHi = (cmdCtr >> 8) & 0xff;
|
|
152
|
+
const input = new Uint8Array(2 + responseData.length);
|
|
153
|
+
input[0] = ctrLo;
|
|
154
|
+
input[1] = ctrHi;
|
|
155
|
+
input.set(responseData, 2);
|
|
156
|
+
const mac = (0, crypto_1.aesCmac)(sessionKey, input);
|
|
157
|
+
return mac.slice(0, 8);
|
|
158
|
+
}
|
|
159
|
+
// ===========================================================================
|
|
120
160
|
// Low-level NFC transceive
|
|
121
161
|
// ===========================================================================
|
|
122
162
|
/**
|
|
123
163
|
* Send a command to the NFC tag and return the response.
|
|
124
164
|
*
|
|
165
|
+
* When CMAC session is active:
|
|
166
|
+
* - Appends 8-byte CMAC to outgoing commands
|
|
167
|
+
* - Verifies and strips 8-byte CMAC from responses
|
|
168
|
+
* - Increments CmdCtr after each command-response pair
|
|
169
|
+
*
|
|
125
170
|
* Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
|
|
126
171
|
*/
|
|
127
172
|
async function transceive(command, _timeoutMs = 2000, retryCount = 0) {
|
|
173
|
+
// Build the actual command to send
|
|
174
|
+
let cmdToSend = command;
|
|
175
|
+
if (cmacSessionKey) {
|
|
176
|
+
const mac = computeCommandCmac(cmacSessionKey, cmacCmdCtr, command);
|
|
177
|
+
cmdToSend = [...command, ...Array.from(mac)];
|
|
178
|
+
}
|
|
128
179
|
try {
|
|
129
180
|
const result = react_native_1.Platform.OS === 'ios'
|
|
130
|
-
? await react_native_nfc_manager_1.default.sendMifareCommandIOS(
|
|
131
|
-
: await react_native_nfc_manager_1.default.nfcAHandler.transceive(
|
|
181
|
+
? await react_native_nfc_manager_1.default.sendMifareCommandIOS(cmdToSend)
|
|
182
|
+
: await react_native_nfc_manager_1.default.nfcAHandler.transceive(cmdToSend);
|
|
183
|
+
if (cmacSessionKey && result && result.length >= 8) {
|
|
184
|
+
// Response has CMAC appended: split data and MAC
|
|
185
|
+
const dataLen = result.length - 8;
|
|
186
|
+
const responseData = result.slice(0, dataLen);
|
|
187
|
+
const responseMac = result.slice(dataLen);
|
|
188
|
+
// Verify response CMAC (constant-time comparison)
|
|
189
|
+
const expectedMac = computeResponseCmac(cmacSessionKey, cmacCmdCtr, responseData);
|
|
190
|
+
let macValid = true;
|
|
191
|
+
for (let i = 0; i < 8; i++) {
|
|
192
|
+
macValid = macValid && (responseMac[i] === expectedMac[i]);
|
|
193
|
+
}
|
|
194
|
+
if (!macValid) {
|
|
195
|
+
throw new Error('CMAC_VERIFY_FAILED');
|
|
196
|
+
}
|
|
197
|
+
cmacCmdCtr++;
|
|
198
|
+
return responseData;
|
|
199
|
+
}
|
|
200
|
+
if (cmacSessionKey) {
|
|
201
|
+
// ACK-only response (e.g. WRITE command) — MAC replaces ACK
|
|
202
|
+
cmacCmdCtr++;
|
|
203
|
+
}
|
|
132
204
|
return result;
|
|
133
205
|
}
|
|
134
206
|
catch (error) {
|
|
135
207
|
const maxRetries = react_native_1.Platform.OS === 'ios' ? 1 : 2;
|
|
136
|
-
if (retryCount < maxRetries) {
|
|
208
|
+
if (retryCount < maxRetries && error?.message !== 'CMAC_VERIFY_FAILED') {
|
|
137
209
|
const wait = 200 * (retryCount + 1);
|
|
138
210
|
await new Promise(r => setTimeout(r, wait));
|
|
139
211
|
return transceive(command, _timeoutMs, retryCount + 1);
|
|
@@ -158,7 +230,7 @@ async function requestNfcTech() {
|
|
|
158
230
|
await react_native_nfc_manager_1.default.requestTechnology(react_native_nfc_manager_1.NfcTech.NfcA);
|
|
159
231
|
}
|
|
160
232
|
if (react_native_1.Platform.OS === 'android') {
|
|
161
|
-
await new Promise(r => setTimeout(r,
|
|
233
|
+
await new Promise(r => setTimeout(r, constants_1.DELAY.ANDROID_POST_TECH));
|
|
162
234
|
}
|
|
163
235
|
else {
|
|
164
236
|
// iOS: probe connection availability
|
|
@@ -175,11 +247,12 @@ async function requestNfcTech() {
|
|
|
175
247
|
* @param forceLongDelay Use a longer post-release delay (e.g. after timeout / tag-lost).
|
|
176
248
|
*/
|
|
177
249
|
async function releaseNfcTech(forceLongDelay = false) {
|
|
250
|
+
clearCmacSession();
|
|
178
251
|
try {
|
|
179
252
|
await react_native_nfc_manager_1.default.cancelTechnologyRequest();
|
|
180
253
|
const delay = react_native_1.Platform.OS === 'ios'
|
|
181
|
-
? (forceLongDelay ?
|
|
182
|
-
: (forceLongDelay ?
|
|
254
|
+
? (forceLongDelay ? constants_1.DELAY.IOS_POST_RELEASE_FORCED : constants_1.DELAY.IOS_POST_RELEASE)
|
|
255
|
+
: (forceLongDelay ? constants_1.DELAY.ANDROID_POST_RELEASE_FORCED : constants_1.DELAY.ANDROID_POST_RELEASE);
|
|
183
256
|
await new Promise(r => setTimeout(r, delay));
|
|
184
257
|
}
|
|
185
258
|
catch (error) {
|
|
@@ -202,6 +275,9 @@ async function releaseNfcTech(forceLongDelay = false) {
|
|
|
202
275
|
* @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
|
|
203
276
|
*/
|
|
204
277
|
async function authenticate(key) {
|
|
278
|
+
// Clear any previous CMAC session before starting auth
|
|
279
|
+
// (auth commands themselves are never CMAC-protected)
|
|
280
|
+
clearCmacSession();
|
|
205
281
|
const iv0 = new Uint8Array(16);
|
|
206
282
|
// Step 1
|
|
207
283
|
const response1 = await transceive([constants_1.CMD_AUTH_PART1, constants_1.KEY_NO_DATA_PROT]);
|
|
@@ -224,15 +300,14 @@ async function authenticate(key) {
|
|
|
224
300
|
ekRndAB.set(ekPart1, 0);
|
|
225
301
|
ekRndAB.set(ekPart2, 16);
|
|
226
302
|
// Step 5
|
|
227
|
-
let response2
|
|
303
|
+
let response2;
|
|
228
304
|
try {
|
|
229
305
|
response2 = await transceive([constants_1.CMD_AUTH_PART2, ...Array.from(ekRndAB)]);
|
|
230
306
|
}
|
|
231
307
|
catch (_) {
|
|
232
|
-
// AUTH_PART2 transceive failure usually means wrong password
|
|
233
308
|
throw new Error('AUTH_WRONG_PASSWORD');
|
|
234
309
|
}
|
|
235
|
-
if (
|
|
310
|
+
if (response2.length < 17 || response2[0] !== 0x00) {
|
|
236
311
|
throw new Error('AUTH_WRONG_PASSWORD');
|
|
237
312
|
}
|
|
238
313
|
// Step 6 – verify RndA'
|
|
@@ -242,6 +317,19 @@ async function authenticate(key) {
|
|
|
242
317
|
if (!(0, crypto_1.arraysEqual)(rndARot, expectedRndARot)) {
|
|
243
318
|
throw new Error('AUTH_VERIFY_FAILED');
|
|
244
319
|
}
|
|
320
|
+
// Step 7 – derive CMAC session key and enable secure messaging
|
|
321
|
+
cmacSessionKey = (0, crypto_1.deriveSessionKey)(key, rndA, rndB);
|
|
322
|
+
cmacCmdCtr = 0;
|
|
323
|
+
}
|
|
324
|
+
// ===========================================================================
|
|
325
|
+
// Shared memory read helpers
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
/** FAST_READ pages 0x08–0x27 (user memory). */
|
|
328
|
+
async function readUserMemory() {
|
|
329
|
+
const response = await transceive([constants_1.CMD_FAST_READ, constants_1.USER_PAGE_START, constants_1.USER_PAGE_END]);
|
|
330
|
+
if (!response || response.length < constants_1.USER_MEMORY_SIZE)
|
|
331
|
+
throw new Error('READ_FAILED');
|
|
332
|
+
return new Uint8Array(response.slice(0, constants_1.USER_MEMORY_SIZE));
|
|
245
333
|
}
|
|
246
334
|
// ===========================================================================
|
|
247
335
|
// PIN retry-counter session helpers
|
package/dist/reader.d.ts
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* Based on MIFARE Ultralight AES (MF0AES(H)20) datasheet.
|
|
13
13
|
*/
|
|
14
14
|
import { DEFAULT_PIN_RETRY_COUNT } from './constants';
|
|
15
|
-
import {
|
|
15
|
+
import { NfcStatusCode, type NfcResult } from './types';
|
|
16
16
|
import { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup } from './nfc-core';
|
|
17
|
-
export {
|
|
17
|
+
export { NfcStatusCode, type NfcResult, isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, DEFAULT_PIN_RETRY_COUNT, };
|
|
18
18
|
declare function parseCardInfo(data: Uint8Array): {
|
|
19
19
|
version: number;
|
|
20
20
|
cardType: number;
|
|
@@ -85,3 +85,14 @@ export declare function readUserNickname(password?: string, onCardIdentified?: (
|
|
|
85
85
|
export declare function readMnemonicRetryCount(onCardIdentified?: () => void): Promise<NfcResult>;
|
|
86
86
|
/** Reset the PIN retry counter to the default value (10). */
|
|
87
87
|
export declare function resetRetryCountTo10(onCardIdentified?: () => void): Promise<NfcResult>;
|
|
88
|
+
/**
|
|
89
|
+
* Read card product version info (GET_VERSION command).
|
|
90
|
+
* No authentication required.
|
|
91
|
+
*/
|
|
92
|
+
export declare function getCardVersion(onCardIdentified?: () => void): Promise<NfcResult>;
|
|
93
|
+
/**
|
|
94
|
+
* Read the ECC originality signature (READ_SIG command).
|
|
95
|
+
* Verifies the card is a genuine NXP product.
|
|
96
|
+
* No authentication required (unless Random ID is enabled).
|
|
97
|
+
*/
|
|
98
|
+
export declare function readOriginality(onCardIdentified?: () => void): Promise<NfcResult>;
|