Skip to main content

age_crypto/errors/
decrypt.rs

1use std::io;
2use thiserror::Error;
3
4/// Errors that can occur during **decryption** operations.
5///
6/// # Overview
7///
8/// This enum represents every possible failure mode that the decryption
9/// functions may encounter. It is designed to be:
10///
11/// - **Specific** – each variant describes a distinct category of failure.
12/// - **Informative** – the attached data (strings, etc.) provide precise
13///   diagnostics, so callers can either display an error message or
14///   programmatically react (e.g., retry with a different identity).
15/// - **Composable** – it implements [`std::error::Error`] and can be
16///   seamlessly converted into the crate‑level [`Error`](super::Error)
17///   (which further wraps both encryption and decryption errors).
18///
19/// The enum is derived with [`thiserror::Error`], which automatically
20/// implements `Display` and `Error` based on the `#[error("...")]`
21/// attributes. This avoids boilerplate while keeping the error messages
22/// clear and greppable.
23///
24/// # Where it appears
25///
26/// Every public decryption API (e.g., [`crate::decrypt_with_passphrase`],
27/// [`crate::decrypt_with_passphrase_armor`]) returns
28/// `crate::errors::Result<T>`, which under the hood is
29/// `std::result::Result<T, crate::errors::Error>`.  
30/// The crate‑level [`Error`](super::Error) has a variant `Decrypt` that
31/// automatically converts from `DecryptError` via `#[from]`. Thus, when a
32/// decryption helper fails with a `DecryptError`, the `?` operator promotes
33/// it to the top‑level error type without manual `.map_err(...)`.
34///
35/// # Example: Basic error handling
36///
37/// ```rust
38/// use age_crypto::{decrypt_with_passphrase, Error};
39///
40/// let ciphertext = b"AGE-ENC..."; // dummy ciphertext for demo
41/// let passphrase = "my-secret";
42///
43/// match decrypt_with_passphrase(ciphertext, passphrase) {
44///     Ok(plaintext) => println!("Success: {} bytes", plaintext.len()),
45///     Err(Error::Decrypt(e)) => eprintln!("Decrypt error: {}", e),
46///     Err(e) => eprintln!("Unexpected error: {}", e),
47/// }
48/// ```
49///
50/// # Example: Distinguishing error variants
51///
52/// ```rust
53/// use age_crypto::{decrypt_with_passphrase, Error};
54/// use age_crypto::errors::DecryptError;
55///
56/// let bad_ct = b"not-valid-age-data";
57/// let result = decrypt_with_passphrase(bad_ct, "any-pass");
58///
59/// if let Err(Error::Decrypt(err)) = result {
60///     match err {
61///         DecryptError::InvalidCiphertext(_) => {
62///             eprintln!("Ciphertext is malformed or corrupted");
63///         }
64///         DecryptError::Failed(_) => {
65///             eprintln!("Wrong passphrase or integrity check failed");
66///         }
67///         DecryptError::Io(e) => {
68///             eprintln!("I/O error during decryption: {}", e);
69///         }
70///         DecryptError::InvalidIdentity(_) => {
71///             // This variant is more relevant for key-based decryption
72///             eprintln!("Identity parsing issue");
73///         }
74///     }
75/// }
76/// ```
77///
78/// # Error handling philosophy
79///
80/// - **No panics** – all error paths return a `Result`; the library never
81///   aborts due to invalid input.
82/// - **Transparent wrapping** – underlying library errors (from the `age`
83///   crate) are stringified with `.to_string()` so that the error chain
84///   remains meaningful even if the inner error type is not exposed.
85/// - **I/O errors are propagated automatically** – see [`Io`] variant.
86#[derive(Debug, Error)]
87pub enum DecryptError {
88    /// The identity (secret key) provided is malformed or cannot be parsed.
89    ///
90    /// Decryption requires a valid `age` identity, typically an
91    /// `AGE-SECRET-KEY-1...` string (X25519 identity). This variant is
92    /// returned when `age::x25519::Identity::from_str` fails. The inner
93    /// [`String`] contains the parser's error message, e.g.:
94    ///
95    /// * "invalid bech32 checksum"
96    /// * "unknown version"
97    /// * "unexpected length"
98    ///
99    /// **Why not just return the raw parse error?**  
100    /// The `age` crate does not expose a structured error type for parsing;
101    /// it returns a `Box<dyn std::error::Error>`. We stringify it so that
102    /// our error type remains concrete, `Send + Sync`, and easy to display.
103    ///
104    /// # Example scenario
105    ///
106    /// ```rust
107    /// use std::str::FromStr;
108    /// use age::x25519::Identity;
109    /// use age_crypto::errors::DecryptError;
110    ///
111    /// # fn example() -> Result<(), DecryptError> {
112    /// let key = "AGE-SECRET-KEY-INVALID-EXAMPLE";
113    /// let identity = Identity::from_str(key)
114    ///     .map_err(|e| DecryptError::InvalidIdentity(format!("Parse error: {}", e)))?;
115    /// # Ok(())
116    /// # }
117    /// ```
118    #[error("Invalid identity: {0}")]
119    InvalidIdentity(String),
120
121    /// The ciphertext (encrypted data) is not a valid age-encrypted stream.
122    ///
123    /// This error occurs when `age::Decryptor::new` fails to parse the
124    /// beginning of the ciphertext. The age format starts with a version
125    /// tag and contains header records; if that structure is corrupted,
126    /// truncated, or simply not an age file, this variant is returned.
127    ///
128    /// The string contains the exact reason from the `age` crate, which
129    /// can help developers distinguish between:
130    ///
131    /// * "header is too short" (truncated file)
132    /// * "unknown version" (future or incompatible format)
133    /// * MAC verification failure at the header level
134    ///
135    /// **Important:** This error is raised *before* any decryption attempt.
136    /// It signals a structural problem, not a wrong key.
137    #[error("Invalid ciphertext: {0}")]
138    InvalidCiphertext(String),
139
140    /// Decryption itself failed — the identity is not suitable for this
141    /// ciphertext, or the data is corrupted beyond simple format errors.
142    ///
143    /// This is a catch‑all for when the decryptor is successfully
144    /// constructed but the actual key exchange or symmetric decryption
145    /// fails. Reasons include:
146    ///
147    /// * No recipient stanza matches the provided identity
148    ///   (e.g., you encrypted to Alice's key but provided Bob's identity).
149    /// * HMAC verification fails (tampered ciphertext).
150    /// * A scrypt passphrase-based identity is wrong.
151    ///
152    /// The inner [`String`] contains the lower‑level error description
153    /// from `age`.
154    ///
155    /// **Design note:** In a later version, we might split this into more
156    /// specific variants (e.g., `WrongKey`, `TamperedData`), but for now
157    /// a single `Failed` keeps the API simple while still providing the
158    /// error message.
159    #[error("Decryption failed: {0}")]
160    Failed(String),
161
162    /// An I/O error occurred while reading or writing during decryption.
163    ///
164    /// Even though our high‑level API operates entirely in memory (using
165    /// `Vec<u8>` or `&[u8]`), the underlying `age` library works with
166    /// generic `Read`/`Write` traits. Therefore, it can theoretically
167    /// produce an [`io::Error`] (e.g., if the in‑memory stream encounters
168    /// an allocation failure, though rare).
169    ///
170    /// This variant implements `From<io::Error>` via the `#[from]`
171    /// attribute, which means you can use the `?` operator directly
172    /// on any I/O operation inside a function that returns
173    /// `Result<_, DecryptError>`. The conversion is lossless – the
174    /// original `io::Error` is stored inside and can be recovered.
175    ///
176    /// # Example of automatic I/O error conversion
177    ///
178    /// ```rust
179    /// use std::io::Read;
180    /// use age_crypto::errors::DecryptError;
181    ///
182    /// fn read_all<R: Read>(stream: &mut R) -> Result<Vec<u8>, DecryptError> {
183    ///     let mut buf = Vec::new();
184    ///     // io::Error is automatically converted to DecryptError::Io via `?`
185    ///     stream.read_to_end(&mut buf)?;
186    ///     Ok(buf)
187    /// }
188    /// ```
189    #[error("I/O error: {0}")]
190    Io(#[from] io::Error),
191}
192
193// ============================================================================
194// UNIT TESTS (for DecryptError itself)
195// ============================================================================
196#[cfg(test)]
197mod tests {
198    use super::*;
199    use std::io::{self, ErrorKind};
200
201    #[test]
202    fn test_invalid_identity_display() {
203        let err = DecryptError::InvalidIdentity("bad bech32".into());
204        assert_eq!(format!("{}", err), "Invalid identity: bad bech32");
205    }
206
207    #[test]
208    fn test_invalid_ciphertext_display() {
209        let err = DecryptError::InvalidCiphertext("header too short".into());
210        assert_eq!(format!("{}", err), "Invalid ciphertext: header too short");
211    }
212
213    #[test]
214    fn test_failed_display() {
215        let err = DecryptError::Failed("wrong key".into());
216        assert_eq!(format!("{}", err), "Decryption failed: wrong key");
217    }
218
219    #[test]
220    fn test_io_error_display() {
221        let io_err = io::Error::new(ErrorKind::UnexpectedEof, "stream ended");
222        let err = DecryptError::Io(io_err);
223        assert_eq!(format!("{}", err), "I/O error: stream ended");
224    }
225
226    #[test]
227    fn test_from_io_error_conversion() {
228        let io_err: io::Error = ErrorKind::PermissionDenied.into();
229        let decrypt_err: DecryptError = io_err.into();
230        assert!(matches!(decrypt_err, DecryptError::Io(_)));
231    }
232
233    #[test]
234    fn test_error_is_send_sync() {
235        fn assert_send_sync<T: Send + Sync>() {}
236        assert_send_sync::<DecryptError>();
237    }
238
239    #[test]
240    fn test_error_source_chain() {
241        use std::error::Error as StdError;
242        let io_err = io::Error::new(ErrorKind::Other, "underlying");
243        let decrypt_err = DecryptError::Io(io_err);
244        assert!(decrypt_err.source().is_some());
245    }
246}