//! Status messages use log::debug; use once_cell::sync::Lazy; use sha2::{Digest, Sha256}; use std::{ io::{self, Write}, str, sync::Mutex, }; use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor}; use x509_cert::der::Encode; use yubikey::{certificate::Certificate, piv::*, YubiKey}; /// Print a success status message (in green if colors are enabled) #[macro_export] macro_rules! status_ok { ($status:expr, $msg:expr) => { $crate::terminal::Status::new() .justified() .bold() .color(termcolor::Color::Green) .status($status) .print_stdout($msg); }; ($status:expr, $fmt:expr, $($arg:tt)+) => { $crate::status_ok!($status, format!($fmt, $($arg)+)); }; } /// Print a warning status message (in yellow if colors are enabled) #[macro_export] macro_rules! status_warn { ($msg:expr) => { $crate::terminal::Status::new() .bold() .color(termcolor::Color::Yellow) .status("warning:") .print_stdout($msg); }; ($fmt:expr, $($arg:tt)+) => { $crate::status_warn!(format!($fmt, $($arg)+)); }; } /// Print an error message (in red if colors are enabled) #[macro_export] macro_rules! status_err { ($msg:expr) => { $crate::terminal::Status::new() .bold() .color(termcolor::Color::Red) .status("error:") .print_stderr($msg); }; ($fmt:expr, $($arg:tt)+) => { $crate::status_err!(format!($fmt, $($arg)+)); }; } /// Color configuration static COLOR_CHOICE: Lazy>> = Lazy::new(|| Mutex::new(None)); /// Standard output pub static STDOUT: Lazy = Lazy::new(|| StandardStream::stdout(get_color_choice())); /// Standard error pub static STDERR: Lazy = Lazy::new(|| StandardStream::stderr(get_color_choice())); /// Obtain the color configuration. /// /// Panics if no configuration has been provided. fn get_color_choice() -> ColorChoice { let choice = COLOR_CHOICE.lock().unwrap(); *choice .as_ref() .expect("terminal stream accessed before initialized!") } /// Set the color configuration. /// /// Panics if the terminal has already been configured. pub(super) fn set_color_choice(color_choice: ColorChoice) { let mut choice = COLOR_CHOICE.lock().unwrap(); assert!(choice.is_none(), "terminal colors already configured!"); *choice = Some(color_choice); } /// Status message builder #[derive(Clone, Debug, Default)] pub struct Status { /// Should the status be justified? justified: bool, /// Should colors be bold? bold: bool, /// Color in which status should be displayed color: Option, /// Prefix of the status message (e.g. `Success`) status: Option, } impl Status { /// Create a new status message with default settings pub fn new() -> Self { Self::default() } /// Justify status on display pub fn justified(mut self) -> Self { self.justified = true; self } /// Make colors bold pub fn bold(mut self) -> Self { self.bold = true; self } /// Set the colors used to display this message pub fn color(mut self, c: Color) -> Self { self.color = Some(c); self } /// Set a status message to display pub fn status(mut self, msg: S) -> Self where S: ToString, { self.status = Some(msg.to_string()); self } /// Print the given message to stdout pub fn print_stdout(self, msg: impl AsRef) { self.print(&STDOUT, msg).expect("error printing to stdout!") } /// Print the given message to stderr pub fn print_stderr(self, msg: impl AsRef) { self.print(&STDERR, msg).expect("error printing to stderr!") } /// Print the given message fn print(self, stream: &StandardStream, msg: impl AsRef) -> io::Result<()> { let mut s = stream.lock(); s.reset()?; s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?; if let Some(status) = self.status { if self.justified { write!(s, "{:>12}", status)?; } else { write!(s, "{}", status)?; } } s.reset()?; writeln!(s, " {}", msg.as_ref())?; s.flush()?; Ok(()) } } /// Write information about certificate found in slot a la yubico-piv-tool output. pub fn print_cert_info( yubikey: &mut YubiKey, slot: SlotId, stream: &mut StandardStreamLock<'_>, ) -> io::Result<()> { let cert = match Certificate::read(yubikey, slot) { Ok(c) => c, Err(e) => { debug!("error reading certificate in slot {:?}: {}", slot, e); return Ok(()); } }; let cert = &cert.cert; let fingerprint = Sha256::digest(cert.to_der().unwrap()); let slot_id: u8 = slot.into(); print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?; print_cert_attr( stream, "Algorithm", cert.tbs_certificate.subject_public_key_info.algorithm.oid, )?; print_cert_attr(stream, "Subject", &cert.tbs_certificate.subject)?; print_cert_attr(stream, "Issuer", &cert.tbs_certificate.issuer)?; print_cert_attr( stream, "Fingerprint", &hex::upper::encode_string(&fingerprint), )?; print_cert_attr( stream, "Not Before", cert.tbs_certificate.validity.not_before, )?; print_cert_attr(stream, "Not After", cert.tbs_certificate.validity.not_after)?; Ok(()) } /// Print a status attribute fn print_cert_attr( stream: &mut StandardStreamLock<'_>, name: &str, value: impl ToString, ) -> io::Result<()> { stream.set_color(ColorSpec::new().set_bold(true))?; write!(stream, "{:>12}:", name)?; stream.reset()?; writeln!(stream, " {}", value.to_string())?; stream.flush()?; Ok(()) }