Last active
January 8, 2025 12:51
-
-
Save bouroo/a3aa857727d909283c33e7b00623b48d to your computer and use it in GitHub Desktop.
Thai National ID Card reader in GO
This file contains 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" | |
"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