Skip to content

Instantly share code, notes, and snippets.

@bouroo
Last active May 9, 2025 14:43
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"
"os"
"strconv"
"github.com/ebfe/scard"
"golang.org/x/text/encoding/charmap"
)
// Interfaces
type ReaderSelector interface {
Select([]string) (int, error)
}
type CardConnector interface {
Connect(*scard.Context, string) (*scard.Card, error)
}
type SmartCardService interface {
Status() (scard.Status, error)
ATR() ([]byte, error)
AdjustRequest([]byte)
Transmit([]byte) ([]byte, error)
GetData([]byte) ([]byte, error)
}
type DataDecoder interface {
Decode([]byte) ([]byte, error)
}
type FileWriter interface {
Write(string, []byte) error
}
// Implementations
// ConsoleReaderSelector prompts the user to pick a reader.
type ConsoleReaderSelector struct{}
func (s *ConsoleReaderSelector) Select(readers []string) (int, error) {
if len(readers) == 0 {
return -1, fmt.Errorf("no readers")
}
if len(readers) == 1 {
return 0, nil
}
fmt.Println("Available readers:")
for i, r := range readers {
fmt.Printf("%d) %s\n", i, r)
}
fmt.Print("Select reader [0]: ")
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
return 0, err
}
n, err := strconv.Atoi(line[:len(line)-1])
if err != nil || n < 0 || n >= len(readers) {
return 0, nil
}
return n, nil
}
// ScardConnector wraps context.Connect.
type ScardConnector struct{}
func (c *ScardConnector) Connect(ctx *scard.Context, reader string) (*scard.Card, error) {
return ctx.Connect(reader, scard.ShareShared, scard.ProtocolAny)
}
// scardService implements SmartCardService over scard.Card.
type scardService struct {
card *scard.Card
reqPrefix []byte
}
func NewScardService(card *scard.Card, req []byte) SmartCardService {
copyReq := make([]byte, len(req))
copy(copyReq, req)
return &scardService{card: card, reqPrefix: copyReq}
}
func (s *scardService) Status() (scard.Status, error) {
return s.card.Status()
}
func (s *scardService) ATR() ([]byte, error) {
return s.card.GetAttrib(scard.AttrAtrString)
}
func (s *scardService) AdjustRequest(atr []byte) {
if len(atr) > 1 && atr[0] == 0x3B && atr[1] == 0x67 {
s.reqPrefix[3] = 0x01
}
}
func (s *scardService) Transmit(cmd []byte) ([]byte, error) {
return s.card.Transmit(cmd)
}
func (s *scardService) GetData(cmd []byte) ([]byte, error) {
if _, err := s.card.Transmit(cmd); err != nil {
return nil, err
}
tail := cmd[len(cmd)-1]
resp, err := s.card.Transmit(append(s.reqPrefix, tail))
if err != nil {
return nil, err
}
return resp[:len(resp)-2], nil
}
// OSFileWriter writes bytes to disk.
type OSFileWriter struct{}
func (w *OSFileWriter) Write(path string, data []byte) error {
return os.WriteFile(path, data, 0644)
}
// CommandProvider holds all APDUs.
type CommandProvider struct {
SelectThai []byte
Infos []InfoCommand
Photos [][]byte
}
type InfoCommand struct {
Name string
Cmd []byte
}
func NewCommandProvider() *CommandProvider {
return &CommandProvider{
SelectThai: []byte{0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01},
Infos: []InfoCommand{
{"CID", []byte{0x80, 0xB0, 0x00, 0x04, 0x02, 0x00, 0x0D}},
{"ThaiFullname", []byte{0x80, 0xB0, 0x00, 0x11, 0x02, 0x00, 0x64}},
{"EnglishFullname", []byte{0x80, 0xB0, 0x00, 0x75, 0x02, 0x00, 0x64}},
{"Birthdate", []byte{0x80, 0xB0, 0x00, 0xD9, 0x02, 0x00, 0x08}},
{"Gender", []byte{0x80, 0xB0, 0x00, 0xE1, 0x02, 0x00, 0x01}},
{"Issuer", []byte{0x80, 0xB0, 0x00, 0xF6, 0x02, 0x00, 0x64}},
{"IssueDate", []byte{0x80, 0xB0, 0x01, 0x67, 0x02, 0x00, 0x08}},
{"ExpireDate", []byte{0x80, 0xB0, 0x01, 0x6F, 0x02, 0x00, 0x08}},
{"Address", []byte{0x80, 0xB0, 0x15, 0x79, 0x02, 0x00, 0x64}},
},
Photos: func() [][]byte {
out := make([][]byte, 15)
for i := range out {
out[i] = []byte{0x80, 0xB0, byte(i + 1), byte(0x7B - i), 0x02, 0x00, 0xFF}
}
return out
}(),
}
}
// CardProcessor orchestrates reads.
type CardProcessor struct {
svc SmartCardService
cmds *CommandProvider
decoder DataDecoder
writer FileWriter
}
func NewCardProcessor(svc SmartCardService, cmds *CommandProvider, dec DataDecoder, w FileWriter) *CardProcessor {
return &CardProcessor{svc: svc, cmds: cmds, decoder: dec, writer: w}
}
func (p *CardProcessor) Process() error {
st, err := p.svc.Status()
if err != nil {
return err
}
fmt.Printf("Reader: %s, State: %x, ATR: % X\n", st.Reader, st.State, st.Atr)
atr, err := p.svc.ATR()
if err != nil {
return err
}
p.svc.AdjustRequest(atr)
if _, err := p.svc.Transmit(p.cmds.SelectThai); err != nil {
return err
}
for _, ic := range p.cmds.Infos {
raw, err := p.svc.GetData(ic.Cmd)
if err != nil {
return err
}
dec, err := p.decoder.Decode(raw)
if err != nil {
return err
}
fmt.Printf("%s: %s\n", ic.Name, bytes.TrimSpace(dec))
}
// photo
var img []byte
for _, pc := range p.cmds.Photos {
chunk, err := p.svc.GetData(pc)
if err != nil {
return err
}
img = append(img, chunk...)
}
return p.writer.Write(p.cmds.Infos[0].Name+".jpg", img)
}
func main() {
ctx, err := scard.EstablishContext()
if err != nil {
fmt.Println(err)
return
}
defer ctx.Release()
readers, err := ctx.ListReaders()
if err != nil || len(readers) == 0 {
fmt.Println("no readers")
return
}
sel := &ConsoleReaderSelector{}
idx, err := sel.Select(readers)
if err != nil {
fmt.Println(err)
return
}
conn := &ScardConnector{}
card, err := conn.Connect(ctx, readers[idx])
if err != nil {
fmt.Println(err)
return
}
defer card.Disconnect(scard.ResetCard)
// setup services and processor
req := []byte{0x00, 0xC0, 0x00, 0x00}
svc := NewScardService(card, req)
cmds := NewCommandProvider()
dec := charmap.Windows874.NewDecoder()
writer := &OSFileWriter{}
processor := NewCardProcessor(svc, cmds, dec, writer)
if err := processor.Process(); err != nil {
fmt.Println("processing error:", err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment