สร้างโปรแกรม Scan Thai ID Card ด้วยภาษา Go-lang

ก่อนอื่นเลย เราจะต้องรู้ก่อนว่า Service ที่เรากำลังจะสร้าง เราจะทำไปเพื่ออะไร ตอบแบบภาพรวมก็คือ โดยการเสียบบัตรเข้าไปที่เครื่องสแกน เพื่อดึงข้อมูลจากบัตรประชาชน แล้ว นำข้อมูลนั้นไปยืนยันหรือเอาไปแนบอ้างอิงธุรกรรมต่าง ๆ โดยไม่ต้องมาเสียเวลากรอกข้อมูลเอง และลดความเสี่ยงเจอบัตรประชาชนปลอม

ตัวอย่างหน่วยงานที่ควรใช้เพื่อให้ทำงานสะดวกขึ้นมีอะไรบ้างก็เช่น

ภาพจาก : ประชาชาติธุรกิจ

  1. หน่วยงานเกี่ยวกับธุรกรรมทางการเงิน เช่น เมื่อเราไปสมัครใช้งานบริการทางการเงินธนาคาร ซึ่งเราไม่เคยทำธุรกรรมกับธนาคาร บริการนี้จำเป็นต้องมีการเก็บข้อมูลจากบัตรประชาชน
  2. บริการจัดส่งพัสดุ เช่น เราไปส่งของที่ไปรษณีย์ พนักงานจะต้องขอข้อมูลบัตรประชาชน เพื่อที่จะตรวจยืนยันว่าเป็นบุคลที่ส่งของคนนี้จริง ๆ
  3. หน่วยงานต่าง ๆ ทั้งหน่วยงานราชการและเอกชน ที่ระบบเดิม ไม่ว่าจะเป็นฟอร์มกระดาษที่ต้องกรอกเอง หรือแบบฟอร์มอิเล็กทรอนิกส์ที่ยังมากรอกทีละช่อง

 

โปรแกรมมีการทำงานอย่างไรรายละเอียดเราจะดึงก็ข้อมูลจากบัตรได้อย่างไรเดี๋ยวเรามาทำไปด้วยกันครับ

เครื่องมือที่เราใช้มีอะไรบ้างมาดูกัน

ภาษา Go-lang

Visual Studio Code โปรแกรม IDE ใช้สำหรับเขียน Code Go-lang (หรือโปรแกรมอะไรก็ได้ที่ถนัดและสามารถใช้ทำกับ ภาษา GO ที่เลือกมาเพราะเป็นโปรแกรมที่ใช้กันทั่วไป มี extension ให้เราเลือกลงได้ และที่มันก็ฟรีด้วย)

เครื่องสแกนบัตร ในที่นี้ที่ยกตัวอย่างคือ ยี่ห้อ ACS รุ่น ACR39U-NFF โดยใช้ port ต่อผ่าน USB Type C ราคาโดยประมาณ 1,000 บาท (บวกลบเล็กน้อย)

ภาพจาก :  R&D COMPUTER SYSTEM CO.,LTD.

เตรียมตัวก่อนเริ่มเขียนโปรแกรม

  1. เริ่มต้นจากติดตั้ง Go-lang (ดูวิธีเพิ่มเติมได้ที่ https://go.dev/doc/install)
  2. ดาวน์โหลด และติดตั้ง Visual Studio Code (Link Download: https://code.visualstudio.com/download)
  3. ผมจะแนะนำให้ติดตั้ง extension ของ GO ด้วยใน Visual Studio Code โดยไปที่ extension แล้วพิมพ์ค้นหาคำว่า GO จะเจอดังรูปด้านล่างแล้วเรากด Install ได้เลย

 

สร้าง Project ใหม่

1) สร้าง Folder  หรือ Directory ใหม่ขึ้นมาด้วการรันคำสั่งใน Command Line >> mkdir go-scancard-api

2) สร้างไฟล์ go.mod ในโปรเจกต์ของเรา โดยรันคำสั่ง go mod init >> go mod init example/go-scancard-api

3) สร้างไฟล์ชื่อ main.go ใน Floder และเขียนโค้ด Hello World ง่ายๆ กันก่อน

package main

 

import (

“fmt”

)

 

func main() {

fmt.Println(“Hello World!”)

}

 

