New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

smartcard

Package Overview
Dependencies
Maintainers
1
Versions
50
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

smartcard

PC/SC bindings for Node.js using N-API - ABI stable across Node.js versions

latest
Source
npmnpm
Version
3.7.2
Version published
Weekly downloads
198
-58.84%
Maintainers
1
Weekly downloads
 
Created
Source

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

Readers

ReaderTypeNotes
ACR122UUSB contactlessAffordable, widely available. Great for getting started.
ACR1252UUSB dual-interfaceSupports both contactless and contact cards.
SCM SCR35xxUSB contactTested with SCR35xx v2.0. Good for contact smart cards.
HID Omnikey 5427USB contactlessEnterprise-grade, faster reads.
Identiv uTrust 3700FUSB contactlessCompact, reliable.

Any PC/SC compatible reader should work. The library uses standard PC/SC APIs.

Cards

Card TypeInterfaceNotes
MIFARE Classic 1K/4KContactlessMost common NFC cards
MIFARE Ultralight / NTAGContactlessStickers, wristbands, keyfobs
MIFARE DESFireContactlessHigher security applications
ISO 14443-4ContactlessGeneric contactless smart cards
ISO 7816ContactStandard 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 Devices class 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 data
  • SW1=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 platform-specific control code
const customCode = SCARD_CTL_CODE(3500);

Constants

// Share modes
SCARD_SHARE_EXCLUSIVE  // Exclusive access
SCARD_SHARE_SHARED     // Shared access (default)
SCARD_SHARE_DIRECT     // Direct access to reader

// Protocols
SCARD_PROTOCOL_T0      // T=0 protocol
SCARD_PROTOCOL_T1      // T=1 protocol
SCARD_PROTOCOL_RAW     // Raw protocol

// Disposition (for disconnect)
SCARD_LEAVE_CARD       // Leave card as-is
SCARD_RESET_CARD       // Reset the card
SCARD_UNPOWER_CARD     // Power down the card
SCARD_EJECT_CARD       // Eject the card

// State flags
SCARD_STATE_PRESENT    // Card is present
SCARD_STATE_EMPTY      // No card in reader
SCARD_STATE_CHANGED    // State has changed
// ... and more

// CCID Feature constants
FEATURE_VERIFY_PIN_START      // 0x01
FEATURE_VERIFY_PIN_FINISH     // 0x02
FEATURE_MODIFY_PIN_START      // 0x03
FEATURE_MODIFY_PIN_FINISH     // 0x04
FEATURE_GET_KEY_PRESSED       // 0x05
FEATURE_VERIFY_PIN_DIRECT     // 0x06
FEATURE_MODIFY_PIN_DIRECT     // 0x07
// ... and more

Common APDU Commands

// Get UID (for contactless cards via PC/SC pseudo-APDU)
const GET_UID = [0xFF, 0xCA, 0x00, 0x00, 0x00];

// Select by AID
const SELECT_AID = [0x00, 0xA4, 0x04, 0x00, /* length */, /* AID bytes */];

// Read binary
const READ_BINARY = [0x00, 0xB0, /* P1: offset high */, /* P2: offset low */, /* Le */];

Error Handling

const { PCSCError, CardRemovedError, TimeoutError } = require('smartcard');

try {
    const response = await card.transmit([0x00, 0xA4, 0x04, 0x00]);
} catch (err) {
    if (err instanceof CardRemovedError) {
        console.log('Card was removed');
    } else if (err instanceof TimeoutError) {
        console.log('Operation timed out');
    } else if (err instanceof PCSCError) {
        console.log(`PC/SC error: ${err.message} (code: ${err.code})`);
    } else {
        throw err;
    }
}

Troubleshooting

"No readers available"

  • Ensure a PC/SC compatible reader is connected
  • On Linux, ensure pcscd service is running: sudo systemctl status pcscd

"PC/SC service not running"

  • Linux: sudo systemctl start pcscd
  • Windows: Check "Smart Card" service is running

"Sharing violation"

  • Another application has exclusive access to the card
  • Close other smart card applications

Build errors on Linux

  • Install development headers: sudo apt-get install libpcsclite-dev

Migrating from v1.x

Version 2.0 is a complete rewrite using N-API for stability across Node.js versions.

Breaking Changes

v1.xv2.x
device-activated eventreader-attached event
device-deactivated eventreader-detached event
event.devicereader (passed directly)
device.on('card-inserted')devices.on('card-inserted')
card.issueCommand()card.transmit()

Migration Example

v1.x:

const { Devices } = require('smartcard');
const devices = new Devices();

devices.on('device-activated', event => {
    const device = event.device;
    device.on('card-inserted', event => {
        const card = event.card;
        card.issueCommand(new CommandApdu({...}));
    });
});

v2.x:

const { Devices } = require('smartcard');
const devices = new Devices();

devices.on('reader-attached', reader => {
    console.log('Reader:', reader.name);
});

devices.on('card-inserted', ({ reader, card }) => {
    const response = await card.transmit([0x00, 0xA4, 0x04, 0x00]);
});

devices.start();

Key Improvements in v2.x/v3.x

  • Works on Node.js 18+ without recompilation (v3.x requires Node.js 18+)
  • Native N-API bindings (no more NAN compatibility issues)
  • Simpler flat event model
  • Full TypeScript definitions
  • Promise-based async API

License

MIT

  • nfc-pcsc - NFC library built on smartcard
  • 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).

Keywords

pcsc

FAQs

Package last updated on 18 Feb 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts