Last active
June 1, 2025 11:27
-
-
Save bouroo/ca8a41ac876f713fa8c64cb37c7b36a9 to your computer and use it in GitHub Desktop.
Thai National ID Card reader in NodeJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-console */ | |
/* | |
* Thai National ID Card reader in NodeJS | |
* | |
* @author Kawin Viriyaprasopsook <[email protected]> | |
* @requires smartcard legacy-encoding hex2imagebase64 | |
* @since 11/06/2019 | |
* | |
*/ | |
const { Devices } = require('smartcard'); | |
const legacy = require('legacy-encoding'); | |
const hex2imagebase64 = require('hex2imagebase64'); | |
const DEFAULT_REQ = [0x00, 0xC0, 0x00, 0x00]; | |
const ALT_REQ = [0x00, 0xC0, 0x00, 0x01]; | |
const PHOTO_CHUNKS = 15; | |
const COMMANDS = { | |
SELECT_APP: [0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01], | |
CID: [0x80, 0xB0, 0x00, 0x04, 0x02, 0x00, 0x0D], | |
TH_NAME: [0x80, 0xB0, 0x00, 0x11, 0x02, 0x00, 0x64], | |
EN_NAME: [0x80, 0xB0, 0x00, 0x75, 0x02, 0x00, 0x64], | |
DOB: [0x80, 0xB0, 0x00, 0xD9, 0x02, 0x00, 0x08], | |
GENDER: [0x80, 0xB0, 0x00, 0xE1, 0x02, 0x00, 0x01], | |
ISSUER: [0x80, 0xB0, 0x00, 0xF6, 0x02, 0x00, 0x64], | |
ISSUE_DATE: [0x80, 0xB0, 0x01, 0x67, 0x02, 0x00, 0x08], | |
EXPIRY_DATE: [0x80, 0xB0, 0x01, 0x6F, 0x02, 0x00, 0x08], | |
ADDRESS: [0x80, 0xB0, 0x15, 0x79, 0x02, 0x00, 0x64], | |
PHOTO_CHUNK: idx => [0x80, 0xB0, idx + 1, 0x7B - idx, 0x02, 0x00, 0xFF], | |
}; | |
const DATA_FIELDS = [ | |
{ cmd: COMMANDS.CID, label: 'Get CID:' }, | |
{ cmd: COMMANDS.TH_NAME, label: 'Get thFullname:' }, | |
{ cmd: COMMANDS.EN_NAME, label: 'Get enFullname:' }, | |
{ cmd: COMMANDS.DOB, label: 'Get dateOfBirth:'}, | |
{ cmd: COMMANDS.GENDER, label: 'Get gender:' }, | |
{ cmd: COMMANDS.ISSUER, label: 'Get issuer:' }, | |
{ cmd: COMMANDS.ISSUE_DATE, label: 'Get issueDate:' }, | |
{ cmd: COMMANDS.EXPIRY_DATE, label: 'Get expireDate:' }, | |
{ cmd: COMMANDS.ADDRESS, label: 'Get address:' }, | |
]; | |
class CardSession { | |
constructor(card) { | |
this.card = card; | |
this.reqHeader = card.getAtr().startsWith('3b67') ? ALT_REQ : DEFAULT_REQ; | |
} | |
async run() { | |
await this._issue(COMMANDS.SELECT_APP); | |
for (const { cmd, label } of DATA_FIELDS) { | |
const data = await this._fetch(cmd); | |
console.log(label, legacy.decode(data.slice(0, -2), 'iso-8859-11')); | |
} | |
await this._fetchPhoto(); | |
console.log('Done'); | |
} | |
async _fetch(cmd) { | |
await this._issue(cmd); | |
const le = cmd[cmd.length - 1]; | |
return this._issue([...this.reqHeader, le]); | |
} | |
async _fetchPhoto() { | |
const promises = Array.from({ length: PHOTO_CHUNKS }, (_, i) => | |
this._fetch(COMMANDS.PHOTO_CHUNK(i)) | |
.then(buf => ({ idx: i, chunk: buf.toString('hex').slice(0, -4) })) | |
); | |
const parts = await Promise.all(promises); | |
const hexPhoto = parts | |
.sort((a, b) => a.idx - b.idx) | |
.map(p => p.chunk) | |
.join(''); | |
console.log('Get photo:'); | |
console.log('data:image/jpg;base64,' + hex2imagebase64(hexPhoto)); | |
} | |
_issue(apdu) { | |
return this.card.issueCommand(apdu); | |
} | |
} | |
class DeviceManager { | |
constructor() { | |
this.devices = new Devices(); | |
this._bindEvents(); | |
} | |
_bindEvents() { | |
this.devices.on('device-activated', ({ device, devices }) => { | |
console.log(`Device '${device}' activated, devices: ${devices}`); | |
device.on('card-inserted', async ({ card, device: dev }) => { | |
console.log(`Card '${card.getAtr()}' inserted into '${dev}'`); | |
try { | |
await new CardSession(card).run(); | |
} catch (err) { | |
console.error(err); | |
process.exit(1); | |
} | |
}); | |
device.on('card-removed', e => | |
console.log(`Card removed from '${e.name}'`) | |
); | |
}); | |
this.devices.on('device-deactivated', e => | |
console.log(`Device '${e.device}' deactivated, devices: ${e.devices}`) | |
); | |
} | |
} | |
// Initialize | |
try { | |
new DeviceManager(); | |
} catch (err) { | |
console.error(err); | |
process.exit(1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment