Skip to content

Instantly share code, notes, and snippets.

@bouroo
Last active January 8, 2025 12:51
Show Gist options
  • Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.
Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.
Thai National ID Card reader in GO
package main
import (
"bufio"
"bytes"
"fmt"
"io/ioutil"
"os"
"strconv"
"github.com/ebfe/scard"
"golang.org/x/text/encoding/charmap"
)
const (
cmdReqBase = 0x00
cmdReqTail = 0x00
cmdReqSelect = 0x01
)
var (
cmdReq = []byte{cmdReqBase, 0xc0, cmdReqTail, cmdReqSelect}
cmdSelectThaiCard = []byte{0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01}
cmdCID = []byte{0x80, 0xb0, 0x00, 0x04, 0x02, 0x00, 0x0d}
cmdTHFullname = []byte{0x80, 0xb0, 0x00, 0x11, 0x02, 0x00, 0x64}
cmdENFullname = []byte{0x80, 0xb0, 0x00, 0x75, 0x02, 0x00, 0x64}
cmdBirth = []byte{0x80, 0xb0, 0x00, 0xD9, 0x02, 0x00, 0x08}
cmdGender = []byte{0x80, 0xb0, 0x00, 0xE1, 0x02, 0x00, 0x01}
cmdIssuer = []byte{0x80, 0xb0, 0x00, 0xF6, 0x02, 0x00, 0x64}
cmdIssueDate = []byte{0x80, 0xb0, 0x01, 0x67, 0x02, 0x00, 0x08}
cmdExpireDate = []byte{0x80, 0xb0, 0x01, 0x6F, 0x02, 0x00, 0x08}
cmdAddress = []byte{0x80, 0xb0, 0x15, 0x79, 0x02, 0x00, 0x64}
cmdPhoto = generatePhotoCommands()
)
func generatePhotoCommands() [][]byte {
cmds := make([][]byte, 15)
for i := 0; i < len(cmds); i++ {
cmds[i] = []byte{0x80, 0xb0, byte(i + 0x01), byte(0x7B - i), 0x02, 0x00, 0xFF}
}
return cmds
}
func main() {
context, err := scard.EstablishContext()
if err != nil {
fmt.Println("Error establishing context:", err)
return
}
defer context.Release()
readers, err := context.ListReaders()
if err != nil {
fmt.Println("Error listing readers:", err)
return
}
if len(readers) == 0 {
fmt.Println("No card readers found.")
return
}
fmt.Println("Available Readers:")
for idx, reader := range readers {
fmt.Printf("ID: %d, Reader: %s\n", idx, reader)
}
readerIDx := selectReader(readers)
if readerIDx < 0 {
return // Error already printed in selectReader
}
card, err := connectToCard(context, readers[readerIDx])
if err != nil {
fmt.Println("Error connecting to card:", err)
return
}
defer card.Disconnect(scard.ResetCard)
if err := processCard(card); err != nil {
fmt.Println("Error processing card:", err)
}
}
func selectReader(readers []string) int {
readerIDx := 0
if len(readers) > 1 {
buf := bufio.NewReader(os.Stdin)
fmt.Print("Select card reader ID [0]: ")
readerIDxInput, err := buf.ReadString('\n')
if err != nil {
fmt.Println("Error reading input:", err)
return -1
}
var errInput error
readerIDx, errInput = strconv.Atoi(readerIDxInput[:len(readerIDxInput)-1]) // Remove newline
if errInput != nil || readerIDx < 0 || readerIDx >= len(readers) {
fmt.Println("Error: Index out of bounds. Defaulting to 0.")
readerIDx = 0
}
}
return readerIDx
}
func connectToCard(context *scard.Context, reader string) (*scard.Card, error) {
card, err := context.Connect(reader, scard.ShareShared, scard.ProtocolAny)
if err != nil {
return nil, err
}
return card, nil
}
func processCard(card *scard.Card) error {
status, err := card.Status()
if err != nil {
return fmt.Errorf("Error fetching card status: %w", err)
}
fmt.Printf("Card status:\n\tReader: %s\n\tState: %x\n\tActive Protocol: %x\n\tATR: % x\n",
status.Reader, status.State, status.ActiveProtocol, status.Atr)
atr, err := card.GetAttrib(scard.AttrAtrString)
if err != nil {
return fmt.Errorf("Error getting ATR: %w", err)
}
fmt.Println("Card ATR:", string(atr))
if atr[0] == 0x3B && atr[1] == 0x67 {
cmdReq[3] = cmdReqSelect
}
fmt.Println("Request Command:", cmdReq)
if err := card.Transmit(cmdSelectThaiCard); err != nil {
return fmt.Errorf("Error selecting Thai ID card: %w", err)
}
infoCommands := []struct {
cmd []byte
name string
}{
{cmdCID, "CID"},
{cmdTHFullname, "Thai Fullname"},
{cmdENFullname, "English Fullname"},
{cmdBirth, "Date of Birth"},
{cmdGender, "Gender"},
{cmdIssuer, "Issuer"},
{cmdIssueDate, "Issue Date"},
{cmdExpireDate, "Expire Date"},
{cmdAddress, "Address"},
}
for _, info := range infoCommands {
resp, err := getString(card, info.cmd)
if err != nil {
return fmt.Errorf("Error getting %s: %w", info.name, err)
}
fmt.Printf("%s: %s\n", info.name, resp)
}
photo, err := getPhoto(card)
if err != nil {
return fmt.Errorf("Error getting photo: %w", err)
}
if err := ioutil.WriteFile(fmt.Sprintf("%s.jpg", infoCommands[0].name), photo, 0644); err != nil {
return fmt.Errorf("Error writing photo: %w", err)
}
return nil
}
func getString(card *scard.Card, cmd []byte) (string, error) {
rawResp, err := getData(card, cmd)
if err != nil {
return "", err
}
thResp, err := thaiToUnicode(rawResp)
if err != nil {
return "", err
}
return string(bytes.TrimSpace(thResp)), nil
}
func thaiToUnicode(data []byte) ([]byte, error) {
decoder := charmap.Windows874.NewDecoder()
return decoder.Bytes(data)
}
func getData(card *scard.Card, cmd []byte) ([]byte, error) {
if _, err := card.Transmit(cmd); err != nil {
return nil, fmt.Errorf("Error transmitting command: %w", err)
}
resp, err := card.Transmit(append(cmdReq, cmd[len(cmd)-1]))
if err != nil {
return nil, fmt.Errorf("Error transmitting request: %w", err)
}
// Remove unused bytes (last two bytes)
return resp[:len(resp)-2], nil
}
func getPhoto(card *scard.Card) ([]byte, error) {
var resp []byte
for _, itemCmd := range cmdPhoto {
tmpArray, err := getData(card, itemCmd)
if err != nil {
return nil, fmt.Errorf("Error getting photo data: %w", err)
}
resp = append(resp, tmpArray...)
}
return resp, nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment