diff --git a/Cargo.lock b/Cargo.lock index e53db8f..4f80d37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -710,6 +710,14 @@ name = "subtle" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "subtle-encoding" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "syn" version = "1.0.11" @@ -856,6 +864,7 @@ dependencies = [ "secrecy 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", "sha-1 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle-encoding 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "x509-parser 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "zeroize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -972,6 +981,7 @@ dependencies = [ "checksum static_assertions 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3" "checksum subtle 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" "checksum subtle 2.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c65d530b10ccaeac294f349038a597e435b18fb456aadd0840a623f83b9e941" +"checksum subtle-encoding 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cbc5188a16f729680b6d495b0deaa776944b8e509d24b7f989489b0d8bbcb63b" "checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e" diff --git a/Cargo.toml b/Cargo.toml index a04d58e..e390e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ rsa = "0.1.4" secrecy = "0.5" sha-1 = "0.8" subtle = "2" +subtle-encoding = "0.5" x509-parser = "0.6" zeroize = "1" diff --git a/cli/src/bin/yubikey/main.rs b/cli/src/bin/yubikey/main.rs index 80db9c8..5df4617 100644 --- a/cli/src/bin/yubikey/main.rs +++ b/cli/src/bin/yubikey/main.rs @@ -9,8 +9,8 @@ )] use gumdrop::Options; -use yubikey_cli::commands::YubikeyCli; +use yubikey_cli::commands::YubiKeyCli; fn main() { - YubikeyCli::parse_args_default_or_exit().run(); + YubiKeyCli::parse_args_default_or_exit().run(); } diff --git a/cli/src/commands.rs b/cli/src/commands.rs index 5ec26b6..9773444 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -1,9 +1,10 @@ //! Commands of the CLI application pub mod readers; +pub mod status; -use self::readers::ReadersCmd; -use crate::status::{self, STDOUT}; +use self::{readers::ReadersCmd, status::StatusCmd}; +use crate::terminal::{self, STDOUT}; use gumdrop::Options; use std::{ env, @@ -11,36 +12,29 @@ use std::{ process::exit, }; use termcolor::{ColorChoice, ColorSpec, WriteColor}; +use yubikey_piv::{Serial, YubiKey}; /// The `yubikey` CLI utility #[derive(Debug, Options)] -pub struct YubikeyCli { +pub struct YubiKeyCli { /// Obtain help about the current command #[options(short = "h", help = "print help message")] pub help: bool, + /// Specify the serial number of the YubiKey to connect to + #[options( + short = "s", + long = "serial", + help = "serial number of the YubiKey to connect to" + )] + pub serial: Option, + /// Subcommand to execute. #[options(command)] pub command: Option, } -impl YubikeyCli { - /// Run the underlying command type or print usage info and exit - pub fn run(&self) { - // TODO(tarcieri): make this more configurable - status::set_color_choice(ColorChoice::Auto); - - // Only show logs if `RUST_LOG` is set - if env::var("RUST_LOG").is_ok() { - env_logger::builder().format_timestamp(None).init(); - } - - match &self.command { - Some(cmd) => cmd.run(), - None => Self::print_usage().unwrap(), - } - } - +impl YubiKeyCli { /// Print usage information pub fn print_usage() -> Result<(), io::Error> { let mut stdout = STDOUT.lock(); @@ -65,6 +59,36 @@ impl YubikeyCli { Ok(()) } + + /// Run the underlying command type or print usage info and exit + pub fn run(&self) { + // TODO(tarcieri): make this more configurable + terminal::set_color_choice(ColorChoice::Auto); + + // Only show logs if `RUST_LOG` is set + if env::var("RUST_LOG").is_ok() { + env_logger::builder().format_timestamp(None).init(); + } + + match &self.command { + Some(cmd) => cmd.run(self.yubikey_init()), + None => Self::print_usage().unwrap(), + } + } + + /// Initialize the YubiKey client driver + fn yubikey_init(&self) -> YubiKey { + match self.serial { + Some(serial) => YubiKey::open_by_serial(serial).unwrap_or_else(|e| { + status_err!("couldn't open YubiKey (serial #{}): {}", serial, e); + exit(1); + }), + None => YubiKey::open().unwrap_or_else(|e| { + status_err!("couldn't open default YubiKey: {}", e); + exit(1); + }), + } + } } /// Subcommands of this application @@ -81,15 +105,20 @@ pub enum Commands { /// `readers` subcommand #[options(help = "list detected readers")] Readers(ReadersCmd), + + /// `status` subcommand + #[options(help = "show yubikey status")] + Status(StatusCmd), } impl Commands { /// Run the given command - pub fn run(&self) { + pub fn run(&self, yubikey: YubiKey) { match self { Commands::Help(help) => help.run(), Commands::Version(version) => version.run(), Commands::Readers(list) => list.run(), + Commands::Status(status) => status.run(yubikey), } } } diff --git a/cli/src/commands/readers.rs b/cli/src/commands/readers.rs index 7db161f..a7f5bc6 100644 --- a/cli/src/commands/readers.rs +++ b/cli/src/commands/readers.rs @@ -1,8 +1,13 @@ //! List detected readers +use crate::terminal::STDOUT; use gumdrop::Options; -use std::process::exit; -use yubikey_piv::readers::Readers; +use std::{ + io::{self, Write}, + process::exit, +}; +use termcolor::{ColorSpec, StandardStreamLock, WriteColor}; +use yubikey_piv::{Readers, Serial}; /// The `readers` subcommand #[derive(Debug, Options)] @@ -26,6 +31,9 @@ impl ReadersCmd { exit(1); } + let mut s = STDOUT.lock(); + s.reset().unwrap(); + for (i, reader) in readers_iter.enumerate() { let name = reader.name(); let mut yubikey = match reader.open() { @@ -34,7 +42,23 @@ impl ReadersCmd { }; let serial = yubikey.serial(); - println!("{}: {} (serial: {})", i + 1, name, serial); + self.print_reader(&mut s, i + 1, &name, serial).unwrap(); } } + + /// Print a reader + fn print_reader( + &self, + stream: &mut StandardStreamLock<'_>, + index: usize, + name: &str, + serial: Serial, + ) -> Result<(), io::Error> { + stream.set_color(ColorSpec::new().set_bold(true))?; + write!(stream, "{:>3}:", index)?; + stream.reset()?; + writeln!(stream, " {} (serial: {})", name, serial)?; + stream.flush()?; + Ok(()) + } } diff --git a/cli/src/commands/status.rs b/cli/src/commands/status.rs new file mode 100644 index 0000000..6eccc22 --- /dev/null +++ b/cli/src/commands/status.rs @@ -0,0 +1,55 @@ +//! Print device status + +use crate::terminal::STDOUT; +use gumdrop::Options; +use std::io::{self, Write}; +use termcolor::{ColorSpec, StandardStreamLock, WriteColor}; +use yubikey_piv::YubiKey; + +// String to use for `None` +const NONE_STR: &str = ""; + +/// The `status` subcommand +#[derive(Debug, Options)] +pub struct StatusCmd {} + +impl StatusCmd { + /// Run the `status` subcommand + pub fn run(&self, mut yk: YubiKey) { + let mut s = STDOUT.lock(); + s.reset().unwrap(); + + self.attr(&mut s, "version", yk.version()).unwrap(); + self.attr(&mut s, "serial", yk.serial()).unwrap(); + + if let Ok(chuid) = yk.chuid() { + self.attr(&mut s, "CHUID", chuid).unwrap(); + } else { + self.attr(&mut s, "CHUID", NONE_STR).unwrap(); + } + + if let Ok(chuid) = yk.cccid() { + self.attr(&mut s, "CCC", chuid).unwrap(); + } else { + self.attr(&mut s, "CCC", NONE_STR).unwrap(); + } + + self.attr(&mut s, "PIN retries", yk.get_pin_retries().unwrap()) + .unwrap(); + } + + /// Print a status attribute + fn attr( + &self, + stream: &mut StandardStreamLock<'_>, + name: &str, + value: impl ToString, + ) -> Result<(), io::Error> { + stream.set_color(ColorSpec::new().set_bold(true))?; + write!(stream, "{:>12}:", name)?; + stream.reset()?; + writeln!(stream, " {}", value.to_string())?; + stream.flush()?; + Ok(()) + } +} diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 7e9024e..04fade7 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -9,6 +9,6 @@ )] #[macro_use] -pub mod status; +pub mod terminal; pub mod commands; diff --git a/cli/src/status.rs b/cli/src/terminal.rs similarity index 97% rename from cli/src/status.rs rename to cli/src/terminal.rs index 6c8bbc5..a4a724d 100644 --- a/cli/src/status.rs +++ b/cli/src/terminal.rs @@ -9,7 +9,7 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; #[macro_export] macro_rules! status_ok { ($status:expr, $msg:expr) => { - $crate::status::Status::new() + $crate::terminal::Status::new() .justified() .bold() .color(termcolor::Color::Green) @@ -25,7 +25,7 @@ macro_rules! status_ok { #[macro_export] macro_rules! status_warn { ($msg:expr) => { - $crate::status::Status::new() + $crate::terminal::Status::new() .bold() .color(termcolor::Color::Yellow) .status("warning:") @@ -40,7 +40,7 @@ macro_rules! status_warn { #[macro_export] macro_rules! status_err { ($msg:expr) => { - $crate::status::Status::new() + $crate::terminal::Status::new() .bold() .color(termcolor::Color::Red) .status("error:") diff --git a/src/cccid.rs b/src/cccid.rs index 424567a..4af6430 100644 --- a/src/cccid.rs +++ b/src/cccid.rs @@ -32,7 +32,8 @@ use crate::{error::Error, yubikey::YubiKey}; use getrandom::getrandom; -use std::fmt::{self, Debug}; +use std::fmt::{self, Debug, Display}; +use subtle_encoding::hex; /// CCCID size pub const CCCID_SIZE: usize = 14; @@ -116,3 +117,13 @@ impl Debug for CCC { write!(f, "CCC({:?})", &self.0[..]) } } + +impl Display for CCC { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + String::from_utf8(hex::encode(&self.0[..])).unwrap() + ) + } +} diff --git a/src/chuid.rs b/src/chuid.rs index ff9132e..c572a84 100644 --- a/src/chuid.rs +++ b/src/chuid.rs @@ -32,7 +32,8 @@ use crate::{error::Error, yubikey::YubiKey}; use getrandom::getrandom; -use std::fmt::{self, Debug}; +use std::fmt::{self, Debug, Display}; +use subtle_encoding::hex; /// CHUID size pub const CHUID_SIZE: usize = 59; @@ -149,6 +150,16 @@ impl CHUID { } } +impl Display for CHUID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + String::from_utf8(hex::encode(&self.0[..])).unwrap() + ) + } +} + impl Debug for CHUID { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "CHUID({:?})", &self.0[..]) diff --git a/src/lib.rs b/src/lib.rs index eb0a8c9..5c2d38b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,7 +156,13 @@ pub mod settings; mod transaction; pub mod yubikey; -pub use self::{error::Error, key::Key, mgm::MgmKey, readers::Readers, yubikey::YubiKey}; +pub use self::{ + error::Error, + key::Key, + mgm::MgmKey, + readers::Readers, + yubikey::{Serial, YubiKey}, +}; /// Object identifiers pub type ObjectId = u32; diff --git a/src/yubikey.rs b/src/yubikey.rs index 3ba012d..8478e22 100644 --- a/src/yubikey.rs +++ b/src/yubikey.rs @@ -43,6 +43,7 @@ use pcsc::Card; use std::{ convert::TryFrom, fmt::{self, Display}, + str::FromStr, }; #[cfg(feature = "untested")] @@ -103,6 +104,14 @@ impl From for u32 { } } +impl FromStr for Serial { + type Err = Error; + + fn from_str(s: &str) -> Result { + u32::from_str(s).map(Serial).map_err(|_| Error::ParseError) + } +} + impl Display for Serial { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) @@ -350,7 +359,6 @@ impl YubiKey { } /// Get the number of PIN retries - #[cfg(feature = "untested")] pub fn get_pin_retries(&mut self) -> Result { let txn = self.begin_transaction()?;