4) ลองรันโค้ดด้วยคำสั่งใน Command Line ใน folder โปเจ็ค

go run .

ผลลัพธ์ ขึ้นข้อความ Hello World! บนจอ terminal แล้ว( . ในที่นี้หมายถึง run ไฟล์ go ทั้งหมดใน project นี้ ทำไมเราใช้ เพราะว่าในขันตอนถัด ๆ ไป เราจะมีไฟล์ต่าง ๆ เพิ่มขึ้นมา)

 

สร้าง Service Scan Card ด้วย Library sscard

1) ก่อนอื่นเราจะเพิ่ม library เข้ามาใน Project เราก่อน

go get -u golang.org/x/net/html/charset

go get -u github.com/gogetth/sscard

go get github.com/ebfe/scard

 

2) สร้างไฟล์ exampleThaiIDCard.go และใส่โค้ดเริ่มต้นโดย main หลักจะเป็นฟังชั่น exampleThaiIDCard()

package main

 

import (

“fmt”

“github.com/ebfe/scard”

“github.com/gogetth/sscard”

)

 

func exampleThaiIDCard() {

 

// Establish a PC/SC context

context, err := scard.EstablishContext()

if err != nil {

fmt.Println(“Error EstablishContext:”, err)

return

}

 

// Release the PC/SC context (when needed)

defer context.Release()

 

// List available readers

readers, err := context.ListReaders()

if err != nil {

fmt.Println(“Error ListReaders:”, err)

return

}

 

// Use the first reader

reader := readers[0]

fmt.Println(“Using reader:”, reader)

 

// Connect to the card

card, err := context.Connect(reader, scard.ShareShared, scard.ProtocolAny)

if err != nil {

fmt.Println(“Error Connect:”, err)

return

}

 

// Disconnect (when needed)

defer card.Disconnect(scard.LeaveCard)

 

// Send select APDU

selectRsp, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardSelect)

if err != nil {

fmt.Println(“Error Transmit:”, err)

return

}

fmt.Println(“resp sscard.APDUThaiIDCardSelect: “, selectRsp)

 

}

 

3) ไฟล์ main.go เราจะแก้โดยการเอา exampleThaiIDCard() ไปใส่

package main

 

func main() {

exampleThaiIDCard()

}

 

4) เราจะรันไฟล์ด้วยคำสั่ง go run . โดยตอนนี้จะทดสอบสถานะเครื่อง แต่ยังไม่ดึงข้อมูลจากบัตร

4.1  รันโดยไม่เสียบเครื่องอ่านบัตร

รันคำสั่ง

ผลลัพธ์ จะเห็นได้ว่าจะขึ้นแจ้ง error บอกว่า ไม่สามารถหาเครื่องอ่านบัตรเจอ

 

โดย error นี้มาจากฟั่งชั่นใน Code คือ context.ListReaders()

 

4.2 รันโดยเสียบเครื่องอ่านบัตรแต่ยังไม่เสียบ บัตรประชาชน เข้าไปด้วย

 

รันคำสั่ง

ผลลัพธ์ ตรงเส้นใต้สีเหลืองจะเห็นแล้วว่า หลังจากที่เราเสียบเครื่องอ่านบัตร เราได้ใช้เครื่องรุ่นอะไร และเส้นใต้สีแดงเป็นการแจ้งว่าบัตรไม่ได้ถูกเสียบกับเครื่องอ่านบัตรอยู่

 

4.3 รันโดยเสียบเครื่องอ่านบัตร แต่เราจะเสียบบัตรอื่นที่ไม่ใช่บัตรประชาชนเข้าไปครับ ตัวอย่างนี้ เสียบบัตรอื่นเข้าไปแทน หรือจะกลับด้านเอาด้านที่ไม่มี chip เสียบเข้าไปแทน

 

รันคำสั่ง

ผลลัพธ์ เราจะเห็นว่าโปรแกรมจะแจ้ง error บอกว่าไม่สามารถส่ง request ไปขอข้อมูลที่ chip ของบัตรได้

โดยในข้อ 4.2 – 4.3 ฟั่งชั่น context.Connect(reader, scard.ShareShared, scard.ProtocolAny) จะเป็นฟั่งชั่นของการอ่านบัตร

 

4.4 รันโดยเสียบเครื่องอ่านบัตรและเสียบบัตรประชาชนเข้าไปด้วย และทดลองเอาบัตรอื่นเสียบที่เครื่อง

 

