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}