Skip to content

Instantly share code, notes, and snippets.

@bouroo
Last active January 7, 2026 06:50
Show Gist options
  • Select an option

  • Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.

Select an option

Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.
Thai National ID Card reader in GO
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/ebfe/scard"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/transform"
)
// ============================================================================
// Custom Errors
// ============================================================================
var (
ErrNoReadersAvailable = errors.New("no smart card readers available")
ErrInvalidReaderIndex = errors.New("invalid reader selection")
ErrInvalidCardResponse = errors.New("invalid card response")
ErrInsufficientData = errors.New("insufficient data returned from card")
)
// CardError wraps smart card operation errors with context
type CardError struct {
Op string
Err error
}
func (e *CardError) Error() string {
return fmt.Sprintf("card %s failed: %v", e.Op, e.Err)
}
func (e *CardError) Unwrap() error {
return e.Err
}
// ============================================================================
// Interfaces
// ============================================================================
// ReaderSelector selects a smart card reader from available options
type ReaderSelector interface {
Select(ctx context.Context, readers []string) (int, error)
}
// CardConnector establishes connections to smart cards
type CardConnector interface {
Connect(ctx context.Context, scardCtx *scard.Context, reader string) (*scard.Card, error)
}
// SmartCardService provides smart card operations
type SmartCardService interface {
Status(ctx context.Context) (scard.Status, error)
ATR(ctx context.Context) ([]byte, error)
AdjustRequest(atr []byte)
Transmit(ctx context.Context, cmd []byte) ([]byte, error)
GetData(ctx context.Context, cmd []byte) ([]byte, error)
Close() error
}
// DataDecoder decodes raw bytes (e.g., TIS-620/Windows-874)
type DataDecoder interface {
Decode(input []byte) ([]byte, error)
}
// FileWriter writes data to storage
type FileWriter interface {
Write(ctx context.Context, path string, data []byte) error
}
// Logger provides structured logging
type Logger interface {
Info(msg string, fields ...any)
Error(msg string, err error, fields ...any)
Debug(msg string, fields ...any)
}
// ============================================================================
// Logger Implementation
// ============================================================================
type stdLogger struct {
logger *log.Logger
debug bool
}
func newStdLogger(debug bool) *stdLogger {
return &stdLogger{
logger: log.New(os.Stdout, "[SmartCard] ", log.LstdFlags|log.Lmsgprefix),
debug: debug,
}
}
func (l *stdLogger) Info(msg string, fields ...any) {
if len(fields) > 0 {
l.logger.Printf("%s %v", msg, fields)
} else {
l.logger.Println(msg)
}
}
func (l *stdLogger) Error(msg string, err error, fields ...any) {
args := []any{msg, err}
args = append(args, fields...)
l.logger.Printf("%s: %v %v", args...)
}
func (l *stdLogger) Debug(msg string, fields ...any) {
if l.debug {
if len(fields) > 0 {
l.logger.Printf("[DEBUG] %s %v", msg, fields)
} else {
l.logger.Println("[DEBUG]", msg)
}
}
}
// ============================================================================
// Implementations - Reader Selector
// ============================================================================
// ConsoleReaderSelector prompts the user to select a reader via stdin
type ConsoleReaderSelector struct {
logger Logger
input io.Reader
}
func NewConsoleReaderSelector(logger Logger) *ConsoleReaderSelector {
return &ConsoleReaderSelector{
logger: logger,
input: os.Stdin,
}
}
func (s *ConsoleReaderSelector) Select(ctx context.Context, readers []string) (int, error) {
if len(readers) == 0 {
return -1, ErrNoReadersAvailable
}
// Auto-select if only one reader
if len(readers) == 1 {
s.logger.Info("Auto-selecting reader", "name", readers[0])
return 0, nil
}
s.logger.Info("Available readers:")
for i, r := range readers {
fmt.Fprintf(os.Stdout, " %d) %s\n", i, r)
}
fmt.Fprint(os.Stdout, "Select reader [0]: ")
select {
case <-ctx.Done():
return 0, ctx.Err()
default:
}
var selection string
if _, err := fmt.Fscanln(s.input, &selection); err != nil {
s.logger.Error("Failed to read selection", err)
return 0, &CardError{Op: "select_reader", Err: err}
}
n, err := strconv.Atoi(selection)
if err != nil {
s.logger.Info("Invalid input, defaulting to 0", "input", selection)
return 0, nil
}
if n < 0 || n >= len(readers) {
return 0, &CardError{Op: "select_reader", Err: ErrInvalidReaderIndex}
}
s.logger.Info("Reader selected", "index", n, "name", readers[n])
return n, nil
}
// ============================================================================
// Implementations - Card Connector
// ============================================================================
// ScardConnector wraps scard.Context.Connect
type ScardConnector struct {
logger Logger
}
func NewScardConnector(logger Logger) *ScardConnector {
return &ScardConnector{logger: logger}
}
func (c *ScardConnector) Connect(ctx context.Context, scardCtx *scard.Context, reader string) (*scard.Card, error) {
result := make(chan *scard.Card, 1)
errChan := make(chan error, 1)
go func() {
card, err := scardCtx.Connect(reader, scard.ShareShared, scard.ProtocolAny)
if err != nil {
errChan <- err
return
}
result <- card
}()
select {
case <-ctx.Done():
return nil, &CardError{Op: "connect", Err: ctx.Err()}
case err := <-errChan:
return nil, &CardError{Op: "connect", Err: err}
case card := <-result:
c.logger.Info("Connected to card", "reader", reader)
return card, nil
}
}
// ============================================================================
// Implementations - Smart Card Service
// ============================================================================
// smartCardService implements SmartCardService for scard.Card
type smartCardService struct {
card *scard.Card
reqPrefix []byte
logger Logger
closed bool
}
func NewSmartCardService(card *scard.Card, reqPrefix []byte, logger Logger) SmartCardService {
copyReq := make([]byte, len(reqPrefix))
copy(copyReq, reqPrefix)
return &smartCardService{
card: card,
reqPrefix: copyReq,
logger: logger,
}
}
func (s *smartCardService) Status(ctx context.Context) (scard.Status, error) {
result := make(chan scard.Status, 1)
errChan := make(chan error, 1)
go func() {
st, err := s.card.Status()
if err != nil {
errChan <- err
return
}
result <- st
}()
select {
case <-ctx.Done():
return scard.Status{}, &CardError{Op: "status", Err: ctx.Err()}
case err := <-errChan:
return scard.Status{}, &CardError{Op: "status", Err: err}
case st := <-result:
s.logger.Debug("Card status", "reader", st.Reader, "state", st.State, "atr", st.Atr)
return st, nil
}
}
func (s *smartCardService) ATR(ctx context.Context) ([]byte, error) {
atr, err := s.card.GetAttrib(scard.AttrAtrString)
if err != nil {
return nil, &CardError{Op: "get_atr", Err: err}
}
s.logger.Debug("ATR retrieved", "atr", atr)
return atr, nil
}
func (s *smartCardService) AdjustRequest(atr []byte) {
if len(atr) > 1 && atr[0] == 0x3B && atr[1] == 0x67 {
if len(s.reqPrefix) > 3 {
s.reqPrefix[3] = 0x01
s.logger.Debug("Request prefix adjusted for Thai card")
}
}
}
func (s *smartCardService) Transmit(ctx context.Context, cmd []byte) ([]byte, error) {
result := make(chan []byte, 1)
errChan := make(chan error, 1)
go func() {
resp, err := s.card.Transmit(cmd)
if err != nil {
errChan <- err
return
}
result <- resp
}()
select {
case <-ctx.Done():
return nil, &CardError{Op: "transmit", Err: ctx.Err()}
case err := <-errChan:
return nil, &CardError{Op: "transmit", Err: err}
case resp := <-result:
s.logger.Debug("Transmit complete", "cmd", cmd, "response_len", len(resp))
return resp, nil
}
}
func (s *smartCardService) GetData(ctx context.Context, cmd []byte) ([]byte, error) {
if _, err := s.Transmit(ctx, cmd); err != nil {
return nil, fmt.Errorf("initial transmit failed: %w", err)
}
if len(cmd) == 0 {
return nil, &CardError{Op: "get_data", Err: ErrInsufficientData}
}
tail := cmd[len(cmd)-1]
resp, err := s.Transmit(ctx, append(s.reqPrefix, tail))
if err != nil {
return nil, fmt.Errorf("response transmit failed: %w", err)
}
if len(resp) < 2 {
return nil, &CardError{Op: "get_data", Err: ErrInvalidCardResponse}
}
return resp[:len(resp)-2], nil
}
func (s *smartCardService) Close() error {
if s.closed {
return nil
}
s.closed = true
return s.card.Disconnect(scard.ResetCard)
}
// ============================================================================
// Implementations - Data Decoder
// ============================================================================
// CharmapDecoder wraps charmap.Decoder for DataDecoder interface
type CharmapDecoder struct {
decoder *charmap.Charmap
}
func NewCharmapDecoder(ch *charmap.Charmap) *CharmapDecoder {
return &CharmapDecoder{decoder: ch}
}
func (d *CharmapDecoder) Decode(input []byte) ([]byte, error) {
if len(input) == 0 {
return []byte{}, nil
}
return d.decoder.NewDecoder().Bytes(input), nil
}
// ============================================================================
// Implementations - File Writer
// ============================================================================
// OSFileWriter implements FileWriter using os package
type OSFileWriter struct {
logger Logger
}
func NewOSFileWriter(logger Logger) *OSFileWriter {
return &OSFileWriter{logger: logger}
}
func (w *OSFileWriter) Write(ctx context.Context, path string, data []byte) error {
result := make(chan error, 1)
go func() {
result <- os.WriteFile(path, data, 0644)
}()
select {
case <-ctx.Done():
return &CardError{Op: "write_file", Err: ctx.Err()}
case err := <-result:
if err != nil {
return &CardError{Op: "write_file", Err: err}
}
w.logger.Info("File written", "path", path, "size", len(data))
return nil
}
}
// ============================================================================
// Command Provider
// ============================================================================
// InfoCommand represents a named APDU command for reading card data
type InfoCommand struct {
Name string
Cmd []byte
}
// CommandProvider holds all APDU commands for Thai national ID card
type CommandProvider struct {
SelectThai []byte
Infos []InfoCommand
Photos [][]byte
reqPrefix []byte
reqPrefixLen int
}
// NewCommandProvider creates a new CommandProvider with Thai ID card commands
func NewCommandProvider() *CommandProvider {
reqPrefix := []byte{0x00, 0xC0, 0x00, 0x00}
// Pre-allocate photo commands for efficiency
photos := make([][]byte, 15)
for i := range photos {
photos[i] = []byte{0x80, 0xB0, byte(i + 1), byte(0x7B - i), 0x02, 0x00, 0xFF}
}
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: photos,
reqPrefix: reqPrefix,
reqPrefixLen: len(reqPrefix),
}
}
// ============================================================================
// Card Processor
// ============================================================================
// ProcessResult contains all data read from the card
type ProcessResult struct {
Info map[string]string
Photo []byte
}
// CardProcessor orchestrates the card reading workflow
type CardProcessor struct {
svc SmartCardService
cmds *CommandProvider
decoder DataDecoder
writer FileWriter
logger Logger
photoSize int // Pre-calculated total photo size for optimization
}
// NewCardProcessor creates a new CardProcessor with all dependencies
func NewCardProcessor(
svc SmartCardService,
cmds *CommandProvider,
decoder DataDecoder,
writer FileWriter,
logger Logger,
) *CardProcessor {
// Pre-calculate estimated photo size (15 chunks * max 255 bytes each)
// This is a conservative estimate for pre-allocation
return &CardProcessor{
svc: svc,
cmds: cmds,
decoder: decoder,
writer: writer,
logger: logger,
photoSize: 15 * 255, // Conservative estimate
}
}
// Process reads all data from the smart card
func (p *CardProcessor) Process(ctx context.Context) (*ProcessResult, error) {
result := &ProcessResult{
Info: make(map[string]string),
}
// Check card status
st, err := p.svc.Status(ctx)
if err != nil {
return nil, fmt.Errorf("get status failed: %w", err)
}
p.logger.Info("Card status", "reader", st.Reader, "state", fmt.Sprintf("%x", st.State), "atr", fmt.Sprintf("% X", st.Atr))
// Get ATR and adjust request
atr, err := p.svc.ATR(ctx)
if err != nil {
return nil, fmt.Errorf("get ATR failed: %w", err)
}
p.svc.AdjustRequest(atr)
// Select Thai application
if _, err := p.svc.Transmit(ctx, p.cmds.SelectThai); err != nil {
return nil, fmt.Errorf("select Thai app failed: %w", err)
}
p.logger.Info("Thai application selected")
// Read personal info
for _, ic := range p.cmds.Infos {
raw, err := p.svc.GetData(ctx, ic.Cmd)
if err != nil {
return nil, fmt.Errorf("get %s failed: %w", ic.Name, err)
}
decoded, err := p.decoder.Decode(raw)
if err != nil {
return nil, fmt.Errorf("decode %s failed: %w", ic.Name, err)
}
result.Info[ic.Name] = string(bytes.TrimSpace(decoded))
p.logger.Info("Info read", "field", ic.Name, "value", result.Info[ic.Name])
}
// Read photo chunks with pre-allocation
img := make([]byte, 0, p.photoSize)
for i, pc := range p.cmds.Photos {
chunk, err := p.svc.GetData(ctx, pc)
if err != nil {
return nil, fmt.Errorf("get photo chunk %d failed: %w", i, err)
}
img = append(img, chunk...)
p.logger.Debug("Photo chunk read", "chunk", i, "size", len(chunk))
}
result.Photo = img
p.logger.Info("Photo read complete", "total_size", len(img))
return result, nil
}
// WritePhoto saves the photo to disk
func (p *CardProcessor) WritePhoto(ctx context.Context, photo []byte, outputPath string) error {
if len(photo) == 0 {
return fmt.Errorf("no photo data to write")
}
return p.writer.Write(ctx, outputPath, photo)
}
// ============================================================================
// Main
// ============================================================================
type Config struct {
ReaderTimeout time.Duration
OperationTimeout time.Duration
Debug bool
OutputPhotoPath string
}
func DefaultConfig() *Config {
return &Config{
ReaderTimeout: 30 * time.Second,
OperationTimeout: 10 * time.Second,
Debug: false,
OutputPhotoPath: "output.jpg",
}
}
func main() {
cfg := DefaultConfig()
// Optionally enable debug logging: cfg.Debug = true
// Optionally customize: cfg.OutputPhotoPath = "photo.jpg"
logger := newStdLogger(cfg.Debug)
ctx := context.Background()
// Establish smart card context
scardCtx, err := scard.EstablishContext()
if err != nil {
logger.Error("Failed to establish context", err)
os.Exit(1)
}
defer scardCtx.Release()
// List readers
readers, err := scardCtx.ListReaders()
if err != nil {
logger.Error("Failed to list readers", err)
os.Exit(1)
}
logger.Info("Readers found", "count", len(readers))
// Select reader with timeout
readerCtx, cancel := context.WithTimeout(ctx, cfg.ReaderTimeout)
defer cancel()
selector := NewConsoleReaderSelector(logger)
idx, err := selector.Select(readerCtx, readers)
if err != nil {
logger.Error("Failed to select reader", err)
os.Exit(1)
}
// Connect to card
connectCtx, cancel := context.WithTimeout(ctx, cfg.OperationTimeout)
defer cancel()
connector := NewScardConnector(logger)
card, err := connector.Connect(connectCtx, scardCtx, readers[idx])
if err != nil {
logger.Error("Failed to connect to card", err)
os.Exit(1)
}
defer func() {
if err := card.Disconnect(scard.ResetCard); err != nil {
logger.Error("Failed to disconnect card", err)
}
}()
// Initialize components
cmds := NewCommandProvider()
decoder := NewCharmapDecoder(charmap.Windows874)
writer := NewOSFileWriter(logger)
svc := NewSmartCardService(card, cmds.reqPrefix, logger)
processor := NewCardProcessor(svc, cmds, decoder, writer, logger)
// Process card with timeout
procCtx, cancel := context.WithTimeout(ctx, cfg.OperationTimeout*6) // Longer timeout for full process
defer cancel()
result, err := processor.Process(procCtx)
if err != nil {
logger.Error("Processing error", err)
os.Exit(1)
}
// Write photo
if err := processor.WritePhoto(procCtx, result.Photo, cfg.OutputPhotoPath); err != nil {
logger.Error("Failed to write photo", err)
os.Exit(1)
}
// Print results
logger.Info("=== Card Data ===")
fmt.Println()
for name, value := range result.Info {
fmt.Printf("%-18s : %s\n", name, value)
}
fmt.Println()
logger.Info("Photo saved", "path", cfg.OutputPhotoPath)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment