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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smartcard",
3
- "version": "3.7.0",
3
+ "version": "3.7.2",
4
4
  "description": "PC/SC bindings for Node.js using N-API - ABI stable across Node.js versions",
5
5
  "author": "Tom KP",
6
6
  "license": "MIT",
@@ -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
- readers_.clear();
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& reader : readers_) {
143
- EmitEvent("reader-attached", reader.name, reader.lastState, reader.atr);
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
- // Build states array
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& reader : readers_) {
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(reader.name);
222
+ readerNames.push_back(pair.first);
162
223
  state.szReader = readerNames.back().c_str();
163
- state.dwCurrentState = reader.lastState;
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
- // No changes, continue
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& r : readers_) {
211
- oldNames.push_back(r.name);
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& r : readers_) {
335
+ for (const auto& pair : readerStates_) {
219
336
  bool found = false;
220
337
  for (const auto& old : oldNames) {
221
- if (old == r.name) {
338
+ if (old == pair.first) {
222
339
  found = true;
223
340
  break;
224
341
  }
225
342
  }
226
343
  if (!found) {
227
- EmitEvent("reader-attached", r.name, r.lastState, r.atr);
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
- bool found = false;
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
- // Reader state change
248
- if (i < readers_.size()) {
249
- DWORD oldState = readers_[i].lastState;
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
- readers_[i].lastState = newState & ~SCARD_STATE_CHANGED;
263
- readers_[i].atr = atr;
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", readerNames[i], newState, atr);
387
+ EmitEvent("card-inserted", readerName, newState, atr);
268
388
  } else if (wasPresent && !isPresent) {
269
- EmitEvent("card-removed", readerNames[i], newState, {});
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
- readers_.clear();
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 list
316
- readers_.clear();
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
- readers_.push_back(info);
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
- // Copy data for transfer to JS thread
331
- EventData* data = new EventData{eventType, readerName, state, atr};
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
- tsfn_.BlockingCall(data, [](Napi::Env env, Napi::Function callback, EventData* data) {
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, data->eventType));
338
- event.Set("reader", Napi::String::New(env, data->readerName));
339
- event.Set("state", Napi::Number::New(env, data->state));
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 (!data->atr.empty()) {
342
- event.Set("atr", Napi::Buffer<uint8_t>::Copy(env, data->atr.data(), data->atr.size()));
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
  }
@@ -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::vector<ReaderInfo> readers_;
48
+ std::unordered_map<std::string, ReaderInfo> readerStates_;
46
49
 
47
50
  // JavaScript methods
48
51
  Napi::Value Start(const Napi::CallbackInfo& info);