Skip to main content

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    /// A KDF operation could not be carried out. Surfaces HKDF
59    /// expansion-length overflows (output longer than `255 *
60    /// digest_size`), Argon2 parameter errors, and PHC-string parse
61    /// failures.
62    Kdf(&'static str),
63}
64
65/// Type alias for `core::result::Result<T, Error>`.
66pub type Result<T> = core::result::Result<T, Error>;
67
68impl fmt::Display for Error {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::InvalidKey { expected, actual } => {
72                write!(
73                    f,
74                    "invalid key length: expected {expected} bytes, got {actual}"
75                )
76            }
77            Self::InvalidCiphertext(why) => write!(f, "invalid ciphertext: {why}"),
78            Self::AuthenticationFailed => f.write_str("authentication failed"),
79            Self::AlgorithmNotEnabled(name) => {
80                write!(f, "algorithm not enabled at compile time: {name}")
81            }
82            Self::RandomFailure(why) => write!(f, "OS random source failed: {why}"),
83            Self::Mac(why) => write!(f, "MAC operation failed: {why}"),
84            Self::Kdf(why) => write!(f, "KDF operation failed: {why}"),
85        }
86    }
87}
88
89#[cfg(feature = "std")]
90impl std::error::Error for Error {}
91
92#[cfg(test)]
93#[allow(clippy::unwrap_used, clippy::expect_used)]
94mod tests {
95    use super::*;
96    use alloc::format;
97    use alloc::string::ToString;
98
99    #[test]
100    fn invalid_key_message_includes_lengths() {
101        let e = Error::InvalidKey {
102            expected: 32,
103            actual: 16,
104        };
105        let rendered = format!("{e}");
106        assert!(rendered.contains("32"));
107        assert!(rendered.contains("16"));
108    }
109
110    #[test]
111    fn authentication_failure_is_opaque() {
112        let rendered = Error::AuthenticationFailed.to_string();
113        assert_eq!(rendered, "authentication failed");
114    }
115
116    #[test]
117    fn debug_does_not_panic_for_any_variant() {
118        for e in [
119            Error::InvalidKey {
120                expected: 32,
121                actual: 0,
122            },
123            Error::InvalidCiphertext("x".to_string()),
124            Error::AuthenticationFailed,
125            Error::AlgorithmNotEnabled("none"),
126            Error::RandomFailure("ENOSYS"),
127            Error::Mac("init"),
128            Error::Kdf("expand"),
129        ] {
130            let _ = format!("{e:?}");
131        }
132    }
133}