รันคำสั่ง

ผลลัพธ์ เราจะเห็นว่าโปรแกรมอ่านบัตรเราเจอ และส่งเลข APDU ว่าเป็นบัตรประชาชนจริง ๆ

โดยฟั่งชั่น selectRsp, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardSelect) จะเป็นตัวอย่างตัวเริ่ม return response ของบัตรแล้วการดึงข้อมูลแต่ละฟิลจะดึงยังไงเดี๋ยวเราไปดูในหัวข้อถัดไปกัน

 

5. เพิ่ม code ในส่วนของการอ่านข้อมูลบัตร ใส่ลง ในไฟล์ exampleThaiIDCard.go

5.1 เพิ่มฟั่งชั่น ConvertTIS620toUTF8(tis620 string) คือการแปลง TIS620 ก่อนเพราะถ้าเราดึง Field ตอน return ค่าออกมามันจะแถมอักขระพิเศษทั้งมองเห็นและไม่เห็น และจะเพี้ยนเมื่อนำไปใช้ เราจึงต้องลบมันออก

func ConvertTIS620toUTF8(tis620 string) (valueUTF8 string) {

tis620Valure := []byte(tis620)

dec := charmap.Windows874.NewDecoder()

makeUTF := make([]byte, len(tis620Valure)*3)

n, _, err := dec.Transform(makeUTF, tis620Valure, false)

if err != nil {

return “This Filed can’t Convert Tis620 to UTF8”

}

valueUTF8 = string(makeUTF[:n])

valueUTF8 = strings.Trim(valueUTF8, “\u0000”)

valueUTF8 = strings.ReplaceAll(valueUTF8, “#”, ” “)

 

valueUTF8 = strings.TrimSpace(valueUTF8)

return

}

 

5.2 เพิ่ม code อ่านฟิล  โค้ดเราจะใส่ไว้ในฟั่งชั่น  exampleThaiIDCard()

ตัวอย่างเรายกมา 3 field

        • CID เลขบัตรประชาชน
        • fullnameEN ชื่อเต็ม ภาษาอังกฤษ
        • fullnameTH ชื่อเต็ม ภาษาไทย
        • birth วันเกิด

 

              cid, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardCID)

if err != nil {

fmt.Println(“Error APDUGetRsp: “, err)

return

}

fmt.Printf(“cid: _%s_\n”, string(cid))

 

fullnameEN, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardFullnameEn)

if err != nil {

fmt.Println(“Error APDUGetRsp: “, err)

return

}

fmt.Printf(“fullnameEN: _%s_\n”, ConvertTIS620toUTF8(string(fullnameEN)))

 

fullnameTH, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardFullnameTh)

fmt.Println(“len v = “, len(fullnameTH))

if err != nil {

fmt.Println(“Error APDUGetRsp: “, err)

return

}

fmt.Printf(“fullnameTH: _%s_\n”, ConvertTIS620toUTF8(string(fullnameTH)))

 

birth, err := sscard.APDUGetRsp(card, sscard.APDUThaiIDCardBirth)

if err != nil {

fmt.Println(“Error APDUGetRsp: “, err)

return

}

fmt.Printf(“birth: _%s_\n”, ConvertTIS620toUTF8(string(birth)))

 

 

6. รันโปรแกรมดูผลลัพธ์ที่ดึงค่าฟิลที่เราระบุไว้

ผลลัพธ์ เราจะเห็นค่า Field ที่ return แล้วในบัตรประชาชน ตรง fullnameTH เราอย่างพึ่งตกใจไปทำไมเป็นข้อความไม่สมประกอบ เหตุผลคือมันแสดงในหน้าจอของ terminal ด้วยภาษาไทย แต่ถ้าเรา copy คำนั้นใส่ word หรือ notepad เราก็จะเห็นข้อความภาษาไทยปกติ

 

  1. มันดึงข้อมูลจาก field ต่าง ๆ ยังไงกัน

การดึงข้อมูลจาก chip ของบัตรประชาชน อธิบายหลักการง่าย ๆ คือ

1)เราจะส่ง request ระบุไปก่อนว่าจะดึง Field ไหน

2) เป็นจังหวะเราส่ง request ขอ response จาก Feild นั้นออกมา

