1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
use nfc_sys::{
  nfc_close, nfc_exit, nfc_init, nfc_initiator_init, nfc_open, nfc_perror,
};
use openssl::error::ErrorStack;
use openssl::pkey::{PKey, Private, Public};
use std::ffi::{c_char, CString};
use std::fmt::{self, Display, Formatter};
use std::marker::PhantomData;
use std::mem::MaybeUninit;
use std::time::Duration;

mod desfire;
mod mobile;
use crate::desfire::*;
use crate::mobile::*;

impl Display for NfcError {
  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    match self {
      Self::NonceMismatch => write!(f, "None mismatch"),
      Self::NoResponse => {
        write!(f, "Did not receive a response. Is the tag too far away?")
      }
      Self::CryptoError(err) => write!(f, "Cryptography error: {err}"),
      Self::ConnectFailed => write!(f, "Failed to connect"),
      Self::CommunicationError => {
        write!(f, "Communication failed. Is the tag too far away?")
      }
      Self::InvalidSignature => {
        write!(f, "Association had an invalid signature!")
      }
      Self::BadAssociation => write!(f, "Association ID is not valid UTF-8"),
    }
  }
}

impl std::error::Error for NfcError {}

#[derive(Debug)]
pub enum NfcError {
  // SendMismatch,
  NonceMismatch,
  NoResponse,
  CryptoError(ErrorStack),
  ConnectFailed,
  CommunicationError,
  InvalidSignature,
  BadAssociation,
}

#[derive(Debug)]
pub(crate) struct DeviceWrapper<'device> {
  _context: NfcContext<'device>,
  device: *mut nfc_sys::nfc_device,
}

impl<'device> Drop for DeviceWrapper<'device> {
  fn drop(&mut self) {
    unsafe { nfc_close(self.device) };
  }
}

/// Wrapper around [`nfc_sys::nfc_context`]. The underlying context will be
/// freed when dropped.
#[derive(Debug)]
struct NfcContext<'context> {
  context: *mut nfc_sys::nfc_context,
  _lifetime: PhantomData<&'context ()>,
}

impl Drop for NfcContext<'_> {
  fn drop(&mut self) {
    unsafe { nfc_exit(self.context) };
  }
}

impl<'a> Default for NfcContext<'a> {
  fn default() -> Self {
    let mut context_uninit = MaybeUninit::<*mut nfc_sys::nfc_context>::uninit();
    Self {
      context: unsafe {
        nfc_init(context_uninit.as_mut_ptr());
        if context_uninit.as_mut_ptr().is_null() {
          panic!("Malloc failed");
        }
        context_uninit.assume_init()
      },
      _lifetime: PhantomData,
    }
  }
}

/// Keys used to write to a particular realm.
/// Most applications will not need these
#[derive(Debug, Clone)]
pub struct RealmWriteKeys<'a> {
  /// KDF secret to get the desfire update key (used to make changes to records
  /// stored on the tag)
  update_key: &'a [u8],
  /// Key used to sign association IDs on desfire tags. Only needed for issuing
  /// new tags
  desfire_signing_private_key: PKey<Private>,
}

/// A realm is a slot on a card that has unique keys.
/// Typical realms are: `Doors`, `Drink`, `Member Projects`.
#[derive(Debug, Clone)]
pub struct Realm<'a> {
  /// Which realm do these keys belong to?
  slot: RealmType,
  /// KDF secret to get the desfire auth key (used to access the card)
  auth_key: Vec<u8>,
  /// KDF secret to get the desfire read key (used to read files on the card)
  read_key: Vec<u8>,
  /// Public key that desfire association IDs are signed by
  desfire_signing_public_key: PKey<Public>,
  /// Private key that can decrypt messages from
  /// [Flask](https://github.com/ComputerScienceHouse/devin)
  mobile_decryption_private_key: PKey<Private>,
  /// Private key used to prove to mobile tags that we're a real reader
  mobile_signing_private_key: PKey<Private>,
  /// Keys used to write to this realm. Only needed to issue new tags.
  secrets: Option<RealmWriteKeys<'a>>,
}

