gatekeeper_core/
lib.rs

1use nfc_sys::{
2  nfc_close, nfc_exit, nfc_init, nfc_initiator_init, nfc_open, nfc_perror,
3};
4use openssl::error::ErrorStack;
5use openssl::pkey::{PKey, Private, Public};
6use std::ffi::{c_char, CString};
7use std::fmt::{self, Display, Formatter};
8use std::marker::PhantomData;
9use std::mem::MaybeUninit;
10use std::time::Duration;
11
12mod desfire;
13mod mobile;
14use crate::desfire::*;
15use crate::mobile::*;
16
17impl Display for NfcError {
18  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
19    match self {
20      Self::NonceMismatch => write!(f, "None mismatch"),
21      Self::NoResponse => {
22        write!(f, "Did not receive a response. Is the tag too far away?")
23      }
24      Self::CryptoError(err) => write!(f, "Cryptography error: {err}"),
25      Self::ConnectFailed => write!(f, "Failed to connect"),
26      Self::CommunicationError => {
27        write!(f, "Communication failed. Is the tag too far away?")
28      }
29      Self::InvalidSignature => {
30        write!(f, "Association had an invalid signature!")
31      }
32      Self::BadAssociation => write!(f, "Association ID is not valid UTF-8"),
33    }
34  }
35}
36
37impl std::error::Error for NfcError {}
38
39#[derive(Debug)]
40pub enum NfcError {
41  // SendMismatch,
42  NonceMismatch,
43  NoResponse,
44  CryptoError(ErrorStack),
45  ConnectFailed,
46  CommunicationError,
47  InvalidSignature,
48  BadAssociation,
49}
50
51#[derive(Debug)]
52pub(crate) struct DeviceWrapper<'device> {
53  _context: NfcContext<'device>,
54  device: *mut nfc_sys::nfc_device,
55}
56
57impl<'device> Drop for DeviceWrapper<'device> {
58  fn drop(&mut self) {
59    unsafe { nfc_close(self.device) };
60  }
61}
62
63/// Wrapper around [`nfc_sys::nfc_context`]. The underlying context will be
64/// freed when dropped.
65#[derive(Debug)]
66struct NfcContext<'context> {
67  context: *mut nfc_sys::nfc_context,
68  _lifetime: PhantomData<&'context ()>,
69}
70
71impl Drop for NfcContext<'_> {
72  fn drop(&mut self) {
73    unsafe { nfc_exit(self.context) };
74  }
75}
76
77impl<'a> Default for NfcContext<'a> {
78  fn default() -> Self {
79    let mut context_uninit = MaybeUninit::<*mut nfc_sys::nfc_context>::uninit();
80    Self {
81      context: unsafe {
82        nfc_init(context_uninit.as_mut_ptr());
83        if context_uninit.as_mut_ptr().is_null() {
84          panic!("Malloc failed");
85        }
86        context_uninit.assume_init()
87      },
88      _lifetime: PhantomData,
89    }
90  }
91}
92
93/// Keys used to write to a particular realm.
94/// Most applications will not need these
95#[derive(Debug, Clone)]
96pub struct RealmWriteKeys<'a> {
97  /// KDF secret to get the desfire update key (used to make changes to records
98  /// stored on the tag)
99  update_key: &'a [u8],
100  /// Key used to sign association IDs on desfire tags. Only needed for issuing
101  /// new tags
102  desfire_signing_private_key: PKey<Private>,
103}
104
105/// A realm is a slot on a card that has unique keys.
106/// Typical realms are: `Doors`, `Drink`, `Member Projects`.
107#[derive(Debug, Clone)]
108pub struct Realm<'a> {
109  /// Which realm do these keys belong to?
110  slot: RealmType,
111  /// KDF secret to get the desfire auth key (used to access the card)
112  auth_key: Vec<u8>,
113  /// KDF secret to get the desfire read key (used to read files on the card)
114  read_key: Vec<u8>,
115  /// Public key that desfire association IDs are signed by
116  desfire_signing_public_key: PKey<Public>,
117  /// Private key that can decrypt messages from
118  /// [Flask](https://github.com/ComputerScienceHouse/devin)
119  mobile_decryption_private_key: PKey<Private>,
120  /// Private key used to prove to mobile tags that we're a real reader
121  mobile_signing_private_key: PKey<Private>,
122  /// Keys used to write to this realm. Only needed to issue new tags.
123  secrets: Option<RealmWriteKeys<'a>>,
124}
125
126impl<'a> Realm<'a> {
127  /// Creates a new realm with the given parameters.
128  /// * `slot` identifies which realm to access
129  /// * `auth_key` is the secret to derive the desfire auth key (used to gain
130  ///   access to the card)
131  /// * `read_key` is the secret to derive the desfire read key (used to read
132  ///   files stored on the card)
133  /// * `desfire_signing_public_key` is the public key that desfire association
134  ///   IDs are signed by
135  /// * `mobile_decryption_private_key` is the private key that can decrypt
136  ///   messages sent by mobile tags.
137  /// * `mobile_signing_private_key` is the private key used to prove to mobile
138  ///   tags that we're authorized to read from this realm.
139  /// * `secrets` optionally contains the keys needed to write to this realm.
140  ///   These are only needed to issue new tags, you probably don't need them.
141  pub fn new(
142    slot: RealmType,
143    auth_key: Vec<u8>,
144    read_key: Vec<u8>,
145    desfire_signing_public_key: &[u8],
146    mobile_decryption_private_key: &[u8],
147    mobile_signing_private_key: &[u8],
148    secrets: Option<RealmWriteKeys<'a>>,
149  ) -> Self {
150    Self {
151      slot,
152      auth_key,
153      read_key,
154      desfire_signing_public_key: PKey::public_key_from_pem(
155        desfire_signing_public_key,
156      )
157      .expect("Bad key format"),
158      mobile_decryption_private_key: PKey::private_key_from_pem(
159        mobile_decryption_private_key,
160      )
161      .expect("Bad key format"),
162      mobile_signing_private_key: PKey::private_key_from_pem(
163        mobile_signing_private_key,
164      )
165      .expect("Bad key format"),
166      secrets,
167    }
168  }
169}
170
171/// Selects a known realm.
172#[repr(u8)]
173#[derive(Debug, Clone)]
174pub enum RealmType {
175  /// The `Door` (slot 0) realm is used by door locks to gate access to common rooms.
176  Door = 0,
177  /// The `Drink` (slot 1) realm is used to authorize drink credit purchases.
178  Drink = 1,
179  /// The `MemberProjects` (slot 2) realm is used by anything lower-security
180  /// that doesn't fall into any of those categories (e.g. `harold-nfc`).
181  /// If you're looking to make a new project, this is probably the realm you
182  /// should use!
183  MemberProjects = 2,
184}
185
186impl Display for UndifferentiatedTag<'_> {
187  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
188    match self {
189      Self::Desfire(_) => write!(f, "Desfire"),
190      Self::Mobile(_) => write!(f, "Mobile"),
191    }
192  }
193}
194
195impl Display for RealmType {
196  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
197    match self {
198      Self::Door => write!(f, "Door"),
199      Self::Drink => write!(f, "Drink"),
200      Self::MemberProjects => write!(f, "Member Projects"),
201    }
202  }
203}
204
205// It's a u8, it's fine...
206impl Copy for RealmType {}
207
208/// High-level interface over an NFC reader. Allows reading association IDs from
209/// the realm for which it was created.
210pub struct GatekeeperReader<'device> {
211  /// Internal device we're reading from
212  device_wrapper: Option<DeviceWrapper<'device>>,
213  /// Connection string for the device
214  connection_string: String,
215  /// Realm to read from
216  realm: Realm<'device>,
217}
218
219impl<'device> DeviceWrapper<'device> {
220  /// Creates a new [`DeviceWrapper`] for the device found at `connection_string`.
221  /// For more information on the format of `connection_string`, see
222  /// [libnfc](https://github.com/nfc-tools/libnfc).
223  fn new(connection_string: String) -> Option<Self> {
224    let context = NfcContext::default();
225    let device_string = CString::new(connection_string).unwrap();
226    let device_string = device_string.into_bytes_with_nul();
227
228    let mut device_string_bytes: [c_char; 1024] = [0; 1024];
229    for (index, character) in device_string.into_iter().enumerate() {
230      device_string_bytes[index] = character as c_char;
231    }
232
233    let device_ptr = unsafe {
234      let device_ptr = nfc_open(context.context, &device_string_bytes);
235      if device_ptr.is_null() {
236        log::error!("Failed to open NFC device...");
237        return None;
238      }
239      log::debug!("Opened an NFC device!");
240      device_ptr
241    };
242    Some(Self {
243      device: device_ptr,
244      _context: context,
245    })
246  }
247}
248
249/// Wrapper over the supported NFC device types. You probably want to look at
250/// this enum's [`NfcTag`] implementation.
251#[derive(Debug)]
252pub enum UndifferentiatedTag<'a> {
253  Desfire(DesfireNfcTag<'a>),
254  Mobile(MobileNfcTag<'a>),
255}
256
257/// Generic NFC tag. Could represent a [`DesfireNfctag`] or a [`MobileNfcTag`]
258pub trait NfcTag {
259  /// Attempt to authenticate this tag. If the tag is valid, returns the
260  /// association ID. If there was an error, an [`NfcError`] is returned
261  /// instead.
262  fn authenticate(&self) -> Result<String, NfcError>;
263}
264
265impl NfcTag for UndifferentiatedTag<'_> {
266  fn authenticate(&self) -> Result<String, NfcError> {
267    match self {
268      Self::Desfire(desfire) => desfire.authenticate(),
269      Self::Mobile(mobile) => mobile.authenticate(),
270    }
271  }
272}
273
274/// Current state of the NFC reader
275#[must_use]
276enum ReaderStatus {
277  /// NFC reader is responding to our requests
278  Available,
279  /// NFC reader is not responding to our requests
280  Unavailable,
281}
282
283impl<'device> GatekeeperReader<'device> {
284  /// Opens a connection with the NFC reader located at `connection_string`.
285  /// Readers are tied to a particular [`Realm`], and can only read
286  /// association IDs from that particular realm.
287  pub fn new(connection_string: String, realm: Realm<'device>) -> Option<Self> {
288    let device_wrapper = Some(DeviceWrapper::new(connection_string.clone())?);
289    Some(Self {
290      device_wrapper,
291      connection_string,
292      realm,
293    })
294  }
295
296  /// Attempt to bring up the NFC reader's field, retrying up to 3 times.
297  /// Returns a [`ReaderStatus`] indicating whether or not the reader is ready
298  /// for use
299  fn ensure_reader_available(&mut self) -> ReaderStatus {
300    for _ in 0..3 {
301      if self.device_wrapper.as_ref().map(|device_wrapper| unsafe {
302        nfc_initiator_init(device_wrapper.device)
303        } < 0).unwrap_or(true)
304      {
305        log::error!("Couldn't init NFC initiator!!!");
306        if let Some(device_wrapper) = &self.device_wrapper {
307          unsafe {
308            let msg = CString::new("Failed to init device initiator :(").unwrap();
309            nfc_perror(device_wrapper.device, msg.as_ptr())
310          };
311        }
312        log::error!("Resetting the device and trying again!");
313        std::thread::sleep(Duration::from_millis(500));
314        self.device_wrapper = None;
315        if let Some(device_wrapper) =
316          DeviceWrapper::new(self.connection_string.clone())
317        {
318          self.device_wrapper = Some(device_wrapper);
319        }
320      } else {
321        // Init success
322        return ReaderStatus::Available;
323      }
324    }
325    ReaderStatus::Unavailable
326  }
327
328  /// Searches for nearby NFC tags.
329  ///
330  /// **Note:** This doesn't actually authenticate the NFC
331  /// tags, just searches for them. You **must** call [`NfcTag::authenticate`]
332  /// before you can know who it belongs to (or if it's even a valid tag!)</div>
333  pub fn get_nearby_tags(&mut self) -> Vec<UndifferentiatedTag> {
334    // Before anything else, make sure the reader is available
335    if let ReaderStatus::Unavailable = self.ensure_reader_available() {
336      return vec![];
337    }
338
339    // First, mobile tags:
340    if let Some(tag) = self.find_first_mobile_tag() {
341      return vec![UndifferentiatedTag::Mobile(tag)];
342    }
343
344    // Then, Desfire stuff:
345    self.find_desfire_tags()
346  }
347}