#![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 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 for Error { fn from(error: string::FromUtf8Error) -> Error { Error::Utf8Error(error) } } #[derive(Debug)] struct Options { slot: u8, file: Option, name: Option, } fn parse_options() -> Result { 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 { 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 { 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::>(); 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); }