@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 +390 -0
- package/README.md +390 -0
- package/dist/constants.d.ts +57 -0
- package/dist/constants.js +78 -0
- package/dist/crypto.d.ts +33 -0
- package/dist/crypto.js +121 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +47 -0
- package/dist/nfc-core.d.ts +65 -0
- package/dist/nfc-core.js +277 -0
- package/dist/reader.d.ts +82 -0
- package/dist/reader.js +488 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.js +94 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +63 -0
- package/dist/writer.d.ts +32 -0
- package/dist/writer.js +545 -0
- package/package.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @ukeyfe/react-native-nfc-litecard
|
|
4
|
+
*
|
|
5
|
+
* NFC read/write library for MIFARE Ultralight AES (LiteCard mnemonic storage).
|
|
6
|
+
*/
|
|
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;
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Unified types & constants (consumers can use a single ResultCode)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
var types_1 = require("./types");
|
|
13
|
+
Object.defineProperty(exports, "ResultCode", { enumerable: true, get: function () { return types_1.ResultCode; } });
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Reader API
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
var reader_1 = require("./reader");
|
|
18
|
+
Object.defineProperty(exports, "checkCard", { enumerable: true, get: function () { return reader_1.checkCard; } });
|
|
19
|
+
Object.defineProperty(exports, "readMnemonic", { enumerable: true, get: function () { return reader_1.readMnemonic; } });
|
|
20
|
+
Object.defineProperty(exports, "readUserNickname", { enumerable: true, get: function () { return reader_1.readUserNickname; } });
|
|
21
|
+
Object.defineProperty(exports, "readMnemonicRetryCount", { enumerable: true, get: function () { return reader_1.readMnemonicRetryCount; } });
|
|
22
|
+
Object.defineProperty(exports, "resetRetryCountTo10", { enumerable: true, get: function () { return reader_1.resetRetryCountTo10; } });
|
|
23
|
+
Object.defineProperty(exports, "cardInfoToJson", { enumerable: true, get: function () { return reader_1.cardInfoToJson; } });
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Writer API
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
var writer_1 = require("./writer");
|
|
28
|
+
Object.defineProperty(exports, "initializeCard", { enumerable: true, get: function () { return writer_1.initializeCard; } });
|
|
29
|
+
Object.defineProperty(exports, "updateCard", { enumerable: true, get: function () { return writer_1.updateCard; } });
|
|
30
|
+
Object.defineProperty(exports, "updatePassword", { enumerable: true, get: function () { return writer_1.updatePassword; } });
|
|
31
|
+
Object.defineProperty(exports, "writeUserNickname", { enumerable: true, get: function () { return writer_1.writeUserNickname; } });
|
|
32
|
+
Object.defineProperty(exports, "resetCard", { enumerable: true, get: function () { return writer_1.resetCard; } });
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// NFC lock & cleanup helpers (for app-level lifecycle management)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
var nfc_core_1 = require("./nfc-core");
|
|
37
|
+
Object.defineProperty(exports, "isNfcOperationLocked", { enumerable: true, get: function () { return nfc_core_1.isNfcOperationLocked; } });
|
|
38
|
+
Object.defineProperty(exports, "releaseNfcOperationLock", { enumerable: true, get: function () { return nfc_core_1.releaseNfcOperationLock; } });
|
|
39
|
+
Object.defineProperty(exports, "markNfcOperationCancelledByCleanup", { enumerable: true, get: function () { return nfc_core_1.markNfcOperationCancelledByCleanup; } });
|
|
40
|
+
Object.defineProperty(exports, "consumeNfcOperationCancelledByCleanup", { enumerable: true, get: function () { return nfc_core_1.consumeNfcOperationCancelledByCleanup; } });
|
|
41
|
+
Object.defineProperty(exports, "getNfcOperationCancelledByCleanupTimestamp", { enumerable: true, get: function () { return nfc_core_1.getNfcOperationCancelledByCleanupTimestamp; } });
|
|
42
|
+
Object.defineProperty(exports, "clearNfcOperationCancelledByCleanup", { enumerable: true, get: function () { return nfc_core_1.clearNfcOperationCancelledByCleanup; } });
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Constants that apps may need
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
var constants_1 = require("./constants");
|
|
47
|
+
Object.defineProperty(exports, "DEFAULT_PIN_RETRY_COUNT", { enumerable: true, get: function () { return constants_1.DEFAULT_PIN_RETRY_COUNT; } });
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core NFC communication layer – shared by reader and writer.
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - A single global operation lock (prevents reader/writer concurrency)
|
|
6
|
+
* - Page-cleanup cancellation flags
|
|
7
|
+
* - Low-level transceive with platform-aware retries
|
|
8
|
+
* - NFC technology request / release
|
|
9
|
+
* - AES 3-pass mutual authentication (MF0AES(H)20 §8.6.1)
|
|
10
|
+
* - PIN retry-counter helpers (session-level read/write)
|
|
11
|
+
*/
|
|
12
|
+
import { passwordToAesKey } from './crypto';
|
|
13
|
+
export { passwordToAesKey };
|
|
14
|
+
/**
|
|
15
|
+
* Acquire the NFC operation lock.
|
|
16
|
+
* If another operation is in progress, polls every 100 ms for up to 10 s.
|
|
17
|
+
*/
|
|
18
|
+
export declare function acquireNfcLock(): Promise<void>;
|
|
19
|
+
export declare function releaseNfcLock(): void;
|
|
20
|
+
export declare function isNfcOperationLocked(): boolean;
|
|
21
|
+
export declare function releaseNfcOperationLock(): void;
|
|
22
|
+
export declare function markNfcOperationCancelledByCleanup(): void;
|
|
23
|
+
export declare function consumeNfcOperationCancelledByCleanup(): boolean;
|
|
24
|
+
export declare function getNfcOperationCancelledByCleanupTimestamp(): number;
|
|
25
|
+
export declare function clearNfcOperationCancelledByCleanup(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Send a command to the NFC tag and return the response.
|
|
28
|
+
*
|
|
29
|
+
* Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
|
|
30
|
+
*/
|
|
31
|
+
export declare function transceive(command: number[], _timeoutMs?: number, retryCount?: number): Promise<number[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Request an NFC technology session (MifareIOS on iOS, NfcA on Android).
|
|
34
|
+
*
|
|
35
|
+
* All prior cleanup must be done by the caller BEFORE invoking this.
|
|
36
|
+
*/
|
|
37
|
+
export declare function requestNfcTech(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Release the NFC technology session.
|
|
40
|
+
* @param forceLongDelay Use a longer post-release delay (e.g. after timeout / tag-lost).
|
|
41
|
+
*/
|
|
42
|
+
export declare function releaseNfcTech(forceLongDelay?: boolean): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Perform AES 3-pass mutual authentication (MF0AES(H)20 §8.6.1).
|
|
45
|
+
*
|
|
46
|
+
* 1. PCD → PICC : AUTH_PART1 (0x1A) + KeyNo
|
|
47
|
+
* 2. PICC → PCD : 0xAF + ek(RndB)
|
|
48
|
+
* 3. PCD decrypts RndB, generates RndA, computes RndB' = rotateLeft8(RndB)
|
|
49
|
+
* 4. PCD → PICC : AUTH_PART2 (0xAF) + ek(RndA ‖ RndB')
|
|
50
|
+
* 5. PICC → PCD : 0x00 + ek(RndA')
|
|
51
|
+
* 6. PCD verifies RndA' === rotateLeft8(RndA)
|
|
52
|
+
*
|
|
53
|
+
* @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
|
|
54
|
+
*/
|
|
55
|
+
export declare function authenticate(key: Uint8Array): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* Decrement the on-card retry counter by 1 (clamped to 0).
|
|
58
|
+
* Must be called within an active NFC session (between requestNfcTech and releaseNfcTech).
|
|
59
|
+
*/
|
|
60
|
+
export declare function decrementRetryCountInSession(): Promise<number | undefined>;
|
|
61
|
+
/**
|
|
62
|
+
* Write a specific retry count to the on-card counter.
|
|
63
|
+
* Must be called within an active NFC session.
|
|
64
|
+
*/
|
|
65
|
+
export declare function writeRetryCountInSession(count: number): Promise<void>;
|
package/dist/nfc-core.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Core NFC communication layer – shared by reader and writer.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - A single global operation lock (prevents reader/writer concurrency)
|
|
7
|
+
* - Page-cleanup cancellation flags
|
|
8
|
+
* - Low-level transceive with platform-aware retries
|
|
9
|
+
* - NFC technology request / release
|
|
10
|
+
* - AES 3-pass mutual authentication (MF0AES(H)20 §8.6.1)
|
|
11
|
+
* - PIN retry-counter helpers (session-level read/write)
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.passwordToAesKey = void 0;
|
|
48
|
+
exports.acquireNfcLock = acquireNfcLock;
|
|
49
|
+
exports.releaseNfcLock = releaseNfcLock;
|
|
50
|
+
exports.isNfcOperationLocked = isNfcOperationLocked;
|
|
51
|
+
exports.releaseNfcOperationLock = releaseNfcOperationLock;
|
|
52
|
+
exports.markNfcOperationCancelledByCleanup = markNfcOperationCancelledByCleanup;
|
|
53
|
+
exports.consumeNfcOperationCancelledByCleanup = consumeNfcOperationCancelledByCleanup;
|
|
54
|
+
exports.getNfcOperationCancelledByCleanupTimestamp = getNfcOperationCancelledByCleanupTimestamp;
|
|
55
|
+
exports.clearNfcOperationCancelledByCleanup = clearNfcOperationCancelledByCleanup;
|
|
56
|
+
exports.transceive = transceive;
|
|
57
|
+
exports.requestNfcTech = requestNfcTech;
|
|
58
|
+
exports.releaseNfcTech = releaseNfcTech;
|
|
59
|
+
exports.authenticate = authenticate;
|
|
60
|
+
exports.decrementRetryCountInSession = decrementRetryCountInSession;
|
|
61
|
+
exports.writeRetryCountInSession = writeRetryCountInSession;
|
|
62
|
+
const react_native_nfc_manager_1 = __importStar(require("react-native-nfc-manager"));
|
|
63
|
+
const react_native_1 = require("react-native");
|
|
64
|
+
const constants_1 = require("./constants");
|
|
65
|
+
const crypto_1 = require("./crypto");
|
|
66
|
+
Object.defineProperty(exports, "passwordToAesKey", { enumerable: true, get: function () { return crypto_1.passwordToAesKey; } });
|
|
67
|
+
// ===========================================================================
|
|
68
|
+
// Global NFC operation lock (single instance for both reader & writer)
|
|
69
|
+
// ===========================================================================
|
|
70
|
+
let nfcOperationLock = false;
|
|
71
|
+
/**
|
|
72
|
+
* Acquire the NFC operation lock.
|
|
73
|
+
* If another operation is in progress, polls every 100 ms for up to 10 s.
|
|
74
|
+
*/
|
|
75
|
+
async function acquireNfcLock() {
|
|
76
|
+
const maxWait = 10000;
|
|
77
|
+
const interval = 100;
|
|
78
|
+
let waited = 0;
|
|
79
|
+
while (nfcOperationLock) {
|
|
80
|
+
if (waited >= maxWait)
|
|
81
|
+
throw new Error('NFC_LOCK_TIMEOUT');
|
|
82
|
+
await new Promise(r => setTimeout(r, interval));
|
|
83
|
+
waited += interval;
|
|
84
|
+
}
|
|
85
|
+
nfcOperationLock = true;
|
|
86
|
+
clearNfcOperationCancelledByCleanup();
|
|
87
|
+
}
|
|
88
|
+
function releaseNfcLock() {
|
|
89
|
+
nfcOperationLock = false;
|
|
90
|
+
}
|
|
91
|
+
function isNfcOperationLocked() {
|
|
92
|
+
return nfcOperationLock;
|
|
93
|
+
}
|
|
94
|
+
function releaseNfcOperationLock() {
|
|
95
|
+
releaseNfcLock();
|
|
96
|
+
}
|
|
97
|
+
// ===========================================================================
|
|
98
|
+
// Page-cleanup cancellation flag
|
|
99
|
+
// ===========================================================================
|
|
100
|
+
let nfcCancelledByCleanup = false;
|
|
101
|
+
let nfcCancelledByCleanupTimestamp = 0;
|
|
102
|
+
function markNfcOperationCancelledByCleanup() {
|
|
103
|
+
nfcCancelledByCleanup = true;
|
|
104
|
+
nfcCancelledByCleanupTimestamp = Date.now();
|
|
105
|
+
}
|
|
106
|
+
function consumeNfcOperationCancelledByCleanup() {
|
|
107
|
+
const cancelled = nfcCancelledByCleanup;
|
|
108
|
+
nfcCancelledByCleanup = false;
|
|
109
|
+
nfcCancelledByCleanupTimestamp = 0;
|
|
110
|
+
return cancelled;
|
|
111
|
+
}
|
|
112
|
+
function getNfcOperationCancelledByCleanupTimestamp() {
|
|
113
|
+
return nfcCancelledByCleanupTimestamp;
|
|
114
|
+
}
|
|
115
|
+
function clearNfcOperationCancelledByCleanup() {
|
|
116
|
+
nfcCancelledByCleanup = false;
|
|
117
|
+
nfcCancelledByCleanupTimestamp = 0;
|
|
118
|
+
}
|
|
119
|
+
// ===========================================================================
|
|
120
|
+
// Low-level NFC transceive
|
|
121
|
+
// ===========================================================================
|
|
122
|
+
/**
|
|
123
|
+
* Send a command to the NFC tag and return the response.
|
|
124
|
+
*
|
|
125
|
+
* Retries: Android up to 2 retries (3 total), iOS up to 1 retry (2 total).
|
|
126
|
+
*/
|
|
127
|
+
async function transceive(command, _timeoutMs = 2000, retryCount = 0) {
|
|
128
|
+
try {
|
|
129
|
+
const result = react_native_1.Platform.OS === 'ios'
|
|
130
|
+
? await react_native_nfc_manager_1.default.sendMifareCommandIOS(command)
|
|
131
|
+
: await react_native_nfc_manager_1.default.nfcAHandler.transceive(command);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const maxRetries = react_native_1.Platform.OS === 'ios' ? 1 : 2;
|
|
136
|
+
if (retryCount < maxRetries) {
|
|
137
|
+
const wait = 200 * (retryCount + 1);
|
|
138
|
+
await new Promise(r => setTimeout(r, wait));
|
|
139
|
+
return transceive(command, _timeoutMs, retryCount + 1);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ===========================================================================
|
|
145
|
+
// NFC technology connect / disconnect
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
/**
|
|
148
|
+
* Request an NFC technology session (MifareIOS on iOS, NfcA on Android).
|
|
149
|
+
*
|
|
150
|
+
* All prior cleanup must be done by the caller BEFORE invoking this.
|
|
151
|
+
*/
|
|
152
|
+
async function requestNfcTech() {
|
|
153
|
+
await new Promise(r => setTimeout(r, 100));
|
|
154
|
+
if (react_native_1.Platform.OS === 'ios') {
|
|
155
|
+
await react_native_nfc_manager_1.default.requestTechnology(react_native_nfc_manager_1.NfcTech.MifareIOS);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
await react_native_nfc_manager_1.default.requestTechnology(react_native_nfc_manager_1.NfcTech.NfcA);
|
|
159
|
+
}
|
|
160
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
161
|
+
await new Promise(r => setTimeout(r, 300));
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
// iOS: probe connection availability
|
|
165
|
+
try {
|
|
166
|
+
await react_native_nfc_manager_1.default.sendMifareCommandIOS([constants_1.CMD_READ, 0x00]);
|
|
167
|
+
}
|
|
168
|
+
catch (_) {
|
|
169
|
+
// Non-fatal – continue even if the probe fails
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Release the NFC technology session.
|
|
175
|
+
* @param forceLongDelay Use a longer post-release delay (e.g. after timeout / tag-lost).
|
|
176
|
+
*/
|
|
177
|
+
async function releaseNfcTech(forceLongDelay = false) {
|
|
178
|
+
try {
|
|
179
|
+
await react_native_nfc_manager_1.default.cancelTechnologyRequest();
|
|
180
|
+
const delay = react_native_1.Platform.OS === 'ios'
|
|
181
|
+
? (forceLongDelay ? 300 : 100)
|
|
182
|
+
: (forceLongDelay ? 1000 : 800);
|
|
183
|
+
await new Promise(r => setTimeout(r, delay));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
console.warn('[nfc-core] releaseNfcTech failed:', error?.message);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ===========================================================================
|
|
190
|
+
// AES 3-pass mutual authentication
|
|
191
|
+
// ===========================================================================
|
|
192
|
+
/**
|
|
193
|
+
* Perform AES 3-pass mutual authentication (MF0AES(H)20 §8.6.1).
|
|
194
|
+
*
|
|
195
|
+
* 1. PCD → PICC : AUTH_PART1 (0x1A) + KeyNo
|
|
196
|
+
* 2. PICC → PCD : 0xAF + ek(RndB)
|
|
197
|
+
* 3. PCD decrypts RndB, generates RndA, computes RndB' = rotateLeft8(RndB)
|
|
198
|
+
* 4. PCD → PICC : AUTH_PART2 (0xAF) + ek(RndA ‖ RndB')
|
|
199
|
+
* 5. PICC → PCD : 0x00 + ek(RndA')
|
|
200
|
+
* 6. PCD verifies RndA' === rotateLeft8(RndA)
|
|
201
|
+
*
|
|
202
|
+
* @throws AUTH_INVALID_RESPONSE / AUTH_WRONG_PASSWORD / AUTH_VERIFY_FAILED
|
|
203
|
+
*/
|
|
204
|
+
async function authenticate(key) {
|
|
205
|
+
const iv0 = new Uint8Array(16);
|
|
206
|
+
// Step 1
|
|
207
|
+
const response1 = await transceive([constants_1.CMD_AUTH_PART1, constants_1.KEY_NO_DATA_PROT]);
|
|
208
|
+
if (!response1 || response1.length < 17 || response1[0] !== 0xaf) {
|
|
209
|
+
throw new Error('AUTH_INVALID_RESPONSE');
|
|
210
|
+
}
|
|
211
|
+
// Step 2 – decrypt RndB
|
|
212
|
+
const ekRndB = new Uint8Array(response1.slice(1, 17));
|
|
213
|
+
const rndB = (0, crypto_1.aesDecrypt)(key, ekRndB, iv0);
|
|
214
|
+
// Step 3
|
|
215
|
+
const rndBRot = (0, crypto_1.rotateLeft8)(rndB);
|
|
216
|
+
const rndA = (0, crypto_1.generateRandom16)();
|
|
217
|
+
// Step 4 – encrypt RndA ‖ RndB' (CBC, two 16-byte blocks)
|
|
218
|
+
const rndAB = new Uint8Array(32);
|
|
219
|
+
rndAB.set(rndA, 0);
|
|
220
|
+
rndAB.set(rndBRot, 16);
|
|
221
|
+
const ekPart1 = (0, crypto_1.aesEncrypt)(key, rndAB.slice(0, 16), iv0);
|
|
222
|
+
const ekPart2 = (0, crypto_1.aesEncrypt)(key, rndAB.slice(16, 32), ekPart1);
|
|
223
|
+
const ekRndAB = new Uint8Array(32);
|
|
224
|
+
ekRndAB.set(ekPart1, 0);
|
|
225
|
+
ekRndAB.set(ekPart2, 16);
|
|
226
|
+
// Step 5
|
|
227
|
+
let response2 = null;
|
|
228
|
+
try {
|
|
229
|
+
response2 = await transceive([constants_1.CMD_AUTH_PART2, ...Array.from(ekRndAB)]);
|
|
230
|
+
}
|
|
231
|
+
catch (_) {
|
|
232
|
+
// AUTH_PART2 transceive failure usually means wrong password
|
|
233
|
+
throw new Error('AUTH_WRONG_PASSWORD');
|
|
234
|
+
}
|
|
235
|
+
if (!response2 || response2.length < 17 || response2[0] !== 0x00) {
|
|
236
|
+
throw new Error('AUTH_WRONG_PASSWORD');
|
|
237
|
+
}
|
|
238
|
+
// Step 6 – verify RndA'
|
|
239
|
+
const ekRndARot = new Uint8Array(response2.slice(1, 17));
|
|
240
|
+
const rndARot = (0, crypto_1.aesDecrypt)(key, ekRndARot, iv0);
|
|
241
|
+
const expectedRndARot = (0, crypto_1.rotateLeft8)(rndA);
|
|
242
|
+
if (!(0, crypto_1.arraysEqual)(rndARot, expectedRndARot)) {
|
|
243
|
+
throw new Error('AUTH_VERIFY_FAILED');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// ===========================================================================
|
|
247
|
+
// PIN retry-counter session helpers
|
|
248
|
+
// ===========================================================================
|
|
249
|
+
/**
|
|
250
|
+
* Decrement the on-card retry counter by 1 (clamped to 0).
|
|
251
|
+
* Must be called within an active NFC session (between requestNfcTech and releaseNfcTech).
|
|
252
|
+
*/
|
|
253
|
+
async function decrementRetryCountInSession() {
|
|
254
|
+
const pageBlock = await transceive([constants_1.CMD_READ, constants_1.RETRY_COUNTER_PAGE]);
|
|
255
|
+
if (!pageBlock || pageBlock.length < constants_1.PAGE_SIZE)
|
|
256
|
+
return undefined;
|
|
257
|
+
const page = pageBlock.slice(0, constants_1.PAGE_SIZE);
|
|
258
|
+
const raw = page[constants_1.RETRY_COUNTER_OFFSET];
|
|
259
|
+
const current = typeof raw === 'number' ? raw & 0xff : 0;
|
|
260
|
+
const next = current > 0 ? current - 1 : 0;
|
|
261
|
+
page[constants_1.RETRY_COUNTER_OFFSET] = next & 0xff;
|
|
262
|
+
await transceive([constants_1.CMD_WRITE, constants_1.RETRY_COUNTER_PAGE, ...page]);
|
|
263
|
+
return next;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Write a specific retry count to the on-card counter.
|
|
267
|
+
* Must be called within an active NFC session.
|
|
268
|
+
*/
|
|
269
|
+
async function writeRetryCountInSession(count) {
|
|
270
|
+
const safeCount = Math.max(0, Math.min(0xff, count | 0));
|
|
271
|
+
const pageBlock = await transceive([constants_1.CMD_READ, constants_1.RETRY_COUNTER_PAGE]);
|
|
272
|
+
if (!pageBlock || pageBlock.length < constants_1.PAGE_SIZE)
|
|
273
|
+
throw new Error('READ_FAILED');
|
|
274
|
+
const page = pageBlock.slice(0, constants_1.PAGE_SIZE);
|
|
275
|
+
page[constants_1.RETRY_COUNTER_OFFSET] = safeCount & 0xff;
|
|
276
|
+
await transceive([constants_1.CMD_WRITE, constants_1.RETRY_COUNTER_PAGE, ...page]);
|
|
277
|
+
}
|
package/dist/reader.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NFC Reader Module
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* - checkCard() – detect whether a card is empty or contains data
|
|
6
|
+
* - readMnemonic() – read mnemonic (password required)
|
|
7
|
+
* - readUserNickname() – read user nickname (password optional)
|
|
8
|
+
* - readMnemonicRetryCount() – read the PIN retry counter
|
|
9
|
+
* - resetRetryCountTo10() – reset the retry counter to default
|
|
10
|
+
* - cardInfoToJson() – serialise card-info to JSON
|
|
11
|
+
*
|
|
12
|
+
* Based on MIFARE Ultralight AES (MF0AES(H)20) datasheet.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_PIN_RETRY_COUNT } from './constants';
|
|
15
|
+
import { ResultCode, type NfcResult } from './types';
|
|
16
|
+
import { isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup } from './nfc-core';
|
|
17
|
+
export { ResultCode, type NfcResult, isNfcOperationLocked, releaseNfcOperationLock, markNfcOperationCancelledByCleanup, consumeNfcOperationCancelledByCleanup, getNfcOperationCancelledByCleanupTimestamp, clearNfcOperationCancelledByCleanup, DEFAULT_PIN_RETRY_COUNT, };
|
|
18
|
+
declare function parseCardInfo(data: Uint8Array): {
|
|
19
|
+
version: number;
|
|
20
|
+
cardType: number;
|
|
21
|
+
rawBytes: string;
|
|
22
|
+
infoBytes: Uint8Array<ArrayBuffer>;
|
|
23
|
+
json: {
|
|
24
|
+
pageInfo: {
|
|
25
|
+
startPage: number;
|
|
26
|
+
endPage: number;
|
|
27
|
+
totalPages: number;
|
|
28
|
+
totalBytes: number;
|
|
29
|
+
};
|
|
30
|
+
version: {
|
|
31
|
+
value: number;
|
|
32
|
+
hex: string;
|
|
33
|
+
};
|
|
34
|
+
cardType: {
|
|
35
|
+
value: number;
|
|
36
|
+
hex: string;
|
|
37
|
+
description: string;
|
|
38
|
+
known: boolean;
|
|
39
|
+
};
|
|
40
|
+
additionalInfo: {
|
|
41
|
+
hex: string;
|
|
42
|
+
bytes: number[];
|
|
43
|
+
};
|
|
44
|
+
rawData: {
|
|
45
|
+
hex: string;
|
|
46
|
+
bytes: number[];
|
|
47
|
+
length: number;
|
|
48
|
+
};
|
|
49
|
+
pageBreakdown: {
|
|
50
|
+
page: number;
|
|
51
|
+
bytes: number[];
|
|
52
|
+
hex: string;
|
|
53
|
+
}[];
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/** Serialise parseCardInfo result to JSON. */
|
|
57
|
+
export declare function cardInfoToJson(cardInfo: ReturnType<typeof parseCardInfo>, pretty?: boolean): string;
|
|
58
|
+
/**
|
|
59
|
+
* Detect whether the card is empty or already contains data.
|
|
60
|
+
*
|
|
61
|
+
* Logic:
|
|
62
|
+
* - Read succeeds & first byte is a valid mnemonic type → HAS_DATA
|
|
63
|
+
* - Read succeeds & first byte is other → EMPTY
|
|
64
|
+
* - Read fails (auth required) → HAS_DATA (read-protection is on)
|
|
65
|
+
*/
|
|
66
|
+
export declare function checkCard(onCardIdentified?: () => void): Promise<NfcResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Read the mnemonic from a password-protected card.
|
|
69
|
+
*
|
|
70
|
+
* Flow: connect → pre-decrement retry counter → authenticate → read →
|
|
71
|
+
* decode entropy → read nickname → reset retry counter → release
|
|
72
|
+
*/
|
|
73
|
+
export declare function readMnemonic(password: string, onCardIdentified?: () => void): Promise<NfcResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Read the user nickname from the card.
|
|
76
|
+
* @param password – supply if the card has read-protection enabled.
|
|
77
|
+
*/
|
|
78
|
+
export declare function readUserNickname(password?: string): Promise<NfcResult>;
|
|
79
|
+
/** Read the current PIN retry counter from the card. */
|
|
80
|
+
export declare function readMnemonicRetryCount(): Promise<NfcResult>;
|
|
81
|
+
/** Reset the PIN retry counter to the default value (10). */
|
|
82
|
+
export declare function resetRetryCountTo10(): Promise<NfcResult>;
|