smartcard 1.0.46 → 2.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/LICENSE +1 -1
- package/README.md +387 -0
- package/binding.gyp +45 -0
- package/lib/devices.js +251 -0
- package/lib/errors.js +72 -0
- package/lib/index.d.ts +247 -0
- package/lib/index.js +76 -14
- package/package.json +56 -39
- package/src/addon.cpp +54 -0
- package/src/async_workers.cpp +234 -0
- package/src/async_workers.h +100 -0
- package/src/pcsc_card.cpp +248 -0
- package/src/pcsc_card.h +41 -0
- package/src/pcsc_context.cpp +224 -0
- package/src/pcsc_context.h +32 -0
- package/src/pcsc_errors.h +49 -0
- package/src/pcsc_reader.cpp +89 -0
- package/src/pcsc_reader.h +39 -0
- package/src/platform/pcsc.h +36 -0
- package/src/reader_monitor.cpp +344 -0
- package/src/reader_monitor.h +57 -0
- package/.prettierrc +0 -3
- package/README.MD +0 -371
- package/babel.config.json +0 -3
- package/demo/device-activated-promise.js +0 -12
- package/demo/device-activated.js +0 -12
- package/demo/device-deactivated-promise.js +0 -12
- package/demo/device-deactivated.js +0 -12
- package/demo/smartcard-demo.js +0 -100
- package/lib/Card.js +0 -129
- package/lib/CommandApdu.js +0 -109
- package/lib/Device.js +0 -138
- package/lib/Devices.js +0 -134
- package/lib/Iso7816Application.js +0 -169
- package/lib/ResponseApdu.js +0 -129
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#include "reader_monitor.h"
|
|
2
|
+
#include "pcsc_errors.h"
|
|
3
|
+
#include <cstring>
|
|
4
|
+
|
|
5
|
+
Napi::FunctionReference ReaderMonitor::constructor;
|
|
6
|
+
|
|
7
|
+
// Event data passed from worker thread to JS thread
|
|
8
|
+
struct EventData {
|
|
9
|
+
std::string eventType;
|
|
10
|
+
std::string readerName;
|
|
11
|
+
DWORD state;
|
|
12
|
+
std::vector<uint8_t> atr;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
Napi::Object ReaderMonitor::Init(Napi::Env env, Napi::Object exports) {
|
|
16
|
+
Napi::Function func = DefineClass(env, "ReaderMonitor", {
|
|
17
|
+
InstanceMethod("start", &ReaderMonitor::Start),
|
|
18
|
+
InstanceMethod("stop", &ReaderMonitor::Stop),
|
|
19
|
+
InstanceAccessor("isRunning", &ReaderMonitor::GetIsRunning, nullptr),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
constructor = Napi::Persistent(func);
|
|
23
|
+
constructor.SuppressDestruct();
|
|
24
|
+
|
|
25
|
+
exports.Set("ReaderMonitor", func);
|
|
26
|
+
return exports;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ReaderMonitor::ReaderMonitor(const Napi::CallbackInfo& info)
|
|
30
|
+
: Napi::ObjectWrap<ReaderMonitor>(info),
|
|
31
|
+
context_(0),
|
|
32
|
+
contextValid_(false),
|
|
33
|
+
running_(false) {
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ReaderMonitor::~ReaderMonitor() {
|
|
37
|
+
// Ensure monitoring is stopped
|
|
38
|
+
if (running_) {
|
|
39
|
+
running_ = false;
|
|
40
|
+
if (contextValid_) {
|
|
41
|
+
SCardCancel(context_);
|
|
42
|
+
}
|
|
43
|
+
if (monitorThread_.joinable()) {
|
|
44
|
+
monitorThread_.join();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (contextValid_) {
|
|
49
|
+
SCardReleaseContext(context_);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Napi::Value ReaderMonitor::Start(const Napi::CallbackInfo& info) {
|
|
54
|
+
Napi::Env env = info.Env();
|
|
55
|
+
|
|
56
|
+
if (running_) {
|
|
57
|
+
Napi::Error::New(env, "Monitor is already running").ThrowAsJavaScriptException();
|
|
58
|
+
return env.Undefined();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Require callback function
|
|
62
|
+
if (info.Length() < 1 || !info[0].IsFunction()) {
|
|
63
|
+
Napi::TypeError::New(env, "Callback function required").ThrowAsJavaScriptException();
|
|
64
|
+
return env.Undefined();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
Napi::Function callback = info[0].As<Napi::Function>();
|
|
68
|
+
|
|
69
|
+
// Establish PC/SC context
|
|
70
|
+
LONG result = SCardEstablishContext(SCARD_SCOPE_SYSTEM, nullptr, nullptr, &context_);
|
|
71
|
+
if (result != SCARD_S_SUCCESS) {
|
|
72
|
+
Napi::Error::New(env, GetPCSCErrorString(result)).ThrowAsJavaScriptException();
|
|
73
|
+
return env.Undefined();
|
|
74
|
+
}
|
|
75
|
+
contextValid_ = true;
|
|
76
|
+
|
|
77
|
+
// Create thread-safe function
|
|
78
|
+
tsfn_ = Napi::ThreadSafeFunction::New(
|
|
79
|
+
env,
|
|
80
|
+
callback,
|
|
81
|
+
"ReaderMonitor",
|
|
82
|
+
0, // Unlimited queue size
|
|
83
|
+
1, // 1 initial thread
|
|
84
|
+
[this](Napi::Env) {
|
|
85
|
+
// Invoke destructor callback - called when tsfn is released
|
|
86
|
+
running_ = false;
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Start monitoring
|
|
91
|
+
running_ = true;
|
|
92
|
+
monitorThread_ = std::thread(&ReaderMonitor::MonitorLoop, this);
|
|
93
|
+
|
|
94
|
+
return env.Undefined();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
Napi::Value ReaderMonitor::Stop(const Napi::CallbackInfo& info) {
|
|
98
|
+
Napi::Env env = info.Env();
|
|
99
|
+
|
|
100
|
+
if (!running_) {
|
|
101
|
+
return env.Undefined();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
running_ = false;
|
|
105
|
+
|
|
106
|
+
// Cancel any blocking SCardGetStatusChange call
|
|
107
|
+
if (contextValid_) {
|
|
108
|
+
SCardCancel(context_);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Wait for thread to finish
|
|
112
|
+
if (monitorThread_.joinable()) {
|
|
113
|
+
monitorThread_.join();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Release thread-safe function
|
|
117
|
+
tsfn_.Release();
|
|
118
|
+
|
|
119
|
+
// Release context
|
|
120
|
+
if (contextValid_) {
|
|
121
|
+
SCardReleaseContext(context_);
|
|
122
|
+
contextValid_ = false;
|
|
123
|
+
context_ = 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
readers_.clear();
|
|
127
|
+
|
|
128
|
+
return env.Undefined();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
Napi::Value ReaderMonitor::GetIsRunning(const Napi::CallbackInfo& info) {
|
|
132
|
+
return Napi::Boolean::New(info.Env(), running_.load());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void ReaderMonitor::MonitorLoop() {
|
|
136
|
+
// Get initial reader list
|
|
137
|
+
UpdateReaderList();
|
|
138
|
+
|
|
139
|
+
// Build initial states array with PnP notification
|
|
140
|
+
std::vector<SCARD_READERSTATE> states;
|
|
141
|
+
std::vector<std::string> readerNames;
|
|
142
|
+
|
|
143
|
+
while (running_) {
|
|
144
|
+
// Build states array
|
|
145
|
+
{
|
|
146
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
147
|
+
states.clear();
|
|
148
|
+
readerNames.clear();
|
|
149
|
+
|
|
150
|
+
// Add known readers
|
|
151
|
+
for (auto& reader : readers_) {
|
|
152
|
+
SCARD_READERSTATE state = {};
|
|
153
|
+
readerNames.push_back(reader.name);
|
|
154
|
+
state.szReader = readerNames.back().c_str();
|
|
155
|
+
state.dwCurrentState = reader.lastState;
|
|
156
|
+
states.push_back(state);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add PnP notification for new reader detection
|
|
160
|
+
readerNames.push_back("\\\\?PnP?\\Notification");
|
|
161
|
+
SCARD_READERSTATE pnpState = {};
|
|
162
|
+
pnpState.szReader = readerNames.back().c_str();
|
|
163
|
+
pnpState.dwCurrentState = SCARD_STATE_UNAWARE;
|
|
164
|
+
states.push_back(pnpState);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Wait for changes (with 1 second timeout for periodic refresh)
|
|
168
|
+
LONG result = SCardGetStatusChange(context_, 1000, states.data(), states.size());
|
|
169
|
+
|
|
170
|
+
if (!running_) {
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (result == SCARD_E_CANCELLED) {
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (result == SCARD_E_TIMEOUT) {
|
|
179
|
+
// No changes, continue
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (result != SCARD_S_SUCCESS) {
|
|
184
|
+
// Error - emit and continue
|
|
185
|
+
EmitEvent("error", GetPCSCErrorString(result), 0, {});
|
|
186
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Process changes
|
|
191
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
192
|
+
|
|
193
|
+
for (size_t i = 0; i < states.size(); i++) {
|
|
194
|
+
if (!(states[i].dwEventState & SCARD_STATE_CHANGED)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// PnP notification - reader list changed
|
|
199
|
+
if (readerNames[i] == "\\\\?PnP?\\Notification") {
|
|
200
|
+
// Get old reader names
|
|
201
|
+
std::vector<std::string> oldNames;
|
|
202
|
+
for (const auto& r : readers_) {
|
|
203
|
+
oldNames.push_back(r.name);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Update reader list
|
|
207
|
+
UpdateReaderList();
|
|
208
|
+
|
|
209
|
+
// Find new readers
|
|
210
|
+
for (const auto& r : readers_) {
|
|
211
|
+
bool found = false;
|
|
212
|
+
for (const auto& old : oldNames) {
|
|
213
|
+
if (old == r.name) {
|
|
214
|
+
found = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!found) {
|
|
219
|
+
EmitEvent("reader-attached", r.name, r.lastState, r.atr);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Find removed readers
|
|
224
|
+
for (const auto& old : oldNames) {
|
|
225
|
+
bool found = false;
|
|
226
|
+
for (const auto& r : readers_) {
|
|
227
|
+
if (r.name == old) {
|
|
228
|
+
found = true;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (!found) {
|
|
233
|
+
EmitEvent("reader-detached", old, 0, {});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Reader state change
|
|
240
|
+
if (i < readers_.size()) {
|
|
241
|
+
DWORD oldState = readers_[i].lastState;
|
|
242
|
+
DWORD newState = states[i].dwEventState;
|
|
243
|
+
|
|
244
|
+
bool wasPresent = (oldState & SCARD_STATE_PRESENT) != 0;
|
|
245
|
+
bool isPresent = (newState & SCARD_STATE_PRESENT) != 0;
|
|
246
|
+
|
|
247
|
+
// Get ATR
|
|
248
|
+
std::vector<uint8_t> atr;
|
|
249
|
+
if (states[i].cbAtr > 0) {
|
|
250
|
+
atr.assign(states[i].rgbAtr, states[i].rgbAtr + states[i].cbAtr);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Update stored state
|
|
254
|
+
readers_[i].lastState = newState & ~SCARD_STATE_CHANGED;
|
|
255
|
+
readers_[i].atr = atr;
|
|
256
|
+
|
|
257
|
+
// Emit appropriate event
|
|
258
|
+
if (!wasPresent && isPresent) {
|
|
259
|
+
EmitEvent("card-inserted", readerNames[i], newState, atr);
|
|
260
|
+
} else if (wasPresent && !isPresent) {
|
|
261
|
+
EmitEvent("card-removed", readerNames[i], newState, {});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
void ReaderMonitor::UpdateReaderList() {
|
|
269
|
+
// Get reader list size
|
|
270
|
+
DWORD readersLen = 0;
|
|
271
|
+
LONG result = SCardListReaders(context_, nullptr, nullptr, &readersLen);
|
|
272
|
+
|
|
273
|
+
if (result == SCARD_E_NO_READERS_AVAILABLE || readersLen == 0) {
|
|
274
|
+
readers_.clear();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (result != SCARD_S_SUCCESS) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Get reader names
|
|
283
|
+
std::vector<char> buffer(readersLen);
|
|
284
|
+
result = SCardListReaders(context_, nullptr, buffer.data(), &readersLen);
|
|
285
|
+
|
|
286
|
+
if (result != SCARD_S_SUCCESS) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Parse multi-string
|
|
291
|
+
std::vector<std::string> newNames;
|
|
292
|
+
const char* p = buffer.data();
|
|
293
|
+
while (*p != '\0') {
|
|
294
|
+
newNames.push_back(std::string(p));
|
|
295
|
+
p += strlen(p) + 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Get initial state for new readers
|
|
299
|
+
std::vector<SCARD_READERSTATE> states(newNames.size());
|
|
300
|
+
for (size_t i = 0; i < newNames.size(); i++) {
|
|
301
|
+
states[i].szReader = newNames[i].c_str();
|
|
302
|
+
states[i].dwCurrentState = SCARD_STATE_UNAWARE;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
SCardGetStatusChange(context_, 0, states.data(), states.size());
|
|
306
|
+
|
|
307
|
+
// Update reader list
|
|
308
|
+
readers_.clear();
|
|
309
|
+
for (size_t i = 0; i < newNames.size(); i++) {
|
|
310
|
+
ReaderInfo info;
|
|
311
|
+
info.name = newNames[i];
|
|
312
|
+
info.lastState = states[i].dwEventState & ~SCARD_STATE_CHANGED;
|
|
313
|
+
if (states[i].cbAtr > 0) {
|
|
314
|
+
info.atr.assign(states[i].rgbAtr, states[i].rgbAtr + states[i].cbAtr);
|
|
315
|
+
}
|
|
316
|
+
readers_.push_back(info);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
void ReaderMonitor::EmitEvent(const std::string& eventType, const std::string& readerName,
|
|
321
|
+
DWORD state, const std::vector<uint8_t>& atr) {
|
|
322
|
+
// Copy data for transfer to JS thread
|
|
323
|
+
EventData* data = new EventData{eventType, readerName, state, atr};
|
|
324
|
+
|
|
325
|
+
// Call JavaScript callback on main thread
|
|
326
|
+
tsfn_.BlockingCall(data, [](Napi::Env env, Napi::Function callback, EventData* data) {
|
|
327
|
+
// Build event object
|
|
328
|
+
Napi::Object event = Napi::Object::New(env);
|
|
329
|
+
event.Set("type", Napi::String::New(env, data->eventType));
|
|
330
|
+
event.Set("reader", Napi::String::New(env, data->readerName));
|
|
331
|
+
event.Set("state", Napi::Number::New(env, data->state));
|
|
332
|
+
|
|
333
|
+
if (!data->atr.empty()) {
|
|
334
|
+
event.Set("atr", Napi::Buffer<uint8_t>::Copy(env, data->atr.data(), data->atr.size()));
|
|
335
|
+
} else {
|
|
336
|
+
event.Set("atr", env.Null());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Call the callback
|
|
340
|
+
callback.Call({event});
|
|
341
|
+
|
|
342
|
+
delete data;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <napi.h>
|
|
4
|
+
#include <thread>
|
|
5
|
+
#include <atomic>
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <mutex>
|
|
9
|
+
#include "platform/pcsc.h"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ReaderMonitor - Native PC/SC event monitoring using ThreadSafeFunction
|
|
13
|
+
*
|
|
14
|
+
* Runs a background thread that monitors for reader/card state changes
|
|
15
|
+
* and emits events to JavaScript without blocking the main thread.
|
|
16
|
+
*/
|
|
17
|
+
class ReaderMonitor : public Napi::ObjectWrap<ReaderMonitor> {
|
|
18
|
+
public:
|
|
19
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
20
|
+
|
|
21
|
+
ReaderMonitor(const Napi::CallbackInfo& info);
|
|
22
|
+
~ReaderMonitor();
|
|
23
|
+
|
|
24
|
+
private:
|
|
25
|
+
static Napi::FunctionReference constructor;
|
|
26
|
+
|
|
27
|
+
// PC/SC context
|
|
28
|
+
SCARDCONTEXT context_;
|
|
29
|
+
bool contextValid_;
|
|
30
|
+
|
|
31
|
+
// Monitoring thread
|
|
32
|
+
std::thread monitorThread_;
|
|
33
|
+
std::atomic<bool> running_;
|
|
34
|
+
std::mutex mutex_;
|
|
35
|
+
|
|
36
|
+
// Thread-safe function for emitting events
|
|
37
|
+
Napi::ThreadSafeFunction tsfn_;
|
|
38
|
+
|
|
39
|
+
// Current known reader states
|
|
40
|
+
struct ReaderInfo {
|
|
41
|
+
std::string name;
|
|
42
|
+
DWORD lastState;
|
|
43
|
+
std::vector<uint8_t> atr;
|
|
44
|
+
};
|
|
45
|
+
std::vector<ReaderInfo> readers_;
|
|
46
|
+
|
|
47
|
+
// JavaScript methods
|
|
48
|
+
Napi::Value Start(const Napi::CallbackInfo& info);
|
|
49
|
+
Napi::Value Stop(const Napi::CallbackInfo& info);
|
|
50
|
+
Napi::Value GetIsRunning(const Napi::CallbackInfo& info);
|
|
51
|
+
|
|
52
|
+
// Internal methods
|
|
53
|
+
void MonitorLoop();
|
|
54
|
+
void UpdateReaderList();
|
|
55
|
+
void EmitEvent(const std::string& eventType, const std::string& readerName,
|
|
56
|
+
DWORD state, const std::vector<uint8_t>& atr);
|
|
57
|
+
};
|
package/.prettierrc
DELETED