Skip to content

Instantly share code, notes, and snippets.

@bouroo
Last active June 1, 2025 11:27
Show Gist options
  • Save bouroo/ca8a41ac876f713fa8c64cb37c7b36a9 to your computer and use it in GitHub Desktop.
Save bouroo/ca8a41ac876f713fa8c64cb37c7b36a9 to your computer and use it in GitHub Desktop.
Thai National ID Card reader in NodeJS
/* 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