Smartcard
PCSC smartcard reader library for nodejs
Install / Use
/learn @tomkp/SmartcardREADME
smartcard
╭────────────────────────────────────────╮
│ │
│ ╭───────────╮ │
│ │ ▄▄▄ ▄▄▄▄▄ │ │
│ │ ███ ▄▄▄▄▄ │ │
│ │ ▀▀▀ ▀▀▀▀▀ │ │
│ ╰───────────╯ │
│ │
│ ░░░░░░░░░░░░ │
│ ░░░░░░░░░░░░ │
╰────────────────────────────────────────╯
Stable PC/SC smart card bindings for Node.js.
Works with Node.js 18+ without recompilation. Built on N-API for long-term stability.
Getting Started
1. Install the package
npm install smartcard
2. Platform setup
macOS/Windows: Ready to go - no additional setup needed.
Linux:
# Install PC/SC libraries
sudo apt-get install libpcsclite-dev pcscd # Debian/Ubuntu
sudo dnf install pcsc-lite-devel pcsc-lite # Fedora/RHEL
# Start the daemon
sudo systemctl start pcscd
3. Connect a reader and run your first script
const { Devices } = require('smartcard');
const devices = new Devices();
devices.on('card-inserted', async ({ reader, card }) => {
console.log(`Card detected in ${reader.name}`);
console.log(`ATR: ${card.atr.toString('hex')}`);
// Get card UID (works with most contactless cards)
const response = await card.transmit([0xFF, 0xCA, 0x00, 0x00, 0x00]);
console.log(`UID: ${response.slice(0, -2).toString('hex')}`);
});
devices.on('error', (err) => console.error(err.message));
devices.start();
Run it:
node app.js
# Tap a card on your reader...
# Card detected in ACS ACR122U
# ATR: 3b8f8001804f0ca0000003060300030000000068
# UID: 04a23b7a
Recommended Hardware
Readers
| Reader | Type | Notes | |--------|------|-------| | ACR122U | USB contactless | Affordable, widely available. Great for getting started. | | ACR1252U | USB dual-interface | Supports both contactless and contact cards. | | SCM SCR35xx | USB contact | Tested with SCR35xx v2.0. Good for contact smart cards. | | HID Omnikey 5427 | USB contactless | Enterprise-grade, faster reads. | | Identiv uTrust 3700F | USB contactless | Compact, reliable. |
Any PC/SC compatible reader should work. The library uses standard PC/SC APIs.
Cards
| Card Type | Interface | Notes | |-----------|-----------|-------| | MIFARE Classic 1K/4K | Contactless | Most common NFC cards | | MIFARE Ultralight / NTAG | Contactless | Stickers, wristbands, keyfobs | | MIFARE DESFire | Contactless | Higher security applications | | ISO 14443-4 | Contactless | Generic contactless smart cards | | ISO 7816 | Contact | Standard contact smart cards (SIM, bank cards, ID cards) |
Features
- ABI Stable: Works across Node.js versions without recompilation
- Async/Promise-based: Non-blocking card operations
- Event-driven API: High-level
Devicesclass with EventEmitter - TypeScript support: Full type definitions included
- Cross-platform: Windows, macOS, and Linux
More Examples
High-Level API (Event-Driven)
const { Devices } = require('smartcard');
const devices = new Devices();
devices.on('reader-attached', (reader) => {
console.log(`Reader attached: ${reader.name}`);
});
devices.on('reader-detached', (reader) => {
console.log(`Reader detached: ${reader.name}`);
});
devices.on('card-inserted', async ({ reader, card }) => {
console.log(`Card inserted in ${reader.name}`);
console.log(` ATR: ${card.atr.toString('hex')}`);
// Send APDU command
try {
const response = await card.transmit([0xFF, 0xCA, 0x00, 0x00, 0x00]);
console.log(` UID: ${response.slice(0, -2).toString('hex')}`);
} catch (err) {
console.error('Transmit error:', err.message);
}
});
devices.on('card-removed', ({ reader }) => {
console.log(`Card removed from ${reader.name}`);
});
devices.on('error', (err) => {
console.error('Error:', err.message);
});
// Start monitoring
devices.start();
// Stop on exit
process.on('SIGINT', () => {
devices.stop();
process.exit();
});
Low-Level API (Direct PC/SC)
const {
Context,
SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0,
SCARD_PROTOCOL_T1,
SCARD_LEAVE_CARD
} = require('smartcard');
async function main() {
// Create PC/SC context
const ctx = new Context();
console.log('Context valid:', ctx.isValid);
// List readers
const readers = ctx.listReaders();
console.log('Readers:', readers.map(r => r.name));
if (readers.length === 0) {
console.log('No readers found');
ctx.close();
return;
}
const reader = readers[0];
console.log(`Using reader: ${reader.name}`);
console.log(` State: ${reader.state}`);
// Connect to card
try {
const card = await reader.connect(
SCARD_SHARE_SHARED,
SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1
);
console.log(`Connected, protocol: ${card.protocol}`);
// Get card status
const status = card.getStatus();
console.log(` ATR: ${status.atr.toString('hex')}`);
// Send APDU (Get UID for contactless cards)
const response = await card.transmit(Buffer.from([0xFF, 0xCA, 0x00, 0x00, 0x00]));
console.log(` Response: ${response.toString('hex')}`);
// Disconnect
card.disconnect(SCARD_LEAVE_CARD);
} catch (err) {
console.error('Card error:', err.message);
}
// Close context
ctx.close();
}
main();
Waiting for Card Changes
const { Context } = require('smartcard');
async function waitForCard() {
const ctx = new Context();
const readers = ctx.listReaders();
if (readers.length === 0) {
console.log('No readers found');
ctx.close();
return;
}
console.log('Waiting for card...');
// Wait for state change (timeout: 30 seconds)
const changes = await ctx.waitForChange(readers, 30000);
if (changes === null) {
console.log('Cancelled');
} else if (changes.length === 0) {
console.log('Timeout');
} else {
for (const change of changes) {
if (change.changed) {
console.log(`${change.name}: state changed to ${change.state}`);
if (change.atr) {
console.log(` ATR: ${change.atr.toString('hex')}`);
}
}
}
}
ctx.close();
}
waitForCard();
API Reference
Context
The low-level PC/SC context.
class Context {
constructor();
readonly isValid: boolean;
listReaders(): Reader[];
waitForChange(readers?: Reader[], timeout?: number): Promise<ReaderState[] | null>;
cancel(): void;
close(): void;
}
Reader
Represents a smart card reader.
interface Reader {
readonly name: string;
readonly state: number;
readonly atr: Buffer | null;
connect(shareMode?: number, protocol?: number): Promise<Card>;
}
Card
Represents a connected smart card.
interface Card {
readonly protocol: number;
readonly connected: boolean;
readonly atr: Buffer | null;
transmit(command: Buffer | number[], options?: { maxRecvLength?: number; autoGetResponse?: boolean }): Promise<Buffer>;
control(code: number, data?: Buffer): Promise<Buffer>;
getStatus(): { state: number; protocol: number; atr: Buffer };
disconnect(disposition?: number): void;
reconnect(shareMode?: number, protocol?: number, init?: number): Promise<number>;
}
Devices
High-level event-driven API.
class Devices extends EventEmitter {
start(): void;
stop(): void;
listReaders(): Reader[];
getCards(): ReadonlyMap<string, Card>; // Get all connected cards by reader name
getCard(readerName: string): Card | null; // Get card for specific reader
on(event: 'reader-attached', listener: (reader: Reader) => void): this;
on(event: 'reader-detached', listener: (reader: Reader) => void): this;
on(event: 'card-inserted', listener: (event: { reader: Reader; card: Card }) => void): this;
on(event: 'card-removed', listener: (event: { reader: Reader; card: Card | null }) => void): this;
on(event: 'error', listener: (error: Error) => void): this;
}
Auto GET RESPONSE (T=0 Protocol)
For T=0 protocol cards, you can automatically handle status words by passing the autoGetResponse option:
SW1=61: Sends GET RESPONSE to retrieve remaining dataSW1=6C: Retries with corrected Le value
// Without auto-response (raw)
const raw = await card.transmit([0x00, 0xA4, 0x04, 0x00, 0x0E, ...aid]);
// Returns: 61 1C (meaning 28 more bytes available)
// With auto-response - handles 61 XX automatically
const response = await card.transmit([0x00, 0xA4, 0x04, 0x00, 0x0E, ...aid], {
autoGetResponse: true
});
// Returns: full response data + 90 00
The transmitWithAutoResponse() helper function is also available for low-level API usage:
const { transmitWithAutoResponse } = require('smartcard');
const response = await transmitWithAutoResponse(card, [0x00, 0xA4, 0x04, 0x00, 0x0E, ...aid], {
autoGetResponse: true
});
Control Codes
Utilities for reader control commands (e.g., PIN verification on pinpad readers).
const {
SCARD_CTL_CODE,
CM_IOCTL_GET_FEATURE_REQUEST,
parseFeatures,
FEATURE_VERIFY_PIN_DIRECT,
FEATURE_MODIFY_PIN_DIRECT
} = require('smartcard');
// Get supported features from reader
const featureResponse = await card.control(CM_IOCTL_GET_FEATURE_REQUEST);
const features = parseFeatures(featureResponse);
if (features.has(FEATURE_VERIFY_PIN_DIRECT)) {
const pinVerifyCode = features.get(FEATURE_VERIFY_PIN_DIRECT);
// Use pinVerifyCode with card.control() for PIN verification
}
// Generate
