libgatekeeper_sys/
lib.rs

1#![deny(warnings)]
2
3use std::ffi::{CStr, CString};
4use std::marker::PhantomData;
5use std::mem::MaybeUninit;
6use std::ptr;
7
8use apdu_core::{Command, Response};
9use openssl::{encrypt::Decrypter, hash::MessageDigest, pkey::PKey, sign::Signer};
10use rand::Rng;
11use std::error::Error;
12use std::fmt::{self, Display, Formatter, Write};
13
14pub mod ffi;
15use crate::ffi::{BaudRate, Modulation, ModulationType, NfcProperty};
16
17pub struct Nfc {
18    context: *mut ffi::context_t,
19}
20
21#[derive(Debug)]
22pub enum NfcError {
23    Unknown,
24    // SendMismatch,
25    NonceMismatch,
26    NoResponse,
27    // CryptoError,
28}
29
30impl Display for NfcError {
31    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
32        use NfcError::*;
33        match self {
34            Unknown => write!(f, "Unknown error"),
35            NonceMismatch => write!(f, "Nonce for mobile tag didn't match"),
36            NoResponse => write!(f, "Didn't get a response from the mobile tag"),
37        }
38    }
39}
40
41impl Error for NfcError {}
42
43impl Nfc {
44    pub fn new() -> Option<Self> {
45        let mut context_uninit = MaybeUninit::<*mut ffi::context_t>::uninit();
46        let context = unsafe {
47            ffi::nfc_init(context_uninit.as_mut_ptr());
48            if context_uninit.as_mut_ptr().is_null() {
49                return None;
50            }
51            context_uninit.assume_init()
52        };
53
54        Some(Nfc { context })
55    }
56
57    pub fn gatekeeper_device(&mut self, conn_str: String) -> Option<NfcDevice> {
58        self.inner_device(conn_str.clone())
59            .map(|device| NfcDevice { device, conn_str })
60    }
61    fn inner_device(&self, conn_str: String) -> Option<NfcDeviceInner> {
62        let device_string = CString::new(conn_str).unwrap();
63        let device = unsafe {
64            let device_ptr = ffi::nfc_open(self.context, device_string.as_ptr());
65            if device_ptr.is_null() {
66                return None;
67            }
68            device_ptr
69        };
70        Some(NfcDeviceInner {
71            device,
72            _context: self,
73        })
74    }
75}
76
77impl Drop for Nfc {
78    fn drop(&mut self) {
79        unsafe {
80            ffi::nfc_exit(self.context);
81        }
82    }
83}
84
85struct NfcDeviceInner<'a> {
86    device: *mut ffi::device_t,
87    _context: &'a Nfc,
88}
89
90pub struct NfcDevice<'a> {
91    device: NfcDeviceInner<'a>,
92    conn_str: String,
93}
94
95const NONCE_LENGTH: usize = 8;
96const RESPONSE_PADDING_LENGTH: usize = 2;
97
98pub struct MobileNfcTag {
99    nonce: [u8; NONCE_LENGTH],
100    _target_guard: NfcTargetGuard,
101}
102
103struct NfcTargetGuard {
104    device: *mut ffi::device_t,
105}
106
107impl Drop for NfcTargetGuard {
108    fn drop(&mut self) {
109        unsafe {
110            let ecode = ffi::nfc_initiator_deselect_target(self.device);
111            if ecode != 0 {
112                eprintln!("Couldn't deslect target!! {}", ecode);
113                let msg = CString::new("Deselect target :(").unwrap();
114                ffi::nfc_perror(self.device, msg.as_ptr());
115            }
116        }
117    }
118}
119
120impl<'b> NfcDevice<'b> {
121    pub fn authenticate_tag(&mut self, realm: &mut Realm) -> Result<Option<String>, NfcError> {
122        if let Some(mut tag) = self.first_mobile_tag(realm) {
123            tag.authenticate(self, realm).map(Some)
124        } else if let Some(mut tag) = self.first_tag() {
125            tag.authenticate(self, realm).map(Some)
126        } else {
127            Ok(None)
128        }
129    }
130    pub fn first_tag(&self) -> Option<FreefareNfcTag> {
131        let (tags, tag) = unsafe {
132            let tags = ffi::freefare_get_tags(self.device.device);
133            if tags.is_null() {
134                return None;
135            }
136
137            let tag = *tags;
138            if tag.is_null() {
139                return None;
140            }
141            (tags, tag)
142        };
143
144        Some(FreefareNfcTag {
145            tags,
146            tag,
147            _device_lifetime: PhantomData,
148        })
149    }
150    pub fn first_mobile_tag(&mut self, realm: &Realm) -> Option<MobileNfcTag> {
151        loop {
152            if unsafe { ffi::nfc_initiator_init(self.device.device) } < 0 {
153                eprintln!("Couldn't init NFC initiator!!!");
154                // let error_code = unsafe { ffi::nfc_device_get_last_error(self.device.device) };
155                unsafe {
156                    let msg = CString::new("Failed to init device initiator :(").unwrap();
157                    ffi::nfc_perror(self.device.device, msg.as_ptr())
158                };
159                eprintln!("Resetting the device and trying again!");
160                self.device = self.device._context.inner_device(self.conn_str.clone())?;
161                continue;
162            }
163            break;
164        }
165
166        unsafe {
167            ffi::nfc_device_set_property_bool(self.device.device, NfcProperty::NP_ACTIVATE_FIELD, 1)
168        };
169        unsafe {
170            ffi::nfc_device_set_property_bool(
171                self.device.device,
172                NfcProperty::NP_INFINITE_SELECT,
173                0,
174            )
175        };
176
177        unsafe {
178            let mut nt = MaybeUninit::uninit();
179            if ffi::nfc_initiator_select_passive_target(
180                self.device.device,
181                Modulation {
182                    nmt: ModulationType::NMT_ISO14443A,
183                    nbr: BaudRate::NBR_106,
184                },
185                std::ptr::null(),
186                0,
187                nt.as_mut_ptr(),
188            ) <= 0
189            {
190                // println!("No tag found");
191                return None;
192            }
193        }
194        let guard = NfcTargetGuard {
195            device: self.device.device,
196        };
197
198        let response = self
199            .send(Command::new_with_payload_le(
200                0x00,
201                0xA4,
202                0x04,
203                0x00,
204                (NONCE_LENGTH + RESPONSE_PADDING_LENGTH) as u16,
205                vec![0xf0, 0x63, 0x73, 0x68, 0x72, 0x69, 0x74 + realm.slot],
206            ))
207            .ok()?;
208        if let Some(response) = response {
209            if response.payload.len() != NONCE_LENGTH {
210                return None;
211            }
212            let nonce = &response.payload[0..NONCE_LENGTH];
213            let mut nonce_arr: [u8; NONCE_LENGTH] = Default::default();
214            nonce_arr.copy_from_slice(nonce);
215            Some(MobileNfcTag {
216                nonce: nonce_arr,
217                _target_guard: guard,
218            })
219        } else {
220            None
221        }
222    }
223    pub fn send(&self, command: Command) -> Result<Option<Response>, NfcError> {
224        let mut response = vec![0u8; command.le.unwrap_or(0).into()];
225        let command: Vec<u8> = command.into();
226        let response_size = unsafe {
227            ffi::nfc_initiator_transceive_bytes(
228                self.device.device,
229                command.as_ptr(),
230                command.len(),
231                response.as_mut_ptr(),
232                response.len(),
233                2000,
234            )
235        };
236        if response_size < 0 {
237            return Err(NfcError::Unknown);
238        }
239        if response_size == 0 {
240            return Ok(None);
241        }
242        // convert response to a vec, from 0..response_size:
243        response.truncate(response_size as usize);
244
245        Ok(Some(Response::from(response)))
246    }
247}
248
249fn sign_message(realm: &Realm, message: &[u8]) -> Result<Vec<u8>, NfcError> {
250    // Sign message using the PKCS#8 encoded EC key realm.private_key using SHA256
251    let pkey = PKey::private_key_from_pem(
252        CString::new(realm.signing_private_key.clone())
253            .unwrap()
254            .as_bytes(),
255    )
256    .unwrap();
257    let mut signer = Signer::new(MessageDigest::sha384(), &pkey).unwrap();
258    signer.update(message).unwrap();
259    Ok(signer.sign_to_vec().unwrap())
260}
261
262fn decrypt_message(realm: &Realm, message: &[u8]) -> Result<Vec<u8>, NfcError> {
263    let pkey = PKey::private_key_from_pem(
264        CString::new(realm.asymmetric_private_key.clone())
265            .unwrap()
266            .as_bytes(),
267    )
268    .unwrap();
269    let decrypter = Decrypter::new(pkey.as_ref()).unwrap();
270    let message_len = decrypter.decrypt_len(message).unwrap();
271    let mut output = vec![0; message_len];
272    let length = decrypter.decrypt(message, output.as_mut_slice()).unwrap();
273    Ok(output[..length].into())
274}
275
276impl NfcTag for MobileNfcTag {
277    fn authenticate(
278        &mut self,
279        nfc_device: &NfcDevice<'_>,
280        realm: &mut Realm,
281    ) -> Result<String, NfcError> {
282        println!("Authenticating! Sending signed nonce");
283        println!("{} is the realm id", realm.slot);
284        // Concatenate self.nonce and our_nonce
285        let our_nonce = rand::thread_rng().gen::<[u8; NONCE_LENGTH]>();
286        let mut signature_data = [0u8; NONCE_LENGTH * 2];
287        signature_data[0..NONCE_LENGTH].copy_from_slice(&self.nonce);
288        signature_data[NONCE_LENGTH..].copy_from_slice(&our_nonce);
289        let mut signature = sign_message(realm, &signature_data)?;
290        // Add our_nonce to signature
291        signature.extend_from_slice(&our_nonce);
292
293        let encrypted_association = nfc_device
294            .send(Command::new_with_payload_le(
295                0xD0, 0x00, 0x00, 0x00, // Encrypted value length is non-determinate
296                512, signature,
297            ))?
298            .ok_or(NfcError::NoResponse)?;
299        let payload = encrypted_association.payload.as_slice();
300        let payload = decrypt_message(realm, payload)?;
301        // take last 8 bytes of association as the nonce
302        let nonce = &payload[payload.len() - self.nonce.len()..payload.len()];
303        // take the rest as the association
304        let association_id = &payload[0..payload.len() - self.nonce.len()];
305
306        if our_nonce != nonce {
307            return Err(NfcError::NonceMismatch);
308        }
309
310        Ok(association_id
311            .iter()
312            .fold(String::new(), |mut collector, id| {
313                write!(collector, "{:02x}", id).unwrap();
314                collector
315            }))
316    }
317}
318
319impl Drop for NfcDeviceInner<'_> {
320    fn drop(&mut self) {
321        unsafe {
322            ffi::nfc_close(self.device);
323        }
324    }
325}
326
327pub struct FreefareNfcTag<'a> {
328    tags: *mut *mut ffi::mifare_t,
329    tag: *mut ffi::mifare_t,
330    _device_lifetime: std::marker::PhantomData<&'a ()>,
331}
332
333pub trait NfcTag {
334    fn authenticate(
335        &mut self,
336        nfc_device: &NfcDevice<'_>,
337        realm: &mut Realm,
338    ) -> Result<String, NfcError>;
339}
340
341impl NfcTag for FreefareNfcTag<'_> {
342    // TODO: None of this is super ideal...
343    fn authenticate(
344        &mut self,
345        _nfc_device: &NfcDevice<'_>,
346        realm: &mut Realm,
347    ) -> Result<String, NfcError> {
348        let mut association_id = [0u8; 37];
349        let auth_result =
350            unsafe { ffi::authenticate_tag(self.tag, realm.realm, association_id.as_mut_ptr()) };
351        if auth_result == 0 {
352            return Err(NfcError::Unknown);
353        }
354
355        let mut association_id = association_id.to_vec();
356        // Pop off NUL byte
357        association_id.pop();
358
359        Ok(String::from_utf8(association_id).unwrap())
360    }
361}
362
363impl FreefareNfcTag<'_> {
364    pub fn get_uid(&mut self) -> Option<String> {
365        unsafe {
366            let tag_uid = ffi::freefare_get_tag_uid(self.tag);
367            if tag_uid.is_null() {
368                return None;
369            }
370            let tag_uid_string = CString::from_raw(tag_uid);
371            Some(tag_uid_string.to_string_lossy().to_string())
372        }
373    }
374
375    pub fn get_friendly_name(&mut self) -> Option<&str> {
376        unsafe {
377            let tag_name = ffi::freefare_get_tag_friendly_name(self.tag);
378            let tag_name_string = CStr::from_ptr(tag_name);
379            tag_name_string.to_str().ok()
380        }
381    }
382
383    pub fn format(
384        &mut self,
385        uid: Option<&str>,
386        system_secret: Option<&str>,
387    ) -> Result<(), NfcError> {
388        let uid_opt = match uid {
389            Some(uid) => CString::new(uid).ok(),
390            None => None,
391        };
392        let uid = match &uid_opt {
393            Some(uid) => uid.as_ptr(),
394            None => ptr::null(),
395        };
396        let system_secret_opt = match system_secret {
397            Some(system_secret) => CString::new(system_secret).ok(),
398            None => None,
399        };
400        let system_secret = match &system_secret_opt {
401            Some(system_secret) => system_secret.as_ptr(),
402            None => ptr::null(),
403        };
404
405        unsafe {
406            let format_result = ffi::format_tag(self.tag, uid, system_secret);
407            if format_result != 0 {
408                return Err(NfcError::Unknown);
409            }
410            Ok(())
411        }
412    }
413
414    pub fn issue(
415        &mut self,
416        system_secret: &str,
417        uid: Option<&str>,
418        in_realms: Vec<&mut Realm>,
419    ) -> Result<(), NfcError> {
420        let system_secret = CString::new(system_secret).unwrap();
421        let uid_opt = match uid {
422            Some(uid) => CString::new(uid).ok(),
423            None => None,
424        };
425        let uid = match &uid_opt {
426            Some(uid) => uid.as_ptr(),
427            None => ptr::null(),
428        };
429
430        let mut realms: Vec<*mut ffi::realm_t> = Vec::with_capacity(in_realms.len());
431        for realm in in_realms {
432            realms.push(realm.realm);
433        }
434        unsafe {
435            let issue_result = ffi::issue_tag(
436                self.tag,
437                system_secret.as_ptr(),
438                uid,
439                realms.as_mut_ptr(),
440                realms.len(),
441            );
442            if issue_result != 0 {
443                return Err(NfcError::Unknown);
444            }
445            Ok(())
446        }
447    }
448}
449
450impl Drop for FreefareNfcTag<'_> {
451    fn drop(&mut self) {
452        unsafe {
453            ffi::freefare_free_tags(self.tags);
454        }
455    }
456}
457
458pub struct Realm {
459    realm: *mut ffi::realm_t,
460    slot: u8,
461    signing_private_key: String,
462    asymmetric_private_key: String,
463}
464
465// A realm is a global thing, it's not tied to a card.
466// Keys here are secrets for that particular project (e.g. drink, gatekeeper)
467// Most likely, the only thing you want to change here is 'association' for each card
468impl Realm {
469    #[allow(clippy::too_many_arguments)]
470    pub fn new(
471        slot: u8,
472        name: &str,
473        association: &str,
474        auth_key: &str,
475        read_key: &str,
476        update_key: &str,
477        public_key: &str,
478        private_key: &str,
479        mobile_private_key: &str,
480        asymmetric_private_key: &str,
481    ) -> Option<Realm> {
482        let ffi_name = CString::new(name).ok()?;
483        let ffi_association = CString::new(association).ok()?;
484        let ffi_auth_key = CString::new(auth_key).ok()?;
485        let ffi_read_key = CString::new(read_key).ok()?;
486        let ffi_update_key = CString::new(update_key).ok()?;
487        let ffi_public_key = CString::new(public_key).ok()?;
488        let ffi_private_key = CString::new(private_key).ok()?;
489
490        let realm = unsafe {
491            ffi::realm_create(
492                slot,
493                ffi_name.as_ptr(),
494                ffi_association.as_ptr(),
495                ffi_auth_key.as_ptr(),
496                ffi_read_key.as_ptr(),
497                ffi_update_key.as_ptr(),
498                ffi_public_key.as_ptr(),
499                ffi_private_key.as_ptr(),
500            )
501        };
502
503        Some(Realm {
504            realm,
505            slot,
506            signing_private_key: mobile_private_key.to_string(),
507            asymmetric_private_key: asymmetric_private_key.to_string(),
508        })
509    }
510}
511
512impl Drop for Realm {
513    fn drop(&mut self) {
514        unsafe {
515            ffi::realm_free(self.realm);
516        }
517    }
518}