smartcard 3.7.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/reader_monitor.cpp +167 -47
- package/src/reader_monitor.h +6 -3
package/README.md
CHANGED
|
@@ -517,3 +517,4 @@ MIT
|
|
|
517
517
|
## Related Projects
|
|
518
518
|
|
|
519
519
|
- [nfc-pcsc](https://www.npmjs.com/package/nfc-pcsc) - NFC library built on smartcard
|
|
520
|
+
- [emv](https://github.com/tomkp/emv) - Interactive EMV chip card explorer built on smartcard. Features a terminal UI for discovering payment applications, reading card data, verifying PINs, and exploring EMV tag structures. Supports GET PROCESSING OPTIONS, application cryptogram generation (ARQC/TC), and Dynamic Data Authentication (DDA).
|
package/package.json
CHANGED
package/src/reader_monitor.cpp
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#include "reader_monitor.h"
|
|
2
2
|
#include "pcsc_errors.h"
|
|
3
3
|
#include <cstring>
|
|
4
|
+
#include <unordered_map>
|
|
5
|
+
#include <memory>
|
|
4
6
|
|
|
5
7
|
Napi::FunctionReference ReaderMonitor::constructor;
|
|
6
8
|
|
|
9
|
+
// Number of iterations between forced full state refreshes (Windows reliability fix)
|
|
10
|
+
static const int STATE_REFRESH_INTERVAL = 10;
|
|
11
|
+
|
|
7
12
|
// Event data passed from worker thread to JS thread
|
|
8
13
|
struct EventData {
|
|
9
14
|
std::string eventType;
|
|
@@ -123,7 +128,7 @@ Napi::Value ReaderMonitor::Stop(const Napi::CallbackInfo& info) {
|
|
|
123
128
|
context_ = 0;
|
|
124
129
|
}
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
readerStates_.clear();
|
|
127
132
|
|
|
128
133
|
return env.Undefined();
|
|
129
134
|
}
|
|
@@ -139,28 +144,84 @@ void ReaderMonitor::MonitorLoop() {
|
|
|
139
144
|
// Emit reader-attached events for all pre-existing readers (Issue #30)
|
|
140
145
|
{
|
|
141
146
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
142
|
-
for (const auto&
|
|
143
|
-
EmitEvent("reader-attached",
|
|
147
|
+
for (const auto& pair : readerStates_) {
|
|
148
|
+
EmitEvent("reader-attached", pair.first, pair.second.lastState, pair.second.atr);
|
|
144
149
|
}
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
// Build initial states array with PnP notification
|
|
148
153
|
std::vector<SCARD_READERSTATE> states;
|
|
149
154
|
std::vector<std::string> readerNames;
|
|
155
|
+
int iterationCount = 0;
|
|
150
156
|
|
|
151
157
|
while (running_) {
|
|
152
|
-
//
|
|
158
|
+
// Periodic full state refresh to handle Windows PC/SC state drift (Issue #111)
|
|
159
|
+
// This ensures we don't miss events if the state tracking gets out of sync
|
|
160
|
+
if (++iterationCount >= STATE_REFRESH_INTERVAL) {
|
|
161
|
+
iterationCount = 0;
|
|
162
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
163
|
+
|
|
164
|
+
// Get fresh state for all readers
|
|
165
|
+
std::vector<SCARD_READERSTATE> refreshStates;
|
|
166
|
+
std::vector<std::string> refreshNames;
|
|
167
|
+
|
|
168
|
+
for (const auto& pair : readerStates_) {
|
|
169
|
+
refreshNames.push_back(pair.first);
|
|
170
|
+
SCARD_READERSTATE state = {};
|
|
171
|
+
state.szReader = refreshNames.back().c_str();
|
|
172
|
+
state.dwCurrentState = SCARD_STATE_UNAWARE; // Force fresh state
|
|
173
|
+
refreshStates.push_back(state);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!refreshStates.empty()) {
|
|
177
|
+
LONG refreshResult = SCardGetStatusChange(context_, 0, refreshStates.data(), refreshStates.size());
|
|
178
|
+
if (refreshResult == SCARD_S_SUCCESS) {
|
|
179
|
+
// Check for state divergence and emit missed events
|
|
180
|
+
for (size_t i = 0; i < refreshStates.size(); i++) {
|
|
181
|
+
const std::string& name = refreshNames[i];
|
|
182
|
+
auto it = readerStates_.find(name);
|
|
183
|
+
if (it != readerStates_.end()) {
|
|
184
|
+
DWORD oldState = it->second.lastState;
|
|
185
|
+
DWORD newState = refreshStates[i].dwEventState & ~SCARD_STATE_CHANGED;
|
|
186
|
+
|
|
187
|
+
bool wasPresent = (oldState & SCARD_STATE_PRESENT) != 0;
|
|
188
|
+
bool isPresent = (newState & SCARD_STATE_PRESENT) != 0;
|
|
189
|
+
|
|
190
|
+
if (wasPresent != isPresent) {
|
|
191
|
+
// State diverged - emit missed event
|
|
192
|
+
std::vector<uint8_t> atr;
|
|
193
|
+
if (refreshStates[i].cbAtr > 0) {
|
|
194
|
+
atr.assign(refreshStates[i].rgbAtr,
|
|
195
|
+
refreshStates[i].rgbAtr + refreshStates[i].cbAtr);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
it->second.lastState = newState;
|
|
199
|
+
it->second.atr = atr;
|
|
200
|
+
|
|
201
|
+
if (isPresent) {
|
|
202
|
+
EmitEvent("card-inserted", name, newState, atr);
|
|
203
|
+
} else {
|
|
204
|
+
EmitEvent("card-removed", name, newState, {});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Build states array using reader names from our map
|
|
153
214
|
{
|
|
154
215
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
155
216
|
states.clear();
|
|
156
217
|
readerNames.clear();
|
|
157
218
|
|
|
158
|
-
// Add known readers
|
|
159
|
-
for (auto&
|
|
219
|
+
// Add known readers - use the map for name-based lookup
|
|
220
|
+
for (const auto& pair : readerStates_) {
|
|
160
221
|
SCARD_READERSTATE state = {};
|
|
161
|
-
readerNames.push_back(
|
|
222
|
+
readerNames.push_back(pair.first);
|
|
162
223
|
state.szReader = readerNames.back().c_str();
|
|
163
|
-
state.dwCurrentState =
|
|
224
|
+
state.dwCurrentState = pair.second.lastState;
|
|
164
225
|
states.push_back(state);
|
|
165
226
|
}
|
|
166
227
|
|
|
@@ -184,7 +245,61 @@ void ReaderMonitor::MonitorLoop() {
|
|
|
184
245
|
}
|
|
185
246
|
|
|
186
247
|
if (result == SCARD_E_TIMEOUT) {
|
|
187
|
-
//
|
|
248
|
+
// Timeout - query fresh state to detect missed events (Issue #111)
|
|
249
|
+
// On Windows, dwEventState after timeout may just mirror dwCurrentState
|
|
250
|
+
// rather than reflecting actual hardware state. We must explicitly
|
|
251
|
+
// query with SCARD_STATE_UNAWARE to get the real current state.
|
|
252
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
253
|
+
|
|
254
|
+
if (readerStates_.empty()) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Build fresh state query for all readers
|
|
259
|
+
std::vector<SCARD_READERSTATE> freshStates;
|
|
260
|
+
std::vector<std::string> freshNames;
|
|
261
|
+
|
|
262
|
+
for (const auto& pair : readerStates_) {
|
|
263
|
+
freshNames.push_back(pair.first);
|
|
264
|
+
SCARD_READERSTATE state = {};
|
|
265
|
+
state.szReader = freshNames.back().c_str();
|
|
266
|
+
state.dwCurrentState = SCARD_STATE_UNAWARE;
|
|
267
|
+
freshStates.push_back(state);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
LONG freshResult = SCardGetStatusChange(context_, 0, freshStates.data(), freshStates.size());
|
|
271
|
+
if (freshResult != SCARD_S_SUCCESS) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (size_t i = 0; i < freshStates.size(); i++) {
|
|
276
|
+
const std::string& name = freshNames[i];
|
|
277
|
+
auto it = readerStates_.find(name);
|
|
278
|
+
if (it != readerStates_.end()) {
|
|
279
|
+
DWORD freshState = freshStates[i].dwEventState & ~SCARD_STATE_CHANGED;
|
|
280
|
+
DWORD storedState = it->second.lastState;
|
|
281
|
+
|
|
282
|
+
bool wasPresent = (storedState & SCARD_STATE_PRESENT) != 0;
|
|
283
|
+
bool isPresent = (freshState & SCARD_STATE_PRESENT) != 0;
|
|
284
|
+
|
|
285
|
+
if (wasPresent != isPresent) {
|
|
286
|
+
std::vector<uint8_t> atr;
|
|
287
|
+
if (freshStates[i].cbAtr > 0) {
|
|
288
|
+
atr.assign(freshStates[i].rgbAtr,
|
|
289
|
+
freshStates[i].rgbAtr + freshStates[i].cbAtr);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
it->second.lastState = freshState;
|
|
293
|
+
it->second.atr = atr;
|
|
294
|
+
|
|
295
|
+
if (isPresent) {
|
|
296
|
+
EmitEvent("card-inserted", name, freshState, atr);
|
|
297
|
+
} else {
|
|
298
|
+
EmitEvent("card-removed", name, freshState, {});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
188
303
|
continue;
|
|
189
304
|
}
|
|
190
305
|
|
|
@@ -195,8 +310,9 @@ void ReaderMonitor::MonitorLoop() {
|
|
|
195
310
|
continue;
|
|
196
311
|
}
|
|
197
312
|
|
|
198
|
-
// Process changes
|
|
313
|
+
// Process changes - use reader name for lookup (Issue #111 fix)
|
|
199
314
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
315
|
+
bool pnpTriggered = false;
|
|
200
316
|
|
|
201
317
|
for (size_t i = 0; i < states.size(); i++) {
|
|
202
318
|
if (!(states[i].dwEventState & SCARD_STATE_CHANGED)) {
|
|
@@ -205,48 +321,52 @@ void ReaderMonitor::MonitorLoop() {
|
|
|
205
321
|
|
|
206
322
|
// PnP notification - reader list changed
|
|
207
323
|
if (readerNames[i] == "\\\\?PnP?\\Notification") {
|
|
324
|
+
pnpTriggered = true;
|
|
208
325
|
// Get old reader names
|
|
209
326
|
std::vector<std::string> oldNames;
|
|
210
|
-
for (const auto&
|
|
211
|
-
oldNames.push_back(
|
|
327
|
+
for (const auto& pair : readerStates_) {
|
|
328
|
+
oldNames.push_back(pair.first);
|
|
212
329
|
}
|
|
213
330
|
|
|
214
|
-
// Update reader list
|
|
331
|
+
// Update reader list (this rebuilds readerStates_ map)
|
|
215
332
|
UpdateReaderList();
|
|
216
333
|
|
|
217
334
|
// Find new readers
|
|
218
|
-
for (const auto&
|
|
335
|
+
for (const auto& pair : readerStates_) {
|
|
219
336
|
bool found = false;
|
|
220
337
|
for (const auto& old : oldNames) {
|
|
221
|
-
if (old ==
|
|
338
|
+
if (old == pair.first) {
|
|
222
339
|
found = true;
|
|
223
340
|
break;
|
|
224
341
|
}
|
|
225
342
|
}
|
|
226
343
|
if (!found) {
|
|
227
|
-
EmitEvent("reader-attached",
|
|
344
|
+
EmitEvent("reader-attached", pair.first, pair.second.lastState, pair.second.atr);
|
|
228
345
|
}
|
|
229
346
|
}
|
|
230
347
|
|
|
231
348
|
// Find removed readers
|
|
232
349
|
for (const auto& old : oldNames) {
|
|
233
|
-
|
|
234
|
-
for (const auto& r : readers_) {
|
|
235
|
-
if (r.name == old) {
|
|
236
|
-
found = true;
|
|
237
|
-
break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
if (!found) {
|
|
350
|
+
if (readerStates_.find(old) == readerStates_.end()) {
|
|
241
351
|
EmitEvent("reader-detached", old, 0, {});
|
|
242
352
|
}
|
|
243
353
|
}
|
|
244
354
|
continue;
|
|
245
355
|
}
|
|
246
356
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
357
|
+
// Skip reader state processing if PnP was triggered in this iteration
|
|
358
|
+
// The reader list has changed, so indices are no longer valid
|
|
359
|
+
// We'll pick up any card changes on the next iteration with fresh state
|
|
360
|
+
if (pnpTriggered) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Reader state change - look up by name, not index (Issue #111 fix)
|
|
365
|
+
const std::string& readerName = readerNames[i];
|
|
366
|
+
auto it = readerStates_.find(readerName);
|
|
367
|
+
|
|
368
|
+
if (it != readerStates_.end()) {
|
|
369
|
+
DWORD oldState = it->second.lastState;
|
|
250
370
|
DWORD newState = states[i].dwEventState;
|
|
251
371
|
|
|
252
372
|
bool wasPresent = (oldState & SCARD_STATE_PRESENT) != 0;
|
|
@@ -258,15 +378,15 @@ void ReaderMonitor::MonitorLoop() {
|
|
|
258
378
|
atr.assign(states[i].rgbAtr, states[i].rgbAtr + states[i].cbAtr);
|
|
259
379
|
}
|
|
260
380
|
|
|
261
|
-
// Update stored state
|
|
262
|
-
|
|
263
|
-
|
|
381
|
+
// Update stored state using the map
|
|
382
|
+
it->second.lastState = newState & ~SCARD_STATE_CHANGED;
|
|
383
|
+
it->second.atr = atr;
|
|
264
384
|
|
|
265
385
|
// Emit appropriate event
|
|
266
386
|
if (!wasPresent && isPresent) {
|
|
267
|
-
EmitEvent("card-inserted",
|
|
387
|
+
EmitEvent("card-inserted", readerName, newState, atr);
|
|
268
388
|
} else if (wasPresent && !isPresent) {
|
|
269
|
-
EmitEvent("card-removed",
|
|
389
|
+
EmitEvent("card-removed", readerName, newState, {});
|
|
270
390
|
}
|
|
271
391
|
}
|
|
272
392
|
}
|
|
@@ -279,7 +399,7 @@ void ReaderMonitor::UpdateReaderList() {
|
|
|
279
399
|
LONG result = SCardListReaders(context_, nullptr, nullptr, &readersLen);
|
|
280
400
|
|
|
281
401
|
if (result == SCARD_E_NO_READERS_AVAILABLE || readersLen == 0) {
|
|
282
|
-
|
|
402
|
+
readerStates_.clear();
|
|
283
403
|
return;
|
|
284
404
|
}
|
|
285
405
|
|
|
@@ -312,41 +432,41 @@ void ReaderMonitor::UpdateReaderList() {
|
|
|
312
432
|
|
|
313
433
|
SCardGetStatusChange(context_, 0, states.data(), states.size());
|
|
314
434
|
|
|
315
|
-
// Update reader
|
|
316
|
-
|
|
435
|
+
// Update reader states map (Issue #111 fix: use map keyed by name)
|
|
436
|
+
readerStates_.clear();
|
|
317
437
|
for (size_t i = 0; i < newNames.size(); i++) {
|
|
318
438
|
ReaderInfo info;
|
|
319
|
-
info.name = newNames[i];
|
|
320
439
|
info.lastState = states[i].dwEventState & ~SCARD_STATE_CHANGED;
|
|
321
440
|
if (states[i].cbAtr > 0) {
|
|
322
441
|
info.atr.assign(states[i].rgbAtr, states[i].rgbAtr + states[i].cbAtr);
|
|
323
442
|
}
|
|
324
|
-
|
|
443
|
+
readerStates_[newNames[i]] = info;
|
|
325
444
|
}
|
|
326
445
|
}
|
|
327
446
|
|
|
328
447
|
void ReaderMonitor::EmitEvent(const std::string& eventType, const std::string& readerName,
|
|
329
448
|
DWORD state, const std::vector<uint8_t>& atr) {
|
|
330
|
-
//
|
|
331
|
-
|
|
449
|
+
// Use shared_ptr to ensure memory is freed even if ThreadSafeFunction is released
|
|
450
|
+
// before the callback executes (prevents memory leak)
|
|
451
|
+
auto data = std::make_shared<EventData>(EventData{eventType, readerName, state, atr});
|
|
332
452
|
|
|
333
453
|
// Call JavaScript callback on main thread
|
|
334
|
-
|
|
454
|
+
// Capture shared_ptr by value to extend lifetime until callback executes
|
|
455
|
+
tsfn_.BlockingCall(data.get(), [data](Napi::Env env, Napi::Function callback, EventData* ptr) {
|
|
335
456
|
// Build event object
|
|
336
457
|
Napi::Object event = Napi::Object::New(env);
|
|
337
|
-
event.Set("type", Napi::String::New(env,
|
|
338
|
-
event.Set("reader", Napi::String::New(env,
|
|
339
|
-
event.Set("state", Napi::Number::New(env,
|
|
458
|
+
event.Set("type", Napi::String::New(env, ptr->eventType));
|
|
459
|
+
event.Set("reader", Napi::String::New(env, ptr->readerName));
|
|
460
|
+
event.Set("state", Napi::Number::New(env, ptr->state));
|
|
340
461
|
|
|
341
|
-
if (!
|
|
342
|
-
event.Set("atr", Napi::Buffer<uint8_t>::Copy(env,
|
|
462
|
+
if (!ptr->atr.empty()) {
|
|
463
|
+
event.Set("atr", Napi::Buffer<uint8_t>::Copy(env, ptr->atr.data(), ptr->atr.size()));
|
|
343
464
|
} else {
|
|
344
465
|
event.Set("atr", env.Null());
|
|
345
466
|
}
|
|
346
467
|
|
|
347
468
|
// Call the callback
|
|
348
469
|
callback.Call({event});
|
|
349
|
-
|
|
350
|
-
delete data;
|
|
470
|
+
// shared_ptr automatically cleaned up when lambda is destroyed
|
|
351
471
|
});
|
|
352
472
|
}
|
package/src/reader_monitor.h
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
#include <vector>
|
|
7
7
|
#include <string>
|
|
8
8
|
#include <mutex>
|
|
9
|
+
#include <unordered_map>
|
|
9
10
|
#include "platform/pcsc.h"
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -13,6 +14,9 @@
|
|
|
13
14
|
*
|
|
14
15
|
* Runs a background thread that monitors for reader/card state changes
|
|
15
16
|
* and emits events to JavaScript without blocking the main thread.
|
|
17
|
+
*
|
|
18
|
+
* Issue #111 fix: Uses a map keyed by reader name instead of array indices
|
|
19
|
+
* to prevent state mismatch when readers are added/removed during monitoring.
|
|
16
20
|
*/
|
|
17
21
|
class ReaderMonitor : public Napi::ObjectWrap<ReaderMonitor> {
|
|
18
22
|
public:
|
|
@@ -36,13 +40,12 @@ private:
|
|
|
36
40
|
// Thread-safe function for emitting events
|
|
37
41
|
Napi::ThreadSafeFunction tsfn_;
|
|
38
42
|
|
|
39
|
-
// Current known reader states
|
|
43
|
+
// Current known reader states (Issue #111: keyed by reader name for reliable lookup)
|
|
40
44
|
struct ReaderInfo {
|
|
41
|
-
std::string name;
|
|
42
45
|
DWORD lastState;
|
|
43
46
|
std::vector<uint8_t> atr;
|
|
44
47
|
};
|
|
45
|
-
std::
|
|
48
|
+
std::unordered_map<std::string, ReaderInfo> readerStates_;
|
|
46
49
|
|
|
47
50
|
// JavaScript methods
|
|
48
51
|
Napi::Value Start(const Napi::CallbackInfo& info);
|