Skip to main content

yb_core/errors/
mod.rs

1// SPDX-FileCopyrightText: 2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
2//
3// SPDX-License-Identifier: MIT
4
5//! Typed errors and their human-readable rendering (spec 0025).
6//!
7//! - [`CardError`]: a failure reported by the card (status word) or by
8//!   PC/SC, tagged with the operation yb was performing.
9//! - [`YbError`]: one of yb's own errors, already phrased as what / why /
10//!   fix.
11//!
12//! Both travel through `anyhow` unchanged; [`render`] turns an error chain
13//! into the text shown to the user.
14
15mod catalog;
16
17pub use catalog::{explain_pcsc, render, Explanation, RenderEnv};
18
19use crate::piv::MgmtAlgo;
20use std::fmt;
21
22/// The card operation that failed, in yb terms.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CardOp {
25    SelectPiv,
26    VerifyPin,
27    /// CHANGE REFERENCE DATA for the PIN.
28    ChangePin,
29    /// CHANGE REFERENCE DATA for the PUK.
30    ChangePuk,
31    MgmtAuth,
32    SetMgmtKey,
33    ReadObject(u32),
34    /// Reading the certificate of a key slot.
35    ReadCertificate(u8),
36    WriteObject(u32),
37    GenerateKey(u8),
38    Sign(u8),
39    Ecdh(u8),
40    GetMetadata(u8),
41    /// RESET of the PIV application.
42    ResetPiv,
43    /// Any other card command (e.g. a raw `send_apdu`).
44    Command,
45}
46
47impl CardOp {
48    /// Short description, used on the details line.
49    pub fn describe(&self) -> String {
50        match self {
51            Self::SelectPiv => "selecting the PIV application".to_owned(),
52            Self::VerifyPin => "PIN verification".to_owned(),
53            Self::ChangePin => "PIN change".to_owned(),
54            Self::ChangePuk => "PUK change".to_owned(),
55            Self::MgmtAuth => "management key authentication".to_owned(),
56            Self::SetMgmtKey => "changing the management key".to_owned(),
57            Self::ReadObject(id) => format!("read object 0x{id:06X}"),
58            Self::ReadCertificate(slot) => format!("read certificate of slot 0x{slot:02x}"),
59            Self::WriteObject(id) => format!("write object 0x{id:06X}"),
60            Self::GenerateKey(slot) => format!("key generation in slot 0x{slot:02x}"),
61            Self::Sign(slot) => format!("signature with slot 0x{slot:02x}"),
62            Self::Ecdh(slot) => format!("key agreement with slot 0x{slot:02x}"),
63            Self::GetMetadata(slot) => format!("metadata of slot 0x{slot:02x}"),
64            Self::ResetPiv => "PIV reset".to_owned(),
65            Self::Command => "card command".to_owned(),
66        }
67    }
68}
69
70/// What the failing step knew when it failed (spec 0025 §1).
71#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72pub struct ErrCtx {
73    /// The management key algorithm yb used, when known.
74    pub algo: Option<MgmtAlgo>,
75}
76
77/// The PC/SC step that failed.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum PcscOp {
80    Establish,
81    ListReaders,
82    Connect,
83    Transmit,
84}
85
86impl PcscOp {
87    fn describe(self) -> &'static str {
88        match self {
89            Self::Establish => "PC/SC context",
90            Self::ListReaders => "listing PC/SC readers",
91            Self::Connect => "connecting to the YubiKey",
92            Self::Transmit => "exchanging data with the YubiKey",
93        }
94    }
95}
96
97/// A PC/SC failure, reduced to the cases the catalog explains.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub enum PcscCode {
100    NoService,
101    NoReaders,
102    SharingViolation,
103    RemovedCard,
104    ResetCard,
105    /// Anything else, with the PC/SC library's description.
106    Other(String),
107}
108
109impl From<pcsc::Error> for PcscCode {
110    fn from(e: pcsc::Error) -> Self {
111        match e {
112            pcsc::Error::NoService | pcsc::Error::ServiceStopped => Self::NoService,
113            pcsc::Error::NoReadersAvailable | pcsc::Error::UnknownReader => Self::NoReaders,
114            pcsc::Error::SharingViolation => Self::SharingViolation,
115            pcsc::Error::RemovedCard | pcsc::Error::NoSmartcard => Self::RemovedCard,
116            pcsc::Error::ResetCard => Self::ResetCard,
117            other => Self::Other(format!("{other:?}")),
118        }
119    }
120}
121
122impl fmt::Display for PcscCode {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        match self {
125            Self::NoService => f.write_str("SCARD_E_NO_SERVICE"),
126            Self::NoReaders => f.write_str("SCARD_E_NO_READERS_AVAILABLE"),
127            Self::SharingViolation => f.write_str("SCARD_E_SHARING_VIOLATION"),
128            Self::RemovedCard => f.write_str("SCARD_W_REMOVED_CARD"),
129            Self::ResetCard => f.write_str("SCARD_W_RESET_CARD"),
130            Self::Other(name) => write!(f, "PC/SC {name}"),
131        }
132    }
133}
134
135/// A failure reported by the card or by PC/SC.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub enum CardError {
138    /// The card answered with a status word other than 9000.
139    Status { op: CardOp, sw: u16, ctx: ErrCtx },
140    /// PC/SC failed.
141    Pcsc { op: PcscOp, code: PcscCode },
142    /// The card's answer made no sense (too short, malformed, a
143    /// cryptographic check failed).
144    Protocol { op: CardOp, what: String },
145}
146
147impl CardError {
148    pub fn status(op: CardOp, sw1: u8, sw2: u8) -> Self {
149        Self::Status {
150            op,
151            sw: u16::from_be_bytes([sw1, sw2]),
152            ctx: ErrCtx::default(),
153        }
154    }
155
156    pub fn with_ctx(mut self, new_ctx: ErrCtx) -> Self {
157        if let Self::Status { ref mut ctx, .. } = self {
158            *ctx = new_ctx;
159        }
160        self
161    }
162
163    pub fn pcsc(op: PcscOp, e: pcsc::Error) -> Self {
164        Self::Pcsc { op, code: e.into() }
165    }
166
167    pub fn protocol(op: CardOp, what: impl Into<String>) -> Self {
168        Self::Protocol {
169            op,
170            what: what.into(),
171        }
172    }
173
174    /// The details line content: operation and code, no identifying data.
175    pub fn details(&self) -> String {
176        match self {
177            Self::Status { op, sw, .. } => format!("{} → SW {sw:04X}", op.describe()),
178            Self::Pcsc { op, code } => format!("{} → {code}", op.describe()),
179            Self::Protocol { op, what } => format!("{} → {what}", op.describe()),
180        }
181    }
182}
183
184impl fmt::Display for CardError {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        f.write_str(&self.details())
187    }
188}
189
190impl std::error::Error for CardError {}
191
192/// One of yb's own errors (not from the card), phrased for the user.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct YbError {
195    pub what: String,
196    pub why: Option<String>,
197    pub fix: Option<String>,
198}
199
200impl YbError {
201    pub fn new(what: impl Into<String>) -> Self {
202        Self {
203            what: what.into(),
204            why: None,
205            fix: None,
206        }
207    }
208
209    pub fn why(mut self, why: impl Into<String>) -> Self {
210        self.why = Some(why.into());
211        self
212    }
213
214    pub fn fix(mut self, fix: impl Into<String>) -> Self {
215        self.fix = Some(fix.into());
216        self
217    }
218}
219
220impl fmt::Display for YbError {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        f.write_str(&self.what)?;
223        if let Some(ref why) = self.why {
224            write!(f, " ({why})")?;
225        }
226        if let Some(ref fix) = self.fix {
227            write!(f, "; {fix}")?;
228        }
229        Ok(())
230    }
231}
232
233impl std::error::Error for YbError {}