impl<'a> Realm<'a> {
  /// Creates a new realm with the given parameters.
  /// * `slot` identifies which realm to access
  /// * `auth_key` is the secret to derive the desfire auth key (used to gain
  ///   access to the card)
  /// * `read_key` is the secret to derive the desfire read key (used to read
  ///   files stored on the card)
  /// * `desfire_signing_public_key` is the public key that desfire association
  ///   IDs are signed by
  /// * `mobile_decryption_private_key` is the private key that can decrypt
  ///   messages sent by mobile tags.
  /// * `mobile_signing_private_key` is the private key used to prove to mobile
  ///   tags that we're authorized to read from this realm.
  /// * `secrets` optionally contains the keys needed to write to this realm.
  ///   These are only needed to issue new tags, you probably don't need them.
  pub fn new(
    slot: RealmType,
    auth_key: Vec<u8>,
    read_key: Vec<u8>,
    desfire_signing_public_key: &[u8],
    mobile_decryption_private_key: &[u8],
    mobile_signing_private_key: &[u8],
    secrets: Option<RealmWriteKeys<'a>>,
  ) -> Self {
    Self {
      slot,
      auth_key,
      read_key,
      desfire_signing_public_key: PKey::public_key_from_pem(
        desfire_signing_public_key,
      )
      .expect("Bad key format"),
      mobile_decryption_private_key: PKey::private_key_from_pem(
        mobile_decryption_private_key,
      )
      .expect("Bad key format"),
      mobile_signing_private_key: PKey::private_key_from_pem(
        mobile_signing_private_key,
      )
      .expect("Bad key format"),
      secrets,
    }
  }
}

/// Selects a known realm.
#[repr(u8)]
#[derive(Debug, Clone)]
pub enum RealmType {
  /// The `Door` (slot 0) realm is used by door locks to gate access to common rooms.
  Door = 0,
  /// The `Drink` (slot 1) realm is used to authorize drink credit purchases.
  Drink = 1,
  /// The `MemberProjects` (slot 2) realm is used by anything lower-security
  /// that doesn't fall into any of those categories (e.g. `harold-nfc`).
  /// If you're looking to make a new project, this is probably the realm you
  /// should use!
  MemberProjects = 2,
}

impl Display for UndifferentiatedTag<'_> {
  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    match self {
      Self::Desfire(_) => write!(f, "Desfire"),
      Self::Mobile(_) => write!(f, "Mobile"),
    }
  }
}

impl Display for RealmType {
  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    match self {
      Self::Door => write!(f, "Door"),
      Self::Drink => write!(f, "Drink"),
      Self::MemberProjects => write!(f, "Member Projects"),
    }
  }
}

// It's a u8, it's fine...
impl Copy for RealmType {}

/// High-level interface over an NFC reader. Allows reading association IDs from
/// the realm for which it was created.
pub struct GatekeeperReader<'device> {
  /// Internal device we're reading from
  device_wrapper: Option<DeviceWrapper<'device>>,
  /// Connection string for the device
  connection_string: String,
  /// Realm to read from
  realm: Realm<'device>,
}

impl<'device> DeviceWrapper<'device> {
  /// Creates a new [`DeviceWrapper`] for the device found at `connection_string`.
  /// For more information on the format of `connection_string`, see
  /// [libnfc](https://github.com/nfc-tools/libnfc).
  fn new(connection_string: String) -> Option<Self> {
    let context = NfcContext::default();
    let device_string = CString::new(connection_string).unwrap();
    let device_string = device_string.into_bytes_with_nul();

    let mut device_string_bytes: [c_char; 1024] = [0; 1024];
    for (index, character) in device_string.into_iter().enumerate() {
      device_string_bytes[index] = character as c_char;
    }

    let device_ptr = unsafe {
      let device_ptr = nfc_open(context.context, &device_string_bytes);
      if device_ptr.is_null() {
        log::error!("Failed to open NFC device...");
        return None;
      }
      log::debug!("Opened an NFC device!");
      device_ptr
    };
    Some(Self {
      device: device_ptr,
      _context: context,
    })
  }
}

