aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 9c2a8c1e4de92e02338e658431afa618d2ab44ef (plain)
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
#![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)]

//! Reads OTP configuration from a QR code and writes it to an OTP slot on a Nitrokey device.

use std::fmt;
use std::fs;
use std::io;
use std::path;
use std::process;
use std::string;

#[derive(Debug)]
enum Error {
    IoError(io::Error),
    Error(String),
    Utf8Error(string::FromUtf8Error),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Error::IoError(ref err) => write!(f, "IO error: {}", err),
            Error::Error(ref string) => write!(f, "Error: {}", string),
            Error::Utf8Error(ref err) => write!(f, "UTF-8 error: {}", err),
        }
    }
}

impl From<&str> for Error {
    fn from(string: &str) -> Error {
        Error::Error(string.to_string())
    }
}

impl From<io::Error> for Error {
    fn from(error: io::Error) -> Error {
        Error::IoError(error)
    }
}

impl From<(&str, process::ExitStatus)> for Error {
    fn from(data: (&str, process::ExitStatus)) -> Error {
        let (command, status) = data;
        let msg = match status.code() {
            Some(code) => format!("{} failed with error code {}", command, code),
            None => format!("{} was terminated by a signal", command),
        };
        Error::Error(msg)
    }
}

impl From<string::FromUtf8Error> for Error {
    fn from(error: string::FromUtf8Error) -> Error {
        Error::Utf8Error(error)
    }
}

#[derive(Debug)]
struct Options {
    slot: u8,
    file: Option<String>,
    name: Option<String>,
}

fn parse_options() -> Result<Options, i32> {
    let mut options = Options {
        slot: 0,
        file: None,
        name: None,
    };
    let mut parser = argparse::ArgumentParser::new();
    parser.set_description(
        "Reads OTP configuration from a QR code and writes it to an OTP slot on a Nitrokey device.",
    );
    parser.refer(&mut options.slot).required().add_argument(
        "slot",
        argparse::Store,
        "The slot to write the OTP data to",
    );
    parser.refer(&mut options.file).add_argument(
        "file",
        argparse::StoreOption,
        "The file to read the QR code from",
    );
    parser.refer(&mut options.name).add_option(
        &["-n", "--name"],
        argparse::StoreOption,
        "The name to store in the OTP slot",
    );
    parser.parse_args()?;
    drop(parser);
    Ok(options)
}

fn import_qr_code() -> Result<path::PathBuf, Error> {
    let mut temp = mktemp::Temp::new_file()?;
    let path = temp.to_path_buf();

    let status = process::Command::new("import").arg(&path).status()?;

    if status.success() {
        temp.release();
        Ok(path)
    } else {
        Err(Error::from(("import", status)))
    }
}

fn decode_qr_code(path: &path::Path) -> Result<String, Error> {
    let output = process::Command::new("zbarimg")
        .arg("--quiet")
        .arg("--raw")
        .arg(path)
        .output()?;
    if output.status.success() {
        let output = String::from_utf8(output.stdout)?;
        let urls = output
            .split("\n")
            .filter(|url| url.starts_with("otpauth://"))
            .collect::<Vec<&str>>();
        if urls.is_empty() {
            Err(Error::from("Could not find an otpauth QR code"))
        } else {
            if urls.len() > 1 {
                println!("Found more than otpauth QR code, using the first one.");
            }
            Ok(urls[0].to_string())
        }
    } else {
        Err(Error::from(("zbarimg", output.status)))
    }
}

fn run(options: Options) -> Result<(), Error> {
    let path = match options.file {
        Some(ref file) => path::PathBuf::from(file),
        None => import_qr_code()?,
    };
    println!("{}", decode_qr_code(&path)?);
    if options.file.is_none() {
        fs::remove_file(&path)?;
    }
    Ok(())
}

fn main() {
    let status = match parse_options() {
        Ok(options) => match run(options) {
            Ok(()) => 0,
            Err(err) => {
                println!("{}", err);
                1
            }
        },
        Err(err) => err,
    };
    process::exit(status);
}