Skip to main content

age_crypto/errors/
encrypt.rs

1use std::io;
2use thiserror::Error;
3
4/// Errors that can happen during **encryption** operations.
5///
6/// # Overview
7///
8/// `EncryptError` enumerates every failure path specific to encryption:
9/// invalid recipient keys, missing recipients, internal encryption
10/// failures, and I/O glitches. It is the symmetric counterpart to
11/// [`DecryptError`] and follows the same design principles:
12///
13/// - **Fine‑grained** – callers can distinguish between "no recipients"
14///   and "invalid recipient X" and act accordingly (e.g., ask the user
15///   to provide a correct key).
16/// - **Context‑rich** – the error variants carry the problematic data
17///   (`recipient` field) and a `reason` string, so error messages are
18///   self‑contained and helpful.
19/// - **Ergonomic** – automatic `From` conversions mean minimal
20///   boilerplate inside the encryption functions.
21///
22/// All public encryption APIs return `crate::errors::Result<T>`, where
23/// `crate::errors::Error` can hold an `EncryptError` via the `#[from]`
24/// conversion.
25///
26/// # Example: Basic error handling with public API
27///
28/// ```rust
29/// use age_crypto::{encrypt_with_passphrase, Error};
30///
31/// let plaintext = b"Secret message";
32/// let passphrase = "my-strong-passphrase";
33///
34/// match encrypt_with_passphrase(plaintext, passphrase) {
35///     Ok(encrypted) => {
36///         println!("Encrypted {} bytes", encrypted.as_bytes().len());
37///     }
38///     Err(Error::Encrypt(e)) => {
39///         eprintln!("Encryption error: {}", e);
40///     }
41///     Err(e) => {
42///         eprintln!("Unexpected error: {}", e);
43///     }
44/// }
45/// ```
46///
47/// # Example: Distinguishing specific error variants
48///
49/// ```rust
50/// use age_crypto::{encrypt_with_passphrase, Error};
51/// use age_crypto::errors::EncryptError;
52///
53/// // Contoh: mencoba encrypt dengan passphrase kosong (mungkin gagal tergantung implementasi)
54/// let result = encrypt_with_passphrase(b"test", "");
55///
56/// if let Err(Error::Encrypt(err)) = result {
57///     match err {
58///         EncryptError::NoRecipients => {
59///             // Variant ini lebih relevan untuk key-based encryption
60///             eprintln!("No recipients specified");
61///         }
62///         EncryptError::InvalidRecipient { recipient, reason } => {
63///             eprintln!("Recipient '{}' is invalid: {}", recipient, reason);
64///         }
65///         EncryptError::Failed(msg) => {
66///             eprintln!("Encryption failed internally: {}", msg);
67///         }
68///         EncryptError::Io(e) => {
69///             eprintln!("I/O error during encryption: {}", e);
70///         }
71///     }
72/// }
73/// ```
74///
75/// # Error handling philosophy
76///
77/// - **No panics** – all error paths return a `Result`; the library never
78///   aborts due to invalid input.
79/// - **Transparent wrapping** – underlying library errors (from the `age`
80///   crate) are stringified with `.to_string()` so that the error chain
81///   remains meaningful even if the inner error type is not exposed.
82/// - **I/O errors are propagated automatically** – see [`Io`] variant.
83#[derive(Debug, Error)]
84pub enum EncryptError {
85    /// The list of recipients is empty.
86    ///
87    /// Age encryption requires at least one valid public key so that the
88    /// resulting ciphertext can be decrypted by the corresponding private
89    /// key. This error is returned immediately from the
90    /// `parse_recipients` helper when an empty slice is provided, before
91    /// any cryptographic work is done.
92    ///
93    /// **Why not just encrypt to "no one"?**  
94    /// That would produce a ciphertext that cannot be decrypted, which is
95    /// almost certainly a mistake. By treating this as an error, we force
96    /// the caller to consciously provide a recipient.
97    ///
98    /// # Example scenario
99    ///
100    /// ```rust
101    /// use age_crypto::errors::EncryptError;
102    ///
103    /// fn validate_recipients(recips: &[String]) -> Result<(), EncryptError> {
104    ///     if recips.is_empty() {
105    ///         return Err(EncryptError::NoRecipients);
106    ///     }
107    ///     Ok(())
108    /// }
109    ///
110    /// // Usage:
111    /// assert!(validate_recipients(&[]).is_err());
112    /// assert!(validate_recipients(&["age1valid...".into()]).is_ok());
113    /// ```
114    #[error("No recipients provided")]
115    NoRecipients,
116
117    /// A specific recipient string could not be parsed as a valid X25519
118    /// public key.
119    ///
120    /// Age public keys normally look like `age1...` followed by a Bech32
121    /// string. This variant is returned when `x25519::Recipient::from_str`
122    /// fails. The fields are:
123    ///
124    /// - `recipient`: the original string that failed to parse.
125    /// - `reason`: the parser's error description (e.g., "invalid bech32",
126    ///   "bad checksum", "unsupported version").
127    ///
128    /// By including the offending string, the caller can report exactly
129    /// which recipient was problematic without needing to copy it into
130    /// the error themselves.
131    ///
132    /// # Example: Parsing validation
133    ///
134    /// ```rust
135    /// use std::str::FromStr;
136    /// use age::x25519::Recipient;
137    /// use age_crypto::errors::EncryptError;
138    ///
139    /// # fn example() -> Result<(), EncryptError> {
140    /// let key = "age1-invalid-key-format";
141    ///
142    /// // Attempt to parse; on failure, convert to our error type
143    /// Recipient::from_str(key)
144    ///     .map_err(|e| EncryptError::InvalidRecipient {
145    ///         recipient: key.to_string(),
146    ///         reason: format!("Parse error: {}", e),
147    ///     })?;
148    /// # Ok(())
149    /// # }
150    /// ```
151    ///
152    /// # Example: Error output format
153    ///
154    /// ```text
155    /// recipients = ["age1valid", "not-a-key", "age1another"]
156    /// -> EncryptError::InvalidRecipient {
157    ///        recipient: "not-a-key",
158    ///        reason: "invalid bech32 checksum"
159    ///    }
160    /// -> Display: "Invalid recipient 'not-a-key': invalid bech32 checksum"
161    /// ```
162    #[error("Invalid recipient '{recipient}': {reason}")]
163    InvalidRecipient {
164        /// The original string that was supposed to be an age public key.
165        recipient: String,
166        /// The explanation from the parser about why it is invalid.
167        reason: String,
168    },
169
170    /// The encryption process encountered an internal error.
171    ///
172    /// This variant covers all age‑internal failures that are not related
173    /// to recipient parsing or I/O. For example:
174    ///
175    /// - Failure to generate a shared secret from a recipient's key.
176    /// - Failure to wrap the symmetric key.
177    /// - Unexpected errors from the random number generator.
178    ///
179    /// The inner [`String`] contains whatever error message the `age`
180    /// crate produced. While this is not as structured as the other
181    /// variants, it ensures no error is silently swallowed.
182    ///
183    /// # When this occurs
184    ///
185    /// This error is relatively rare in practice because the `age` crate
186    /// is well-tested. Common triggers include:
187    ///
188    /// - Memory allocation failures during cryptographic operations
189    /// - Unexpected state in the encryption state machine
190    /// - Platform-specific cryptographic backend issues
191    ///
192    /// # Debugging tip
193    ///
194    /// If you encounter this error frequently, consider:
195    ///
196    /// 1. Updating the `age` and `age-crypto` crates to latest versions
197    /// 2. Checking system resources (memory, entropy pool)
198    /// 3. Reporting the issue with the full error message for investigation
199    #[error("Encryption failed: {0}")]
200    Failed(String),
201
202    /// An I/O error occurred while writing the encrypted output.
203    ///
204    /// Even though our API writes into a `Vec<u8>`, the `Encryptor` uses
205    /// a generic `Write` implementation. In extremely rare circumstances
206    /// (e.g., out‑of‑memory), writing to the vector may yield an
207    /// [`io::Error`].
208    ///
209    /// Because this variant is annotated with `#[from] io::Error`, any
210    /// `?` on an I/O operation inside an encryption function will
211    /// automatically promote the `io::Error` into `EncryptError::Io`.
212    ///
213    /// # Example of automatic I/O error conversion
214    ///
215    /// ```rust
216    /// use std::io::Write;
217    /// use age_crypto::errors::EncryptError;
218    ///
219    /// fn write_encrypted<W: Write>(
220    ///     writer: &mut W,
221    ///     plaintext: &[u8]
222    /// ) -> Result<(), EncryptError> {
223    ///     // Any io::Error from write_all is automatically converted
224    ///     // to EncryptError::Io via the `?` operator
225    ///     writer.write_all(plaintext)?;
226    ///     Ok(())
227    /// }
228    /// ```
229    ///
230    /// # Common I/O error scenarios
231    ///
232    /// | ErrorKind | Likely cause |
233    /// |-----------|-------------|
234    /// | `WriteZero` | Writer returned 0 bytes written (unusual for Vec) |
235    /// | `OutOfMemory` | System ran out of memory during allocation |
236    /// | `Interrupted` | Operation interrupted by signal (rare in memory ops) |
237    #[error("I/O error: {0}")]
238    Io(#[from] io::Error),
239}
240
241// ============================================================================
242// HELPER METHODS (optional but useful for error inspection)
243// ============================================================================
244impl EncryptError {
245    /// Returns `true` if this error indicates a user-correctable issue
246    /// (e.g., invalid recipient format) rather than an internal failure.
247    ///
248    /// This can help decide whether to prompt the user to retry with
249    /// different input.
250    ///
251    /// # Example
252    ///
253    /// ```rust
254    /// use age_crypto::errors::EncryptError;
255    ///
256    /// let err = EncryptError::InvalidRecipient {
257    ///     recipient: "bad-key".into(),
258    ///     reason: "invalid bech32".into(),
259    /// };
260    ///
261    /// if err.is_user_correctable() {
262    ///     println!("Please check your recipient key and try again.");
263    /// }
264    /// ```
265    #[must_use]
266    pub fn is_user_correctable(&self) -> bool {
267        matches!(
268            self,
269            EncryptError::NoRecipients | EncryptError::InvalidRecipient { .. }
270        )
271    }
272
273    /// Returns the problematic recipient string if this is an
274    /// `InvalidRecipient` error, or `None` otherwise.
275    ///
276    /// # Example
277    ///
278    /// ```rust
279    /// use age_crypto::errors::EncryptError;
280    ///
281    /// let err = EncryptError::InvalidRecipient {
282    ///     recipient: "age1bad...".into(),
283    ///     reason: "checksum failed".into(),
284    /// };
285    ///
286    /// if let Some(bad_key) = err.invalid_recipient() {
287    ///     eprintln!("The key '{}' is invalid", bad_key);
288    /// }
289    /// ```
290    #[must_use]
291    pub fn invalid_recipient(&self) -> Option<&str> {
292        match self {
293            EncryptError::InvalidRecipient { recipient, .. } => Some(recipient),
294            _ => None,
295        }
296    }
297}
298
299// ============================================================================
300// UNIT TESTS (comprehensive coverage for EncryptError)
301// ============================================================================
302#[cfg(test)]
303mod tests {
304    use super::*;
305    use std::io::{self, ErrorKind};
306
307    // ────────────────────────────────────────────────────────────────
308    // Display trait tests
309    // ────────────────────────────────────────────────────────────────
310    #[test]
311    fn test_no_recipients_display() {
312        let err = EncryptError::NoRecipients;
313        assert_eq!(format!("{}", err), "No recipients provided");
314    }
315
316    #[test]
317    fn test_invalid_recipient_display() {
318        let err = EncryptError::InvalidRecipient {
319            recipient: "age1badkey".into(),
320            reason: "invalid checksum".into(),
321        };
322        assert_eq!(
323            format!("{}", err),
324            "Invalid recipient 'age1badkey': invalid checksum"
325        );
326    }
327
328    #[test]
329    fn test_failed_display() {
330        let err = EncryptError::Failed("internal crypto error".into());
331        assert_eq!(
332            format!("{}", err),
333            "Encryption failed: internal crypto error"
334        );
335    }
336
337    #[test]
338    fn test_io_error_display() {
339        let io_err = io::Error::new(ErrorKind::OutOfMemory, "alloc failed");
340        let err = EncryptError::Io(io_err);
341        assert_eq!(format!("{}", err), "I/O error: alloc failed");
342    }
343
344    // ────────────────────────────────────────────────────────────────
345    // From<io::Error> conversion tests
346    // ────────────────────────────────────────────────────────────────
347    #[test]
348    fn test_from_io_error_conversion() {
349        let io_err: io::Error = ErrorKind::PermissionDenied.into();
350        let encrypt_err: EncryptError = io_err.into();
351        assert!(matches!(encrypt_err, EncryptError::Io(_)));
352    }
353
354    #[test]
355    fn test_io_error_preserves_kind() {
356        let io_err = io::Error::new(ErrorKind::UnexpectedEof, "test");
357        let encrypt_err = EncryptError::Io(io_err);
358
359        if let EncryptError::Io(e) = encrypt_err {
360            assert_eq!(e.kind(), ErrorKind::UnexpectedEof);
361        } else {
362            panic!("Expected Io variant");
363        }
364    }
365
366    // ────────────────────────────────────────────────────────────────
367    // Helper method tests
368    // ────────────────────────────────────────────────────────────────
369    #[test]
370    fn test_is_user_correctable_true() {
371        assert!(EncryptError::NoRecipients.is_user_correctable());
372
373        let invalid = EncryptError::InvalidRecipient {
374            recipient: "bad".into(),
375            reason: "x".into(),
376        };
377        assert!(invalid.is_user_correctable());
378    }
379
380    #[test]
381    fn test_is_user_correctable_false() {
382        assert!(!EncryptError::Failed("x".into()).is_user_correctable());
383        assert!(!EncryptError::Io(io::Error::last_os_error()).is_user_correctable());
384    }
385
386    #[test]
387    fn test_invalid_recipient_some() {
388        let err = EncryptError::InvalidRecipient {
389            recipient: "age1test".into(),
390            reason: "bad".into(),
391        };
392        assert_eq!(err.invalid_recipient(), Some("age1test"));
393    }
394
395    #[test]
396    fn test_invalid_recipient_none() {
397        assert_eq!(EncryptError::NoRecipients.invalid_recipient(), None);
398        assert_eq!(EncryptError::Failed("x".into()).invalid_recipient(), None);
399    }
400
401    // ────────────────────────────────────────────────────────────────
402    // Type safety & trait tests
403    // ────────────────────────────────────────────────────────────────
404    #[test]
405    fn test_error_is_send_sync() {
406        fn assert_send_sync<T: Send + Sync>() {}
407        assert_send_sync::<EncryptError>();
408    }
409
410    #[test]
411    fn test_error_implements_std_error() {
412        fn assert_error<T: std::error::Error>() {}
413        assert_error::<EncryptError>();
414    }
415
416    #[test]
417    fn test_error_source_chain() {
418        use std::error::Error as StdError;
419
420        let io_err = io::Error::new(ErrorKind::Other, "underlying cause");
421        let encrypt_err = EncryptError::Io(io_err);
422
423        assert!(encrypt_err.source().is_some());
424    }
425
426    #[test]
427    fn test_debug_format_contains_variant_name() {
428        let err = EncryptError::InvalidRecipient {
429            recipient: "key".into(),
430            reason: "bad".into(),
431        };
432        let debug = format!("{:?}", err);
433        assert!(debug.contains("InvalidRecipient"));
434        assert!(debug.contains("key"));
435    }
436}