nfc_oath/
lib.rs

1#[macro_use]
2extern crate log;
3extern crate byteorder;
4extern crate nfc;
5
6use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
7use nfc::context;
8use nfc::device;
9use nfc::ffi;
10use nfc::initiator;
11use nfc::misc;
12use std::collections::HashMap;
13use std::fmt;
14use std::io::{Cursor, Read, Write};
15use std::mem;
16use std::ptr;
17use std::time::{Duration, Instant, SystemTime};
18
19#[cfg(test)]
20mod tests {
21    use std::time::{Duration, SystemTime};
22
23    #[test]
24    fn it_works() {}
25
26    #[test]
27    fn code_formatter_works() {
28        let code = ::OathCode {
29            digits: ::OathDigits::Six,
30            value: 595641143,
31            expiration: 0,
32            steam: false,
33        };
34        assert_eq!(format!("{}", code), "641143");
35    }
36
37    #[test]
38    fn parse_tlv_works() {
39        let resp = ::parse_tlv(&vec![0x76, 0x05, 0x06, 0x23, 0x80, 0xc3, 0x37, 0x90, 0x00]);
40        println!("{:?}", resp);
41    }
42
43    #[test]
44    fn parse_list_works() {
45        let result = ::parse_list(&vec![
46            0x72, 0x1A, 0x21, 0x44, 0x6F, 0x6F, 0x72, 0x79, 0x3A, 0x64, 0x6F, 0x6F, 0x72, 0x79,
47            0x40, 0x63, 0x75, 0x74, 0x65, 0x6C, 0x61, 0x62, 0x2E, 0x68, 0x6F, 0x75, 0x73, 0x65,
48            0x90, 0x00,
49        ]);
50        let answer = vec![::OathCredential::new(
51            "Doory:doory@cutelab.house",
52            ::OathType::Totp,
53            false,
54            ::OathAlgo::Sha1,
55        )];
56        assert_eq!(answer, result.unwrap());
57
58        let result = ::parse_list(&vec![
59            0x72, 0x16, 0x21, 0x44, 0x6F, 0x6D, 0x61, 0x69, 0x6E, 0x3A, 0x79, 0x6F, 0x75, 0x72,
60            0x40, 0x65, 0x6D, 0x61, 0x69, 0x6C, 0x2E, 0x63, 0x6F, 0x6D, 0x72, 0x17, 0x21, 0x44,
61            0x6F, 0x6F, 0x72, 0x79, 0x3A, 0x65, 0x76, 0x40, 0x63, 0x75, 0x74, 0x65, 0x6C, 0x61,
62            0x62, 0x2E, 0x68, 0x6F, 0x75, 0x73, 0x65, 0x90, 0x00,
63        ]);
64        let answer = vec![
65            ::OathCredential::new(
66                "Domain:your@email.com",
67                ::OathType::Totp,
68                false,
69                ::OathAlgo::Sha1,
70            ),
71            ::OathCredential::new(
72                "Doory:ev@cutelab.house",
73                ::OathType::Totp,
74                false,
75                ::OathAlgo::Sha1,
76            ),
77        ];
78        assert_eq!(answer, result.unwrap());
79    }
80
81    #[test]
82    fn time_challenge_works() {
83        let vectors: &[(Duration, Vec<u8>)] = &[
84            (
85                Duration::new(0, 0),
86                vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
87            ),
88            (
89                Duration::new(12345678, 0),
90                vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x47, 0x82],
91            ),
92            (
93                Duration::new(1484223461, 264495800),
94                vec![0x00, 0x00, 0x00, 0x00, 0x02, 0xf2, 0xea, 0x43],
95            ),
96        ];
97
98        for i in 0..vectors.len() {
99            let (dur, ref answer) = vectors[i];
100
101            let time = SystemTime::UNIX_EPOCH + dur;
102            let result = ::time_challenge(Some(time));
103
104            assert_eq!(*answer, result);
105        }
106    }
107
108}
109
110#[repr(u8)]
111pub enum Tag {
112    Name = 0x71,
113    NameList = 0x72,
114    Key = 0x73,
115    Challenge = 0x74,
116    Response = 0x75,
117    TruncatedResponse = 0x76,
118    Hotp = 0x77,
119    Property = 0x78,
120    Version = 0x79,
121    Imf = 0x7a,
122    Algorithm = 0x7b,
123    Touch = 0x7c,
124}
125
126#[derive(Debug, PartialEq)]
127#[repr(u8)]
128pub enum OathAlgo {
129    Sha1 = 0x01,
130    Sha256 = 0x02,
131}
132
133impl OathAlgo {
134    fn from_u8(n: u8) -> Option<OathAlgo> {
135        match n {
136            0x01 => Some(OathAlgo::Sha1),
137            0x02 => Some(OathAlgo::Sha256),
138            _ => None,
139        }
140    }
141}
142
143#[derive(Debug, PartialEq)]
144#[repr(u8)]
145pub enum OathType {
146    Hotp = 0x10,
147    Totp = 0x20,
148}
149
150impl OathType {
151    fn from_u8(n: u8) -> Option<OathType> {
152        match n {
153            0x10 => Some(OathType::Hotp),
154            0x20 => Some(OathType::Totp),
155            _ => None,
156        }
157    }
158}
159
160#[repr(u8)]
161pub enum Properties {
162    RequireTouch = 0x02,
163}
164
165#[repr(u8)]
166pub enum Ins {
167    Put = 0x01,
168    Delete = 0x02,
169    SetCode = 0x03,
170    Reset = 0x04,
171    List = 0xa1,
172    Calculate = 0xa2,
173    Validate = 0xa3,
174    CalculateAll = 0xa4,
175    SendRemaining = 0xa5,
176}
177
178#[repr(u8)]
179pub enum Mask {
180    Algo = 0x0f,
181    Type = 0xf0,
182}
183
184pub enum Sw {
185    NoSpace = 0x6a84,
186    CommandAborted = 0x6f00,
187    MoreData = 0x61,
188    InvalidInstruction = 0x6d00,
189}
190
191#[derive(Clone, Copy, Debug, PartialEq)]
192pub enum OathDigits {
193    Six = 6,
194    Eight = 8,
195}
196
197#[derive(Debug, PartialEq)]
198pub struct OathCode {
199    pub digits: OathDigits,
200    pub value: u32,
201    pub expiration: u32, // FIXME
202    pub steam: bool,
203}
204#[derive(Debug, PartialEq)]
205pub struct OathCredential {
206    pub name: String,
207    pub code: Result<OathCode, String>,
208    pub oath_type: OathType,
209    pub touch: bool,
210    pub algo: OathAlgo,
211    pub hidden: bool,
212    pub steam: bool,
213}
214pub struct OathController {
215    pub context: *mut ffi::nfc_context,
216    pub device: *mut ffi::nfc_device,
217}
218
219pub const INS_SELECT: u8 = 0xa4;
220pub const OATH_AID: [u8; 8] = [0xa0, 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01];
221
222pub fn tlv(tag: Tag, value: &[u8]) -> Vec<u8> {
223    let mut buf = vec![tag as u8];
224    let len = value.len();
225    if len < 0x80 {
226        buf.push(len as u8);
227    } else if len < 0xff {
228        buf.push(0x81);
229        buf.push(len as u8);
230    } else {
231        buf.push(0x82);
232        buf.write_u16::<BigEndian>(len as u16).unwrap();
233    }
234    buf.write(value).unwrap();
235    buf
236}
237
238pub fn parse_tlv(data: &[u8]) -> HashMap<u8, Vec<u8>> {
239    let mut rdr = Cursor::new(data);
240    let mut map = HashMap::new();
241    loop {
242        let tag = match rdr.read_u8() {
243            Ok(tag) => tag,
244            Err(_) => break,
245        };
246        let mut len: u16 = match rdr.read_u8() {
247            Ok(len) => len as u16,
248            Err(_) => break,
249        };
250        if len > 0x80 {
251            let n_bytes = len - 0x80;
252            if n_bytes == 1 {
253                len = match rdr.read_u8() {
254                    Ok(len) => len as u16,
255                    Err(_) => break,
256                };
257            } else if n_bytes == 2 {
258                len = match rdr.read_u16::<BigEndian>() {
259                    Ok(len) => len,
260                    Err(_) => break,
261                };
262            }
263        }
264        let mut dst = Vec::with_capacity(len as usize);
265        unsafe {
266            dst.set_len(len as usize);
267        }
268        match rdr.read_exact(dst.as_mut_slice()) {
269            Ok(_) => (),
270            Err(_) => break,
271        };
272        map.insert(tag, dst);
273    }
274    map
275}
276
277fn parse_list(resp: &[u8]) -> Result<Vec<OathCredential>, String> {
278    let mut rdr = Cursor::new(resp);
279
280    let mut result: Vec<OathCredential> = Vec::new();
281
282    while let Ok(tag) = rdr.read_u8() {
283        if tag != (Tag::NameList as u8) {
284            break;
285        }
286        let length = rdr.read_u8().or(Err("Missing length after tag"))?;
287
288        let type_code = rdr
289            .read_u8()
290            .or(Err("Malformed response, missing type code"))?;
291
292        let mut buf = vec![0; (length - 1) as usize];
293        rdr.read_exact(&mut buf)
294            .or(Err("Malformed response, incorrect key name length"))?;
295        result.push(OathCredential::new(
296            &String::from_utf8(buf).or(Err("Invalid key name"))?,
297            OathType::from_u8(type_code & (Mask::Type as u8)).ok_or("Invalid type")?,
298            false,
299            OathAlgo::from_u8(type_code & (Mask::Algo as u8)).ok_or("Invalid algorithm")?,
300        ));
301    }
302
303    return Ok(result);
304}
305
306pub fn time_challenge(datetime: Option<SystemTime>) -> Vec<u8> {
307    let ts = match datetime {
308        Some(datetime) => {
309            datetime
310                .duration_since(SystemTime::UNIX_EPOCH)
311                .unwrap()
312                .as_secs()
313                / 30
314        }
315        None => {
316            SystemTime::now()
317                .duration_since(SystemTime::UNIX_EPOCH)
318                .unwrap()
319                .as_secs()
320                / 30
321        }
322    };
323    let mut buf = vec![];
324    buf.write_u64::<BigEndian>(ts).unwrap();
325    buf
326}
327
328impl fmt::Display for OathCode {
329    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
330        const STEAM_CHAR_TABLE_LEN: u32 = 26;
331        const STEAM_CHAR_TABLE: [char; STEAM_CHAR_TABLE_LEN as usize] = [
332            '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M',
333            'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y',
334        ];
335        let mut code = self.value;
336        if self.steam {
337            let mut str = String::new();
338            for _i in 0..5 {
339                str.push(
340                    *STEAM_CHAR_TABLE
341                        .get((code % STEAM_CHAR_TABLE_LEN) as usize)
342                        .unwrap(),
343                );
344                code /= STEAM_CHAR_TABLE_LEN;
345            }
346            try!(fmt.write_str(&str));
347        } else {
348            let code = self.value % (10 as u32).pow(self.digits as u32);
349            match self.digits {
350                OathDigits::Six => write!(fmt, "{:06}", code),
351                OathDigits::Eight => write!(fmt, "{:08}", code),
352            }
353            .unwrap();
354        }
355        Ok(())
356    }
357}
358
359impl OathCredential {
360    pub fn new(name: &str, oath_type: OathType, touch: bool, algo: OathAlgo) -> OathCredential {
361        OathCredential {
362            name: name.to_string(),
363            code: Err("No code calculated yet".to_string()),
364            oath_type: oath_type,
365            touch: touch,
366            algo: algo,
367            hidden: name.starts_with("_hidden:"),
368            steam: name.starts_with("Steam:"),
369        }
370    }
371}
372
373impl OathController {
374    pub fn new() -> Result<OathController, String> {
375        let mut context = context::new();
376
377        if context.is_null() {
378            return Err("Unable to initialize new NFC context!".to_string());
379        }
380
381        nfc::init(&mut context);
382
383        debug!("libnfc version: {}", ::misc::version());
384
385        let device = nfc::open(context, ptr::null());
386        if device.is_null() {
387            return Err("Unable to initialize new NFC device!".to_string());
388        }
389
390        initiator::init(Box::new(device));
391
392        debug!("NFC reader: {} opened", device::get_name(device));
393
394        device::set_property_bool(device, ffi::Enum_Unnamed1::NP_AUTO_ISO14443_4, 1);
395
396        Ok(OathController {
397            context: context,
398            device: device,
399        })
400    }
401
402    pub fn close(&self) {
403        nfc::close(self.device);
404    }
405
406    pub fn poll(&self, duration: Option<Duration>) -> bool {
407        let start = Instant::now();
408
409        debug!("Polling for target...");
410        let modu = ffi::nfc_modulation {
411            nmt: ffi::nfc_modulation_type::NMT_ISO14443A,
412            nbr: ffi::nfc_baud_rate::NBR_106,
413        };
414        unsafe {
415            let mut target: ffi::nfc_target = mem::uninitialized();
416            while initiator::poll_target(self.device, &modu, 1, 1, 1, &mut target) <= 0 {
417                if let Some(duration) = duration {
418                    if Instant::now() > (start + duration) {
419                        debug!("Poll timed out");
420                        return false;
421                    }
422                }
423            }
424            while initiator::select_passive_target(
425                self.device,
426                modu,
427                (*target.nti.nai()).abtUid.as_mut_ptr(),
428                (*target.nti.nai()).szUidLen,
429                &mut target,
430            ) <= 0
431            {}
432        }
433        debug!("Target detected!");
434        return true;
435    }
436
437    /* https://en.wikipedia.org/wiki/Application_protocol_data_unit
438    Command APDU
439    Field name	Length (bytes)	Description
440    CLA	1	Instruction class - indicates the type of command, e.g. interindustry or proprietary
441    INS	1	Instruction code - indicates the specific command, e.g. "write data"
442    P1-P2	2	Instruction parameters for the command, e.g. offset into file at which to write the data
443    Lc	0, 1 or 3	Encodes the number (Nc) of bytes of command data to follow
444        0 bytes denotes Nc=0
445        1 byte with a value from 1 to 255 denotes Nc with the same value
446        3 bytes, the first of which must be 0, denotes Nc in the range 1 to 65 535 (all three bytes may not be zero)
447    Command data	Nc	Nc bytes of data
448    Le	0, 1, 2 or 3	Encodes the maximum number (Ne) of response bytes expected
449        0 bytes denotes Ne=0
450        1 byte in the range 1 to 255 denotes that value of Ne, or 0 denotes Ne=256
451        2 bytes (if Lc was present in the command) in the range 1 to 65 535 denotes Ne of that value, or two zero bytes denotes 65 536
452        3 bytes (if Lc was not present in the command), the first of which must be 0, denote Ne in the same way as two-byte Le
453    */
454    pub fn send_apdu(
455        &self,
456        class: u8,
457        instruction: u8,
458        parameter1: u8,
459        parameter2: u8,
460        data: Option<&[u8]>,
461    ) -> Result<Vec<u8>, String> {
462        let mut tx_buf = vec![];
463        let nc = match data {
464            Some(ref data) => data.len(),
465            None => 0,
466        };
467        // Header
468        tx_buf.push(class);
469        tx_buf.push(instruction);
470        tx_buf.push(parameter1);
471        tx_buf.push(parameter2);
472
473        // Data
474        if nc > 255 {
475            tx_buf.push(0);
476            tx_buf.write_u16::<BigEndian>(nc as u16).unwrap();
477        } else if nc > 0 {
478            tx_buf.push(nc as u8);
479        }
480        if let Some(data) = data {
481            tx_buf.write(data).unwrap();
482        }
483
484        let mut s = String::new();
485        for byte in &tx_buf {
486            s += &format!("{:02X} ", byte);
487        }
488        debug!(">> {}", s);
489
490        let mut rx_buf = Vec::with_capacity(256);
491        let bytes_read = initiator::transceive_bytes(
492            self.device,
493            tx_buf.as_ptr(),
494            tx_buf.len(),
495            rx_buf.as_mut_ptr(),
496            256,
497            0,
498        );
499        if bytes_read < 0 {
500            return Err("Error no bytes were returned".to_string());
501        }
502
503        unsafe {
504            rx_buf.set_len(bytes_read as usize);
505        }
506
507        let mut s = String::new();
508        for byte in &rx_buf {
509            s += &format!("{:02X} ", byte);
510        }
511        debug!("<< {}", s);
512
513        {
514            let sw1 = match rx_buf.get((bytes_read - 2) as usize) {
515                Some(sw1) => sw1,
516                None => return Err("Error invalid bytes were returned".to_string()),
517            };
518            let sw2 = match rx_buf.get((bytes_read - 1) as usize) {
519                Some(sw2) => sw2,
520                None => return Err("Error invalid bytes were returned".to_string()),
521            };
522            if *sw1 != 0x90 || *sw2 != 0x00 {
523                return Err(format!("Error {:x} {:x}", sw1, sw2));
524            }
525        }
526        Ok(rx_buf)
527    }
528
529    pub fn list(&self) -> Result<Vec<OathCredential>, String> {
530        // Switch to the OATH applet
531        self.send_apdu(0, INS_SELECT, 0x04, 0, Some(&OATH_AID))?;
532
533        let resp = self.send_apdu(0, Ins::List as u8, 0, 0, None)?;
534        return parse_list(&resp);
535    }
536
537    pub fn calculate(&self, mut credential: OathCredential) -> OathCredential {
538        // Switch to the OATH applet
539        if let Err(err) = self.send_apdu(0, INS_SELECT, 0x04, 0, Some(&OATH_AID)) {
540            credential.code = Err(err);
541            return credential;
542        }
543
544        let mut data = tlv(Tag::Name, credential.name.as_bytes());
545        let datetime = SystemTime::now();
546        let challenge = time_challenge(Some(datetime));
547        data.write(&tlv(Tag::Challenge, &challenge)).unwrap();
548        let resp = match self.send_apdu(0, Ins::Calculate as u8, 0, 0x01, Some(&data)) {
549            Ok(resp) => resp,
550            Err(err) => {
551                credential.code = Err(err);
552                return credential;
553            }
554        };
555
556        let resp = parse_tlv(&resp[0..resp.len() - 2]);
557        let resp = match resp.get(&(Tag::TruncatedResponse as u8)) {
558            Some(resp) => resp,
559            None => {
560                credential.code = Err("Response tlv was invalid".to_string());
561                return credential;
562            }
563        };
564        let mut rdr = Cursor::new(resp);
565
566        let digits = match rdr.read_u8() {
567            Ok(digits) => {
568                let digits = match digits {
569                    6 => OathDigits::Six,
570                    8 => OathDigits::Eight,
571                    _ => {
572                        credential.code = Err("Digits can only be 6 or 8".to_owned());
573                        return credential;
574                    }
575                };
576                digits
577            }
578            Err(err) => {
579                credential.code = Err(err.to_string());
580                return credential;
581            }
582        };
583
584        let val = match rdr.read_u32::<BigEndian>() {
585            Ok(val) => val,
586            Err(err) => {
587                credential.code = Err(err.to_string());
588                return credential;
589            }
590        };
591        let expiration = ((datetime
592            .duration_since(SystemTime::UNIX_EPOCH)
593            .unwrap()
594            .as_secs()
595            + 30)
596            / 30)
597            * 30;
598
599        credential.code = Ok(OathCode {
600            digits: digits,
601            value: val,
602            expiration: expiration as u32,
603            steam: credential.steam,
604        });
605        credential
606    }
607}