Last active
October 18, 2023 17:12
-
-
Save bouroo/493448154cadaea87de792e9baf9e8e9 to your computer and use it in GitHub Desktop.
Thai ID card reader in Rust
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
extern crate pcsc; | |
use std::io; | |
use std::io::prelude::*; | |
use std::fs::File; | |
use std::str; | |
use std::string::String; | |
use pcsc::{Card, Context, ShareMode, Scope}; | |
use pcsc::Error; | |
use pcsc::CardState; | |
fn main() -> Result<(), Error> { | |
// Create context with pcscd | |
let context = Context::establish(Scope::User)?; | |
// Get all readers | |
let readers = context.list_readers()?; | |
if readers.is_empty() { | |
println!("Error: Card readers not found."); | |
return Ok(()); | |
} | |
println!("Readers:"); | |
for (r_idx, r_item) in readers.iter().enumerate() { | |
println!("Card reader ID: {}, Item: {}", r_idx, r_item); | |
} | |
// Select reader | |
let reader_idx = select_reader(&readers)?; | |
let reader = &readers[reader_idx]; | |
// Connect to the card | |
let card = context.connect(reader, ShareMode::Shared, Default::default())?; | |
println!("Card status:"); | |
let status = card.status()?; | |
println!( | |
"\treader: {}\n\tstate: {:x}\n\tactive protocol: {:x}\n\tatr: {:x?}", | |
status.reader, status.state, status.protocol, status.atr | |
); | |
let atr = card.get_attrib(pcsc::Attribute::AtrString)?; | |
// Get card attribute | |
println!("Card ATR: {}", str::from_utf8(&atr)?); | |
let cmd_req = get_cmd_req(&atr); | |
println!("Req: {:?}", cmd_req); | |
// Select the Thai national ID card | |
card.transmit(&[0x00, 0xA4, 0x04, 0x00, 0x08, 0xA0, 0x00, 0x00, 0x00, 0x54, 0x48, 0x00, 0x01])?; | |
let cid = get_string(&card, &cmd_req, &cmdCID)?; | |
println!("cid: {}", cid); | |
let th_fullname = get_string(&card, &cmd_req, &cmdTHFullname)?; | |
println!("thFullname: {}", th_fullname); | |
let en_fullname = get_string(&card, &cmd_req, &cmdENFullname)?; | |
println!("enFullname: {}", en_fullname); | |
let date_of_birth = get_string(&card, &cmd_req, &cmdBirth)?; | |
println!("dateOfBirth: {}", date_of_birth); | |
let gender = get_string(&card, &cmd_req, &cmdGender)?; | |
println!("gender: {}", gender); | |
let issuer = get_string(&card, &cmd_req, &cmdIssuer)?; | |
println!("issuer: {}", issuer); | |
let issue_date = get_string(&card, &cmd_req, &cmdIssueDate)?; | |
println!("issueDate: {}", issue_date); | |
let expire_date = get_string(&card, &cmd_req, &cmdExpireDate)?; | |
println!("expireDate: {}", expire_date); | |
let address = get_string(&card, &cmd_req, &cmdAddress)?; | |
println!("address: {}", address); | |
let photo = get_photo(&card, &cmd_req)?; | |
save_photo(&cid, &photo)?; | |
Ok(()) | |
} | |
fn select_reader(readers: &[String]) -> Result<usize, Error> { | |
let reader_idx: usize; | |
if readers.len() > 1 { | |
loop { | |
let reader_idx_input = user_input("Select card reader ID [0]: ")?; | |
match reader_idx_input.parse::<usize>() { | |
Ok(idx) if idx < readers.len() => { | |
reader_idx = idx; | |
break; | |
} | |
_ => { | |
println!("Error selecting reader: invalid input"); | |
} | |
} | |
} | |
} else { | |
reader_idx = 0; | |
} | |
Ok(reader_idx) | |
} | |
fn user_input(prompt: &str) -> Result<String, Error> { | |
print!("{}", prompt); | |
io::stdout().flush()?; | |
let mut input = String::new(); | |
io::stdin().read_line(&mut input)?; | |
Ok(input.trim().to_string()) | |
} | |
fn get_cmd_req(atr: &[u8]) -> Vec<u8> { | |
if atr.len() > 1 && atr[0] == 0x3B && atr[1] == 0x67 { | |
vec![0x00, 0xc0, 0x00, 0x01] | |
} else { | |
vec![0x00, 0xc0, 0x00, 0x00] | |
} | |
} | |
fn get_string(card: &Card, req: &[u8], cmd: &[u8]) -> Result<String, Error> { | |
let raw_resp = get_data(card, cmd, req)?; | |
let th_resp = thai_to_unicode(&raw_resp)?; | |
Ok(th_resp.trim().to_string()) | |
} | |
fn thai_to_unicode(data: &[u8]) -> Result<String, Error> { | |
let decoder = charmap::decode(charmap::WINDOWS_874); | |
let result = decoder.decode(data); | |
Ok(result) | |
} | |
fn get_data(card: &Card, cmd: &[u8], req: &[u8]) -> Result<Vec<u8>, Error> { | |
// Send cmd | |
card.transmit(cmd)?; | |
// Send select cmd | |
let mut req = req.to_vec(); | |
req.push(cmd[cmd.len() - 1]); | |
let resp = card.transmit(&req)?; | |
Ok(resp[..resp.len() - 2].to_vec()) | |
} | |
fn get_photo(card: &Card, req: &[u8]) -> Result<Vec<u8>, Error> { | |
let mut resp = Vec::new(); | |
for item_cmd in cmdPhoto.iter() { | |
let tmp_array = get_data(card, item_cmd, req)?; | |
resp.extend_from_slice(&tmp_array); | |
} | |
Ok(resp) | |
} | |
fn save_photo(filename: &str, data: &[u8]) -> Result<(), io::Error> { | |
let mut file = File::create(format!("{}.jpg", filename))?; | |
file.write_all(data)?; | |
Ok(()) | |
} | |
// Define other command constants here | |
const cmdCID: [u8; 7] = [0x80, 0xb0, 0x00, 0x04, 0x02, 0x00, 0x0d]; | |
const cmdTHFullname: [u8; 7] = [0x80, 0xb0, 0x00, 0x11, 0x02, 0x00, 0x64]; | |
const cmdENFullname: [u8; 7] = [0x80, 0xb0, 0x00, 0x75, 0x02, 0x00, 0x64]; | |
const cmdBirth: [u8; 7] = [0x80, 0xb0, 0x00, 0xD9, 0x02, 0x00, 0x08]; | |
const cmdGender: [u8; 7] = [0x80, 0xb0, 0x00, 0xE1, 0x02, 0x00, 0x01]; | |
const cmdIssuer: [u8; 7] = [0x80, 0xb0, 0x00, 0xF6, 0x02, 0x00, 0x64]; | |
const cmdIssueDate: [u8; 7] = [0x80, 0xb0, 0x01, 0x67, 0x02, 0x00, 0x08]; | |
const cmdExpireDate: [u8; 7] = [0x80, 0xb0, 0x01, 0x6F, 0x02, 0x00, 0x08]; | |
const cmdAddress: [u8; 7] = [0x80, 0xb0, 0x15, 0x79, 0x02, 0x00, 0x64]; | |
const cmdPhoto: [[u8; 7]; 20] = [ | |
[0x80, 0xb0, 0x01, 0x7B, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x02, 0x7A, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x03, 0x79, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x04, 0x78, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x05, 0x77, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x06, 0x76, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x07, 0x75, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x08, 0x74, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x09, 0x73, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0A, 0x72, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0B, 0x71, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0C, 0x70, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0D, 0x6F, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0E, 0x6E, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x0F, 0x6D, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x10, 0x6C, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x11, 0x6B, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x12, 0x6A, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x13, 0x69, 0x02, 0x00, 0xFF], | |
[0x80, 0xb0, 0x14, 0x68, 0x02, 0x00, 0xFF], | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment