Last active
May 9, 2025 14:43
-
-
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([]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