//! PC/SC access to the reader (ACR1252U PICC slot) and Type 2 tag memory. use crate::ndef::{self, Record, RecordView}; use pcsc::{ Attribute, Card, Context, Error as PcscError, MAX_BUFFER_SIZE, Protocols, Scope, ShareMode, }; use serde::Serialize; const GET_UID: [u8; 5] = [0xFF, 0xCA, 0x00, 0x00, 0x00]; /// Last inspected tag, so the status poll does not re-read the whole tag every second. #[derive(Default)] pub struct Cache { tag: Option, } #[derive(Clone, Serialize)] pub struct TagInfo { pub uid: String, pub kind: String, pub formatted: bool, pub capacity: Option, pub used: Option, pub writable: bool, pub records: Vec, /// Why the tag cannot be programmed, or a read error. pub note: Option, } #[derive(Serialize)] #[serde(tag = "state", rename_all = "snake_case")] pub enum Status { NoReader { message: String }, NoCard { reader: String }, Card { reader: String, tag: Box }, Error { message: String }, } #[derive(Serialize)] pub struct Programmed { pub uid: String, pub record: RecordView, pub bytes: usize, pub capacity: usize, } enum Kind { Type2, Unsupported(String), } struct Cc { formatted: bool, capacity: usize, writable: bool, } fn open() -> Result<(String, Option), Status> { let no_reader = || Status::NoReader { message: "No NFC reader found. Is the ACR1252U plugged in?".into(), }; let ctx = Context::establish(Scope::User).map_err(|e| match e { PcscError::NoService | PcscError::ServiceStopped | PcscError::NoReadersAvailable => { no_reader() } e => Status::Error { message: format!("PC/SC: {e}"), }, })?; let readers = ctx.list_readers_owned().map_err(|e| match e { PcscError::NoReadersAvailable | PcscError::NoService | PcscError::ServiceStopped => { no_reader() } e => Status::Error { message: format!("PC/SC: {e}"), }, })?; // The ACR1252U shows up as two readers: "... PICC 0" (contactless) and "... SAM 0". let reader = readers .iter() .find(|r| r.to_string_lossy().contains("PICC")) .or_else(|| readers.first()) .ok_or_else(no_reader)?; let name = reader.to_string_lossy().into_owned(); match ctx.connect(reader, ShareMode::Shared, Protocols::ANY) { Ok(card) => Ok((name, Some(card))), Err( PcscError::NoSmartcard | PcscError::RemovedCard | PcscError::UnpoweredCard | PcscError::UnresponsiveCard | PcscError::ResetCard, ) => Ok((name, None)), Err(PcscError::ReaderUnavailable | PcscError::UnknownReader) => Err(no_reader()), Err(e) => Err(Status::Error { message: format!("Connecting to the tag failed: {e}"), }), } } fn apdu(card: &Card, cmd: &[u8]) -> Result, String> { let mut buf = [0u8; MAX_BUFFER_SIZE]; let r = card.transmit(cmd, &mut buf).map_err(|e| { format!("Tag communication failed ({e}). Keep the tag still on the reader.") })?; if r.len() < 2 { return Err("Reader returned a short response.".into()); } let (data, sw) = r.split_at(r.len() - 2); if sw != [0x90, 0x00] { return Err(format!( "Tag rejected the command (status {:02X}{:02X}).", sw[0], sw[1] )); } Ok(data.to_vec()) } fn hex(b: &[u8]) -> String { b.iter() .map(|x| format!("{x:02X}")) .collect::>() .join(":") } /// Card type from the PC/SC Part 3 storage-card ATR (3B 8F 80 01 80 4F 0C A0 00 00 03 06 SS NN NN ...). fn kind(atr: &[u8]) -> Kind { let storage = atr.len() >= 15 && atr[..7] == [0x3B, 0x8F, 0x80, 0x01, 0x80, 0x4F, 0x0C] && atr[7..12] == [0xA0, 0x00, 0x00, 0x03, 0x06]; if !storage { return Kind::Unsupported("ISO 14443-4 card (e.g. DESFire, bank card or phone)".into()); } match (atr[13], atr[14]) { (0x00, 0x03) | (0x00, 0x3A) => Kind::Type2, (0x00, 0x01) => Kind::Unsupported("MIFARE Classic 1K".into()), (0x00, 0x02) => Kind::Unsupported("MIFARE Classic 4K".into()), (0x00, 0x26) => Kind::Unsupported("MIFARE Mini".into()), (a, b) => Kind::Unsupported(format!("storage card type {a:02X}{b:02X}")), } } fn type2_name(cc: &Cc) -> String { let chip = match cc.capacity { 48 => " (MIFARE Ultralight)", 128 => " (Ultralight EV1)", 144 => " (NTAG213)", 496 => " (NTAG215)", 872 => " (NTAG216)", _ => "", }; format!("NFC Type 2{chip}") } fn read_cc(card: &Card) -> Result { let d = apdu(card, &[0xFF, 0xB0, 0x00, 0x03, 0x10])?; // pages 3..6 let cc = d .get(..4) .ok_or("Could not read the capability container.")?; Ok(Cc { formatted: cc[0] == 0xE1, capacity: cc[2] as usize * 8, // lower nibble of byte 3 = write access (0 = granted) writable: cc[3] & 0x0F == 0, }) } /// Read `len` bytes starting at page 4, 16 bytes (4 pages) per command. fn read_data(card: &Card, len: usize) -> Result, String> { let mut out = Vec::with_capacity(len + 16); let mut page = 4u8; while out.len() < len { out.extend(apdu(card, &[0xFF, 0xB0, 0x00, page, 0x10])?); page = page.checked_add(4).ok_or("Tag memory too large.")?; } out.truncate(len); Ok(out) } fn write_page(card: &Card, page: usize, data: &[u8]) -> Result<(), String> { let page = u8::try_from(page).map_err(|_| "Page number out of range.")?; let mut cmd = vec![0xFF, 0xD6, 0x00, page, 0x04]; cmd.extend_from_slice(data); apdu(card, &cmd).map(|_| ()) } fn inspect(card: &Card, uid: String) -> TagInfo { let mut info = TagInfo { uid, kind: String::new(), formatted: false, capacity: None, used: None, writable: false, records: vec![], note: None, }; let atr = card .get_attribute_owned(Attribute::AtrString) .unwrap_or_default(); if let Kind::Unsupported(name) = kind(&atr) { info.kind = name; info.note = Some("Only NTAG21x / MIFARE Ultralight (NFC Type 2) tags can be programmed.".into()); return info; } let result = (|| -> Result<(), String> { let cc = read_cc(card)?; info.kind = type2_name(&cc); info.formatted = cc.formatted; info.writable = cc.writable; if !cc.formatted { info.note = Some("Tag is not NDEF formatted.".into()); return Ok(()); } info.capacity = Some(cc.capacity); if !cc.writable { info.note = Some("Tag is locked (read-only).".into()); } let area = read_data(card, cc.capacity)?; match ndef::find_message(&area)? { Some((msg, used)) => { info.used = Some(used); info.records = ndef::decode(msg)?; } None => info.used = Some(0), } Ok(()) })(); if let Err(e) = result { if info.kind.is_empty() { info.kind = "NFC Type 2".into(); } info.note = Some(e); } info } pub fn status(cache: &mut Cache) -> Status { let (reader, card) = match open() { Ok(x) => x, Err(s) => { cache.tag = None; return s; } }; let Some(card) = card else { cache.tag = None; return Status::NoCard { reader }; }; let uid = match apdu(&card, &GET_UID) { Ok(u) => hex(&u), Err(_) => return Status::NoCard { reader }, }; if let Some(tag) = cache.tag.as_ref().filter(|t| t.uid == uid) { return Status::Card { reader, tag: Box::new(tag.clone()), }; } let tag = inspect(&card, uid); // Don't cache a failed read (tag moved away mid-read): retry on the next poll. let failed = tag .note .as_deref() .is_some_and(|n| n.contains("communication") || n.contains("rejected")); cache.tag = (!failed).then(|| tag.clone()); Status::Card { reader, tag: Box::new(tag), } } fn status_message(s: Status) -> String { match s { Status::NoReader { message } | Status::Error { message } => message, Status::NoCard { .. } => "No tag on the reader.".into(), Status::Card { .. } => "Unexpected reader state.".into(), } } pub fn program(cache: &mut Cache, input: &str) -> Result { let input = input.trim(); if input.is_empty() { return Err("Enter a URL or some text first.".into()); } let record = Record::auto(input); let tlv = ndef::tlv(&ndef::encode(&record)); let (_, card) = open().map_err(status_message)?; let card = card.ok_or("No tag on the reader. Place a tag on the reader and press Program again.")?; let uid = hex(&apdu(&card, &GET_UID)?); let atr = card .get_attribute_owned(Attribute::AtrString) .unwrap_or_default(); if let Kind::Unsupported(name) = kind(&atr) { return Err(format!( "{name} is not supported. Use an NTAG21x / MIFARE Ultralight tag." )); } let cc = read_cc(&card)?; if !cc.formatted { return Err("Tag is not NDEF formatted (no capability container).".into()); } if !cc.writable { return Err("Tag is locked (read-only).".into()); } if tlv.len() > cc.capacity { return Err(format!( "Too long: needs {} bytes, this tag holds {}.", tlv.len(), cc.capacity )); } let mut data = tlv; data.resize(data.len().div_ceil(4) * 4, 0); // Empty the NDEF TLV first and write its header last, so an interrupted // write leaves an empty tag instead of a corrupt message. write_page(&card, 4, &[0x03, 0x00, 0xFE, 0x00])?; for (i, chunk) in data.chunks(4).enumerate().skip(1) { write_page(&card, 4 + i, chunk)?; } write_page(&card, 4, &data[..4])?; let back = read_data(&card, data.len())?; if back != data { return Err("Verification failed: the tag content does not match what was written.".into()); } cache.tag = None; // re-read on the next status poll Ok(Programmed { uid, record: record.view(), bytes: data.len(), capacity: cc.capacity, }) }