/// Wrapper over the supported NFC device types. You probably want to look at
/// this enum's [`NfcTag`] implementation.
#[derive(Debug)]
pub enum UndifferentiatedTag<'a> {
  Desfire(DesfireNfcTag<'a>),
  Mobile(MobileNfcTag<'a>),
}

/// Generic NFC tag. Could represent a [`DesfireNfctag`] or a [`MobileNfcTag`]
pub trait NfcTag {
  /// Attempt to authenticate this tag. If the tag is valid, returns the
  /// association ID. If there was an error, an [`NfcError`] is returned
  /// instead.
  fn authenticate(&self) -> Result<String, NfcError>;
}

impl NfcTag for UndifferentiatedTag<'_> {
  fn authenticate(&self) -> Result<String, NfcError> {
    match self {
      Self::Desfire(desfire) => desfire.authenticate(),
      Self::Mobile(mobile) => mobile.authenticate(),
    }
  }
}

/// Current state of the NFC reader
#[must_use]
enum ReaderStatus {
  /// NFC reader is responding to our requests
  Available,
  /// NFC reader is not responding to our requests
  Unavailable,
}

impl<'device> GatekeeperReader<'device> {
  /// Opens a connection with the NFC reader located at `connection_string`.
  /// Readers are tied to a particular [`Realm`], and can only read
  /// association IDs from that particular realm.
  pub fn new(connection_string: String, realm: Realm<'device>) -> Option<Self> {
    let device_wrapper = Some(DeviceWrapper::new(connection_string.clone())?);
    Some(Self {
      device_wrapper,
      connection_string,
      realm,
    })
  }

  /// Attempt to bring up the NFC reader's field, retrying up to 3 times.
  /// Returns a [`ReaderStatus`] indicating whether or not the reader is ready
  /// for use
  fn ensure_reader_available(&mut self) -> ReaderStatus {
    for _ in 0..3 {
      if self.device_wrapper.as_ref().map(|device_wrapper| unsafe {
        nfc_initiator_init(device_wrapper.device)
        } < 0).unwrap_or(true)
      {
        log::error!("Couldn't init NFC initiator!!!");
        if let Some(device_wrapper) = &self.device_wrapper {
          unsafe {
            let msg = CString::new("Failed to init device initiator :(").unwrap();
            nfc_perror(device_wrapper.device, msg.as_ptr())
          };
        }
        log::error!("Resetting the device and trying again!");
        std::thread::sleep(Duration::from_millis(500));
        self.device_wrapper = None;
        if let Some(device_wrapper) =
          DeviceWrapper::new(self.connection_string.clone())
        {
          self.device_wrapper = Some(device_wrapper);
        }
      } else {
        // Init success
        return ReaderStatus::Available;
      }
    }
    ReaderStatus::Unavailable
  }

  /// Searches for nearby NFC tags.
  ///
  /// **Note:** This doesn't actually authenticate the NFC
  /// tags, just searches for them. You **must** call [`NfcTag::authenticate`]
  /// before you can know who it belongs to (or if it's even a valid tag!)</div>
  pub fn get_nearby_tags(&mut self) -> Vec<UndifferentiatedTag> {
    // Before anything else, make sure the reader is available
    if let ReaderStatus::Unavailable = self.ensure_reader_available() {
      return vec![];
    }

    // First, mobile tags:
    if let Some(tag) = self.find_first_mobile_tag() {
      return vec![UndifferentiatedTag::Mobile(tag)];
    }

    // Then, Desfire stuff:
    self.find_desfire_tags()
  }
}