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}