339 lines
10 KiB
Rust
339 lines
10 KiB
Rust
//! 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<TagInfo>,
|
|
}
|
|
|
|
#[derive(Clone, Serialize)]
|
|
pub struct TagInfo {
|
|
pub uid: String,
|
|
pub kind: String,
|
|
pub formatted: bool,
|
|
pub capacity: Option<usize>,
|
|
pub used: Option<usize>,
|
|
pub writable: bool,
|
|
pub records: Vec<RecordView>,
|
|
/// Why the tag cannot be programmed, or a read error.
|
|
pub note: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
#[serde(tag = "state", rename_all = "snake_case")]
|
|
pub enum Status {
|
|
NoReader { message: String },
|
|
NoCard { reader: String },
|
|
Card { reader: String, tag: Box<TagInfo> },
|
|
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<Card>), 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<Vec<u8>, 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::<Vec<_>>()
|
|
.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<Cc, String> {
|
|
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<Vec<u8>, 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<Programmed, String> {
|
|
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,
|
|
})
|
|
}
|