crypt_io/error.rs
1//! Error types for `crypt-io`.
2//!
3//! All fallible operations in the crate return [`Result<T>`], an alias for
4//! `core::result::Result<T, Error>`. [`Error`] is `#[non_exhaustive]` — match
5//! sites must include a wildcard arm so future minor releases can add
6//! variants without breaking downstream code.
7//!
8//! Error messages are redaction-clean: no key bytes, no plaintext, no nonce
9//! values, no ciphertext are ever included in a rendered error. Errors are
10//! safe to log, safe to ship to monitoring, safe to include in audit records.
11
12use alloc::string::String;
13use core::fmt;
14
15/// The error type for all `crypt-io` operations.
16///
17/// Authentication failures are deliberately collapsed into a single variant —
18/// distinguishing "wrong key" from "tampered ciphertext" would leak which
19/// failure mode an attacker is closer to, which is a side-channel.
20#[non_exhaustive]
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum Error {
23 /// The supplied key was not the correct size for the selected algorithm
24 /// (ChaCha20-Poly1305 and AES-256-GCM both require exactly 32 bytes).
25 InvalidKey {
26 /// The expected key length in bytes.
27 expected: usize,
28 /// The length actually supplied.
29 actual: usize,
30 },
31
32 /// The ciphertext was malformed (too short to contain a nonce + tag, or
33 /// the embedded length fields were inconsistent).
34 InvalidCiphertext(String),
35
36 /// Authentication of the ciphertext failed. This is the single
37 /// observable outcome of *any* corruption: wrong key, tampered bytes,
38 /// truncated message, or wrong associated data. The variant is opaque
39 /// by design.
40 AuthenticationFailed,
41
42 /// The requested algorithm is not enabled at compile time. Re-build
43 /// with the appropriate Cargo feature.
44 AlgorithmNotEnabled(&'static str),
45
46 /// The OS random source failed to produce a nonce. This is rare and
47 /// almost always indicates a misconfigured sandbox or exhausted
48 /// `getrandom` entropy on a freshly-booted VM.
49 RandomFailure(&'static str),
50
51 /// A MAC operation could not be initialised or computed. In practice
52 /// this is unreachable for the algorithms shipped (HMAC accepts any
53 /// key length; BLAKE3 keyed takes a fixed-size key), but the variant
54 /// exists because the upstream `Mac` trait surface is fallible by
55 /// signature.
56 Mac(&'static str),
57}
58
59/// Type alias for `core::result::Result<T, Error>`.
60pub type Result<T> = core::result::Result<T, Error>;
61
62impl fmt::Display for Error {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 match self {
65 Self::InvalidKey { expected, actual } => {
66 write!(
67 f,
68 "invalid key length: expected {expected} bytes, got {actual}"
69 )
70 }
71 Self::InvalidCiphertext(why) => write!(f, "invalid ciphertext: {why}"),
72 Self::AuthenticationFailed => f.write_str("authentication failed"),
73 Self::AlgorithmNotEnabled(name) => {
74 write!(f, "algorithm not enabled at compile time: {name}")
75 }
76 Self::RandomFailure(why) => write!(f, "OS random source failed: {why}"),
77 Self::Mac(why) => write!(f, "MAC operation failed: {why}"),
78 }
79 }
80}
81
82#[cfg(feature = "std")]
83impl std::error::Error for Error {}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::expect_used)]
87mod tests {
88 use super::*;
89 use alloc::format;
90 use alloc::string::ToString;
91
92 #[test]
93 fn invalid_key_message_includes_lengths() {
94 let e = Error::InvalidKey {
95 expected: 32,
96 actual: 16,
97 };
98 let rendered = format!("{e}");
99 assert!(rendered.contains("32"));
100 assert!(rendered.contains("16"));
101 }
102
103 #[test]
104 fn authentication_failure_is_opaque() {
105 let rendered = Error::AuthenticationFailed.to_string();
106 assert_eq!(rendered, "authentication failed");
107 }
108
109 #[test]
110 fn debug_does_not_panic_for_any_variant() {
111 for e in [
112 Error::InvalidKey {
113 expected: 32,
114 actual: 0,
115 },
116 Error::InvalidCiphertext("x".to_string()),
117 Error::AuthenticationFailed,
118 Error::AlgorithmNotEnabled("none"),
119 Error::RandomFailure("ENOSYS"),
120 Error::Mac("init"),
121 ] {
122 let _ = format!("{e:?}");
123 }
124 }
125}