โดยข้อมูลที่ส่งไปจะเป็นค่า hex ซึ่งใน Library sscard มีชุดคำสั่งค่า hex แต่ละ Field ไว้ให้แล้วสะดวกมาก ซึ่งอยู่ในไฟล์ apducmd_thidcard.go ของ Library

ตัวอย่างตัวแปลชุดคำสั่งดึง Field CID

แล้วเราจะรู้ที่มาที่ไปของเลข hax บัตรประชาชนไทยได้อย่างไร ผมขออนุญาติแนบบทความเกี่ยวกับเนื้อหานี้เพิ่มเติมนะครับ >>> Smart card: บัตรประชาชนยุคดิจิตอล | by patda9 | horganice | Medium

ตอนนี้เราพอรู้แล้วว่าแต่ละ Field เราจะดึงข้อมูลออกมาอย่างไร หลังจากนั้นเราค่อยไปเพิ่มในส่วน Field ที่อยากได้มาแสดงตามความต้องการ และต่อมาเราจะดึงรูปภาพของเราจากบัตรประชาชนกันครับ

 

8. การดึงรูปภาพจากบัตรประชาชน

8.1 ใส่โค้ด การดึงรูปจากบัตรประชาชน ใส่ในฟังชั่น exampleThaiIDCard()

              cardPhotoJpg, err := sscard.APDUGetBlockRsp(card, sscard.APDUThaiIDCardPhoto, sscard.APDUThaiIDCardPhotoRsp)

if err != nil {

fmt.Println(“Error: “, err)

return

}

 

//Write Image

n2, err := sscard.WriteBlockToFile(cardPhotoJpg, “./idcPhoto.jpg”)

if err != nil {

fmt.Println(“Error WriteBlockToFile: “, err)

return

}

fmt.Printf(“Img wrote %d bytes\n”, n2)

ผลลัพธ์ ไฟล์รูปจะถูก save ไว้ที่โปรเจ็คของเรา ถ้าเรามาดูตรง Terminal เราจะเห็นว่ารูปภาพเราเขียนลงไปกี่ bytes

 

โดย sscard.WriteBlockToFile(cardPhotoJpg, “./idcPhoto.jpg”) จะประกอบด้วย Parameter 2 คือ  1.ตัวที่รับ Response จากฟั่งชั่นดึงรูป  2. กำหนด paht ที่เราจะ Save รูปลง กำหนดนามสกุลไฟล์ให้มันด้วย

 

9. Build ไฟล์ .exe ของโปรเจ็ค

รันคำสั่ง

go build

ผลลัพธ์ เราจะได้ไฟล์ go-scan-thai-card.exe ที่โปรเจ็คของเราครับ

 

ที่กล่าวข้างต้นเราได้ทดสอบการใช้เครื่องสแกนบัตร และดึงข้อมูลจากบัตรจาก Field ต่าง ๆ ถ้าไม่เคยเล่นคำสั่ง ADPU จะเห็นว่าการส่งขอข้อมูลเป็นเทคนิคเฉพาะ โดยต้องรู้เกี่ยวกับเลขฐาน(hex) ด้วย และตอนนี้เราได้โปรแกรมที่นำไปใช้งานได้แล้ว แต่ยังมีการนำโปรแกรมไปใช้งานในจุดประสงค์อื่น ๆ เช่นนำไปรันไว้ใน server หรือเราจะสร้าง service ให้รันในเครื่อง ส่วนตรงนี้เราจะเปลี่ยนโปรแกรมธรรมดาให้เป็น service เดี๋ยวเราจะมาสร้าง service กันต่อในบทความต่อไปกันครับ 🙂

ทั้งนี้ ทางเราได้อัพโหลดโปรเจ็คนี้ขึ้นบน github ท่านที่สนใจสามารถสอบถามรายละเอียดเพิ่มเติมได้ที่ marketing@stream.co.th

สุดท้ายนี้ทางทีมงานสตรีมฯ ของเรายังมี โซลูชั่น เทคโนโลยี นวัตกรรม ด้านดิจิทัลอื่น ๆ อีกมากมาย ครอบคลุมเกือบทุกอุตสาหกรรม รอติดตามบทความต่อๆไปได้เลยครับ

 

เรียบเรียงโดย Piyapat Khajornpan

Stream I.T. Consulting Ltd.