yubikey_cli/
terminal.rs

1//! Status messages
2
3use log::debug;
4use once_cell::sync::Lazy;
5use sha2::{Digest, Sha256};
6use std::{
7    io::{self, Write},
8    str,
9    sync::Mutex,
10};
11use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
12use x509_parser::parse_x509_certificate;
13use yubikey::{certificate::Certificate, piv::*, YubiKey};
14
15/// Print a success status message (in green if colors are enabled)
16#[macro_export]
17macro_rules! status_ok {
18    ($status:expr, $msg:expr) => {
19        $crate::terminal::Status::new()
20            .justified()
21            .bold()
22            .color(termcolor::Color::Green)
23            .status($status)
24            .print_stdout($msg);
25    };
26    ($status:expr, $fmt:expr, $($arg:tt)+) => {
27        $crate::status_ok!($status, format!($fmt, $($arg)+));
28    };
29}
30
31/// Print a warning status message (in yellow if colors are enabled)
32#[macro_export]
33macro_rules! status_warn {
34    ($msg:expr) => {
35        $crate::terminal::Status::new()
36            .bold()
37            .color(termcolor::Color::Yellow)
38            .status("warning:")
39            .print_stdout($msg);
40    };
41    ($fmt:expr, $($arg:tt)+) => {
42        $crate::status_warn!(format!($fmt, $($arg)+));
43    };
44}
45
46/// Print an error message (in red if colors are enabled)
47#[macro_export]
48macro_rules! status_err {
49    ($msg:expr) => {
50        $crate::terminal::Status::new()
51            .bold()
52            .color(termcolor::Color::Red)
53            .status("error:")
54            .print_stderr($msg);
55    };
56    ($fmt:expr, $($arg:tt)+) => {
57        $crate::status_err!(format!($fmt, $($arg)+));
58    };
59}
60
61/// Color configuration
62static COLOR_CHOICE: Lazy<Mutex<Option<ColorChoice>>> = Lazy::new(|| Mutex::new(None));
63
64/// Standard output
65pub static STDOUT: Lazy<StandardStream> = Lazy::new(|| StandardStream::stdout(get_color_choice()));
66
67/// Standard error
68pub static STDERR: Lazy<StandardStream> = Lazy::new(|| StandardStream::stderr(get_color_choice()));
69
70/// Obtain the color configuration.
71///
72/// Panics if no configuration has been provided.
73fn get_color_choice() -> ColorChoice {
74    let choice = COLOR_CHOICE.lock().unwrap();
75    *choice
76        .as_ref()
77        .expect("terminal stream accessed before initialized!")
78}
79
80/// Set the color configuration.
81///
82/// Panics if the terminal has already been configured.
83pub(super) fn set_color_choice(color_choice: ColorChoice) {
84    let mut choice = COLOR_CHOICE.lock().unwrap();
85    assert!(choice.is_none(), "terminal colors already configured!");
86    *choice = Some(color_choice);
87}
88
89/// Status message builder
90#[derive(Clone, Debug, Default)]
91pub struct Status {
92    /// Should the status be justified?
93    justified: bool,
94
95    /// Should colors be bold?
96    bold: bool,
97
98    /// Color in which status should be displayed
99    color: Option<Color>,
100
101    /// Prefix of the status message (e.g. `Success`)
102    status: Option<String>,
103}
104
105impl Status {
106    /// Create a new status message with default settings
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Justify status on display
112    pub fn justified(mut self) -> Self {
113        self.justified = true;
114        self
115    }
116
117    /// Make colors bold
118    pub fn bold(mut self) -> Self {
119        self.bold = true;
120        self
121    }
122
123    /// Set the colors used to display this message
124    pub fn color(mut self, c: Color) -> Self {
125        self.color = Some(c);
126        self
127    }
128
129    /// Set a status message to display
130    pub fn status<S>(mut self, msg: S) -> Self
131    where
132        S: ToString,
133    {
134        self.status = Some(msg.to_string());
135        self
136    }
137
138    /// Print the given message to stdout
139    pub fn print_stdout(self, msg: impl AsRef<str>) {
140        self.print(&STDOUT, msg).expect("error printing to stdout!")
141    }
142
143    /// Print the given message to stderr
144    pub fn print_stderr(self, msg: impl AsRef<str>) {
145        self.print(&STDERR, msg).expect("error printing to stderr!")
146    }
147
148    /// Print the given message
149    fn print(self, stream: &StandardStream, msg: impl AsRef<str>) -> io::Result<()> {
150        let mut s = stream.lock();
151        s.reset()?;
152        s.set_color(ColorSpec::new().set_fg(self.color).set_bold(self.bold))?;
153
154        if let Some(status) = self.status {
155            if self.justified {
156                write!(s, "{:>12}", status)?;
157            } else {
158                write!(s, "{}", status)?;
159            }
160        }
161
162        s.reset()?;
163        writeln!(s, " {}", msg.as_ref())?;
164        s.flush()?;
165
166        Ok(())
167    }
168}
169
170/// Write information about certificate found in slot a la yubico-piv-tool output.
171pub fn print_cert_info(
172    yubikey: &mut YubiKey,
173    slot: SlotId,
174    stream: &mut StandardStreamLock<'_>,
175) -> io::Result<()> {
176    let cert = match Certificate::read(yubikey, slot) {
177        Ok(c) => c,
178        Err(e) => {
179            debug!("error reading certificate in slot {:?}: {}", slot, e);
180            return Ok(());
181        }
182    };
183    let buf = cert.into_buffer();
184
185    if !buf.is_empty() {
186        let fingerprint = Sha256::digest(&buf);
187        let slot_id: u8 = slot.into();
188        print_cert_attr(stream, "Slot", format!("{:x}", slot_id))?;
189        match parse_x509_certificate(&buf) {
190            Ok((_rem, cert)) => {
191                print_cert_attr(
192                    stream,
193                    "Algorithm",
194                    cert.tbs_certificate.subject_pki.algorithm.algorithm,
195                )?;
196
197                print_cert_attr(stream, "Subject", cert.tbs_certificate.subject)?;
198                print_cert_attr(stream, "Issuer", cert.tbs_certificate.issuer)?;
199                print_cert_attr(
200                    stream,
201                    "Fingerprint",
202                    &hex::upper::encode_string(&fingerprint),
203                )?;
204                print_cert_attr(
205                    stream,
206                    "Not Before",
207                    cert.tbs_certificate
208                        .validity
209                        .not_before
210                        .to_rfc2822()
211                        .unwrap(),
212                )?;
213                print_cert_attr(
214                    stream,
215                    "Not After",
216                    cert.tbs_certificate
217                        .validity
218                        .not_after
219                        .to_rfc2822()
220                        .unwrap(),
221                )?;
222            }
223            _ => {
224                println!("Failed to parse certificate");
225                return Ok(());
226            }
227        };
228    }
229
230    Ok(())
231}
232
233/// Print a status attribute
234fn print_cert_attr(
235    stream: &mut StandardStreamLock<'_>,
236    name: &str,
237    value: impl ToString,
238) -> io::Result<()> {
239    stream.set_color(ColorSpec::new().set_bold(true))?;
240    write!(stream, "{:>12}:", name)?;
241    stream.reset()?;
242    writeln!(stream, " {}", value.to_string())?;
243    stream.flush()?;
244    Ok(())
245}