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}