Last active
July 8, 2025 08:51
-
-
Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.
Thai National ID Card reader in GO
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
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(cmd []byte) ([]byte, error) | |
} | |
type DataDecoder interface { | |
Decode([]byte) ([]byte, error) | |
} | |
type FileWriter interface { | |
Write(path string, data []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) | |
} | |
// SmartCardService implementation for scard.Card | |
type smartCardService struct { | |
card *scard.Card | |
reqPrefix []byte | |
} | |
func NewSmartCardService(card *scard.Card, req []byte) SmartCardService { | |
copyReq := make([]byte, len(req)) | |
copy(copyReq, req) | |
return &smartCardService{card: card, reqPrefix: copyReq} | |
} | |
func (s *smartCardService) Status() (scard.Status, error) { | |
return s.card.Status() | |
} | |
func (s *smartCardService) ATR() ([]byte, error) { | |
return s.card.GetAttrib(scard.AttrAtrString) | |
} | |
func (s *smartCardService) AdjustRequest(atr []byte) { | |
if len(atr) > 1 && atr[0] == 0x3B && atr[1] == 0x67 { | |
s.reqPrefix[3] = 0x01 | |
} | |
} | |
func (s *smartCardService) Transmit(cmd []byte) ([]byte, error) { | |
return s.card.Transmit(cmd) | |
} | |
func (s *smartCardService) 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 | |
reqPrefix []byte | |
reqPrefixLen int | |
} | |
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 | |
}(), | |
reqPrefix: []byte{0x00, 0xC0, 0x00, 0x00}, | |
reqPrefixLen: len([]byte{0x00, 0xC0, 0x00, 0x00}), | |
} | |
} | |
// 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 | |
} | |
decoded, err := p.decoder.Decode(raw) | |
if err != nil { | |
return err | |
} | |
fmt.Printf("%s: %s\n", ic.Name, bytes.TrimSpace(decoded)) | |
} | |
// 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("output.jpg", img) | |
} | |
func main() { | |
ctx, err := scard.EstablishContext() | |
if err != nil { | |
fmt.Println("Failed to establish context:", err) | |
return | |
} | |
defer ctx.Release() | |
readers, err := ctx.ListReaders() | |
if err != nil { | |
fmt.Println("Failed to list readers:", err) | |
return | |
} | |
idx, err := (&ConsoleReaderSelector{}).Select(readers) | |
if err != nil { | |
fmt.Println("Failed to select reader:", err) | |
return | |
} | |
connector := &ScardConnector{} | |
card, err := connector.Connect(ctx, readers[idx]) | |
if err != nil { | |
fmt.Println("Failed to connect to card:", err) | |
return | |
} | |
defer card.Disconnect(scard.ResetCard) | |
cmds := NewCommandProvider() | |
decoder := charmap.Windows874.NewDecoder() | |
writer := &OSFileWriter{} | |
svc := NewSmartCardService(card, cmds.reqPrefix) | |
processor := NewCardProcessor(svc, cmds, decoder, 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