Skip to main content

ic_core/
error.rs

1//! The single error domain for the whole library.
2
3use core::fmt;
4
5/// Result alias used by every fallible operation in IronCrypto.
6pub type Result<T> = core::result::Result<T, Error>;
7
8/// Machine-actionable classification of a failure.
9///
10/// The discriminants are stable and are mirrored verbatim into the ontology
11/// (`ic-ontology::error_catalog`) so an agent can reason about recovery
12/// strategy without parsing English prose.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[non_exhaustive]
15pub enum ErrorKind {
16    /// A buffer was too short or too long for the algorithm's contract.
17    InvalidLength,
18    /// A key, nonce, or parameter was structurally unacceptable.
19    InvalidParameter,
20    /// Authentication (MAC / AEAD tag / signature) failed to verify.
21    AuthenticationFailed,
22    /// The requested algorithm exists but is not implemented in this build.
23    Unsupported,
24    /// The operation is not permitted while the module is in FIPS approved mode.
25    NotApprovedInFipsMode,
26    /// A FIPS 140-3 self-test failed; the module has entered the error state.
27    SelfTestFailed,
28    /// The module is in a hard error state and refuses all cryptographic service.
29    ModuleErrorState,
30    /// The entropy source failed or did not pass its health tests.
31    EntropyFailure,
32    /// A counter (DRBG reseed, GCM invocation, sequence number) was exhausted.
33    CounterExhausted,
34    /// Input could not be decoded (hex, base64, DER, point encoding).
35    MalformedEncoding,
36    /// An internal invariant was violated — always a library bug.
37    Internal,
38}
39
40impl ErrorKind {
41    /// Every kind, for callers that enumerate them.
42    ///
43    /// This type is `#[non_exhaustive]`, so no other crate can match on it
44    /// exhaustively and none can tell whether it has seen them all. That is
45    /// deliberate — it lets a variant be added without breaking callers — but
46    /// it also means a list like the ontology's error catalog cannot check its
47    /// own completeness. This crate can, so the list is published from here and
48    /// a test keeps it honest.
49    pub const ALL: &'static [ErrorKind] = &[
50        ErrorKind::InvalidLength,
51        ErrorKind::InvalidParameter,
52        ErrorKind::AuthenticationFailed,
53        ErrorKind::MalformedEncoding,
54        ErrorKind::Unsupported,
55        ErrorKind::NotApprovedInFipsMode,
56        ErrorKind::SelfTestFailed,
57        ErrorKind::ModuleErrorState,
58        ErrorKind::EntropyFailure,
59        ErrorKind::CounterExhausted,
60        ErrorKind::Internal,
61    ];
62
63    /// Stable kebab-case identifier used in ontology exports and CLI/MCP output.
64    pub const fn id(self) -> &'static str {
65        match self {
66            Self::InvalidLength => "invalid-length",
67            Self::InvalidParameter => "invalid-parameter",
68            Self::AuthenticationFailed => "authentication-failed",
69            Self::Unsupported => "unsupported",
70            Self::NotApprovedInFipsMode => "not-approved-in-fips-mode",
71            Self::SelfTestFailed => "self-test-failed",
72            Self::ModuleErrorState => "module-error-state",
73            Self::EntropyFailure => "entropy-failure",
74            Self::CounterExhausted => "counter-exhausted",
75            Self::MalformedEncoding => "malformed-encoding",
76            Self::Internal => "internal",
77        }
78    }
79
80    /// Whether retrying the identical call could plausibly succeed.
81    ///
82    /// Agents use this to decide between *retry*, *re-parameterize*, and *abort*.
83    #[must_use]
84    pub const fn retryable(self) -> bool {
85        matches!(self, Self::EntropyFailure)
86    }
87
88    /// Whether the caller should change inputs and try again.
89    #[must_use]
90    pub const fn caller_correctable(self) -> bool {
91        matches!(
92            self,
93            Self::InvalidLength
94                | Self::InvalidParameter
95                | Self::MalformedEncoding
96                | Self::Unsupported
97                | Self::NotApprovedInFipsMode
98                | Self::CounterExhausted
99        )
100    }
101}
102
103/// A failure from any IronCrypto operation.
104///
105/// Deliberately opaque about *why* an authentication check failed — the
106/// [`ErrorKind`] is the only distinguishing datum for verification failures, so
107/// error handling cannot become a decryption oracle.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct Error {
110    kind: ErrorKind,
111    context: &'static str,
112}
113
114impl Error {
115    /// Construct an error with a static context string (an algorithm or field name).
116    pub const fn new(kind: ErrorKind, context: &'static str) -> Self {
117        Self { kind, context }
118    }
119
120    /// The machine-actionable classification.
121    pub const fn kind(&self) -> ErrorKind {
122        self.kind
123    }
124
125    /// A short static hint naming the failing parameter or component.
126    pub const fn context(&self) -> &'static str {
127        self.context
128    }
129}
130
131impl fmt::Display for Error {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}: {}", self.kind.id(), self.context)
134    }
135}
136
137#[cfg(feature = "std")]
138impl std::error::Error for Error {}
139
140/// Shorthand for building an [`Error`].
141#[macro_export]
142macro_rules! err {
143    ($kind:ident, $ctx:literal) => {
144        $crate::Error::new($crate::ErrorKind::$kind, $ctx)
145    };
146}
147
148/// Shorthand for `return Err(err!(..))` guarded by a condition.
149#[macro_export]
150macro_rules! ensure {
151    ($cond:expr, $kind:ident, $ctx:literal) => {
152        if !($cond) {
153            return Err($crate::Error::new($crate::ErrorKind::$kind, $ctx));
154        }
155    };
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    /// `ALL` must really be all of them.
163    ///
164    /// The match below is exhaustive, and this is the crate that defines the
165    /// type, so `#[non_exhaustive]` does not apply here and adding a variant
166    /// stops this compiling until it is handled. Requiring `ALL` to contain
167    /// each one is what turns "the compiler noticed" into "the list was
168    /// updated".
169    #[test]
170    fn the_variant_list_is_complete() {
171        // The match is a no-op by construction, and that is the point: it
172        // exists so the compiler refuses this file when a variant is added,
173        // not to compute anything. Clippy is right that it does nothing and
174        // wrong that it is therefore unnecessary.
175        #[allow(clippy::needless_match)]
176        fn identify(kind: ErrorKind) -> ErrorKind {
177            match kind {
178                ErrorKind::InvalidLength => ErrorKind::InvalidLength,
179                ErrorKind::InvalidParameter => ErrorKind::InvalidParameter,
180                ErrorKind::AuthenticationFailed => ErrorKind::AuthenticationFailed,
181                ErrorKind::MalformedEncoding => ErrorKind::MalformedEncoding,
182                ErrorKind::Unsupported => ErrorKind::Unsupported,
183                ErrorKind::NotApprovedInFipsMode => ErrorKind::NotApprovedInFipsMode,
184                ErrorKind::SelfTestFailed => ErrorKind::SelfTestFailed,
185                ErrorKind::ModuleErrorState => ErrorKind::ModuleErrorState,
186                ErrorKind::EntropyFailure => ErrorKind::EntropyFailure,
187                ErrorKind::CounterExhausted => ErrorKind::CounterExhausted,
188                ErrorKind::Internal => ErrorKind::Internal,
189            }
190        }
191
192        // The match catches a variant being added: it stops compiling until
193        // the new one is handled, and handling it means editing the list right
194        // there. It does not catch one being dropped from `ALL`, because a
195        // shorter list still maps each of its members to itself. This count
196        // sits beside the match so the two are edited together.
197        assert_eq!(ErrorKind::ALL.len(), 11);
198
199        for kind in ErrorKind::ALL {
200            assert_eq!(identify(*kind), *kind);
201        }
202
203        // Identifiers are the join key the ontology matches on, so a duplicate
204        // would make two kinds indistinguishable to every consumer. Compared
205        // pairwise rather than collected, since this crate has no allocator.
206        for (i, a) in ErrorKind::ALL.iter().enumerate() {
207            for b in ErrorKind::ALL.iter().skip(i + 1) {
208                assert_ne!(a.id(), b.id(), "two kinds share an identifier");
209            }
210            assert!(!a.id().is_empty());
211        }
212    }
213}