hackgt_nfc/nfc/
badge.rs

1use std::fmt;
2use std::borrow::Cow;
3use url::Url;
4use super::ndef::NDEF;
5
6#[derive(Debug)]
7pub struct CardResponse {
8	pub status: [u8; 2],
9	pub data: Vec<u8>,
10}
11
12/// Encapsulates PCSC errors and card response errors into a single error type
13pub enum Error {
14	PCSC(pcsc::Error),
15	Response([u8; 2]),
16	Message(&'static str),
17}
18impl fmt::Debug for Error {
19	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
20		match *self {
21			Error::PCSC(pcsc_error) => write!(f, "{:?}", pcsc_error),
22			Error::Response(bytes) => write!(f, "{:x?}", bytes),
23			Error::Message(s) => write!(f, "{}", s),
24		}
25	}
26}
27impl From<pcsc::Error> for Error {
28	fn from(err: pcsc::Error) -> Error {
29		Error::PCSC(err)
30	}
31}
32impl From<[u8; 2]> for Error {
33	fn from(err: [u8; 2]) -> Error {
34		Error::Response(err)
35	}
36}
37impl From<&'static str> for Error {
38	fn from(err: &'static str) -> Error {
39		Error::Message(err)
40	}
41}
42
43pub struct NFCBadge<'a> {
44	card: &'a pcsc::Card,
45}
46
47impl NFCBadge<'_> {
48	pub fn new(card: &pcsc::Card) -> NFCBadge {
49		NFCBadge {
50			card,
51		}
52	}
53
54	pub fn get_user_id(&self) -> Result<String, Error> {
55		/*
56		Finally figured some cool stuff out:
57
58		According to the datasheet (https://www.nxp.com/docs/en/data-sheet/NTAG213_215_216.pdf) for the NTAG213 series,
59		the cards support a FAST_READ command that isn't part of the ISO/IEC 14443 standard. This command lets you
60		read all of the memory pages on the card with one transaction. The standardized READ command (listed as
61		Read Binary Blocks command in the ACR122U USB reader datasheet http://downloads.acs.com.hk/drivers/en/API-ACR122U-2.02.pdf)
62		only allows reading up to 16 bytes at once (4 pages of 4 bytes each) which means reading the entire memory
63		space of the card is extremely slow and error-prone (what happens if the tag is removed before all of the read
64		operations have been executed?)
65
66		These NXP-specific commands like FAST_READ don't use the typical APDU interface. Instead, we send:
67		- A pseudo-APDU to the USB reader (0xFF, 0x00, 0x00, 0x00)
68		- A length field telling the reader we're sending 5 raw bytes
69		- The InCommunicateThru command (0xD4, 0x42) -- read by the card's PN532 NFC communcation controller
70			See: https://www.nxp.com/docs/en/user-guide/141520.pdf section 7.3.9
71		- The 0x3A FAST_READ command specified in the datasheet plus the start page and end page
72
73		Outputs (according to the PN532 datasheet) will be: 0xD5, 0x43, and a status bytes (where 0x00 indicates success)
74
75		This Stack Overflow answer has more related information:
76		https://stackoverflow.com/questions/44237726/how-to-authenticate-ntag213-with-acr122u/44243037#44243037
77		*/
78		const START_PAGE: u8 = 0x04; // 0x00 through 0x03 contain tag-related info. User data starts at 0x04
79		const END_PAGE: u8 = 0x27; // 0x27 is the last data page on the NTAG213
80		let apdu = [0xFF, 0x00, 0x00, 0x00, 0x05, 0xD4, 0x42, 0x3A, START_PAGE, END_PAGE];
81		let response = self.send_data(&apdu)?;
82
83		if &response.data[0..3] != [0xD5, 0x43, 0x00] {
84			return Err(Error::Message("Invalid PN532 response"));
85		}
86		let data = &response.data[3..];
87		let message = NDEF::parse(data)?;
88		let url = message.get_content().ok_or("NDEF message not URL")?;
89		let url = Url::parse(&url).ok().ok_or("Invalid URL")?;
90
91		for keyvalue in url.query_pairs() {
92			match keyvalue.0 {
93				Cow::Borrowed("user") => return Ok(keyvalue.1.to_string()),
94				_ => {},
95			}
96		}
97		Err(Error::Message("URL did not contain user ID"))
98	}
99
100	pub fn set_buzzer(&self, enabled: bool) -> Result<bool, Error> {
101		let value = if enabled { 0xFF } else { 0x00 };
102		let apdu = [0xFF, 0x00, 0x52, value, 0x00];
103		self.send_data(&apdu)?;
104		Ok(enabled)
105	}
106
107	pub(crate) fn send_data(&self, apdu: &[u8]) -> Result<CardResponse, Error> {
108		let mut rapdu_buf = [0u8; pcsc::MAX_BUFFER_SIZE];
109		let mut rapdu = self.card.transmit(apdu, &mut rapdu_buf)?.to_vec();
110
111		if rapdu.len() < 2 {
112			return Err(pcsc::Error::InvalidValue.into());
113		}
114
115		let status = [rapdu[rapdu.len() - 2], rapdu[rapdu.len() - 1]];
116		rapdu.truncate(rapdu.len() - 2);
117		// APDU response of 0x90, 0x00 means command executing successfully
118		if status[0] == 0x90 && status[1] == 0x00 {
119			Ok(CardResponse {
120				status,
121				data: rapdu,
122			})
123		}
124		else {
125			Err(status.into())
126		}
127	}
128}