Skip to main content

origin_crypto_sdk/
error.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Unified error type for cryptographic operations.
4//!
5//! All fallible operations in this crate return [`Result<T>`](Result),
6//! which is `std::result::Result<T, CryptoError>`.
7//!
8//! # Error categories
9//!
10//! | Variant | When it occurs |
11//! |---------|---------------|
12//! | [`CryptoError::InvalidKeyLength`] | Key bytes are wrong size for the algorithm |
13//! | [`CryptoError::InvalidKey`] | Key is malformed or fails validation |
14//! | [`CryptoError::InvalidNonceLength`] | Nonce bytes are wrong size |
15//! | [`CryptoError::AuthenticationFailed`] | Signature or AEAD tag verification failed |
16//! | [`CryptoError::Encryption`] | Encryption failed (rare — usually a bug) |
17//! | [`CryptoError::Decryption`] | Decryption failed (ciphertext tampered, wrong key, etc.) |
18//! | [`CryptoError::Kdf`] | Key derivation failed (bad params, Argon2 failure, etc.) |
19//! | [`CryptoError::Pqc`] | Post-quantum primitive failed |
20//! | [`CryptoError::ReedSolomon`] | Error correction encode/decode failed |
21//! | [`CryptoError::InvalidParameter`] | Invalid input parameter (empty seed, expired handle, etc.) |
22//! | [`CryptoError::Io`] | Underlying I/O error |
23//! | [`CryptoError::Serialization`] | Serialization or deserialization failed |
24//! | [`CryptoError::Compression`] | Compression or decompression failed |
25
26use std::fmt;
27
28/// Unified error type for cryptographic operations.
29///
30/// See [module-level documentation](self) for a description of each variant.
31#[derive(Debug)]
32pub enum CryptoError {
33    // ── Key / nonce errors (structured) ──
34    /// Key bytes are the wrong length for the algorithm.
35    ///
36    /// Example: passed 16 bytes to a function expecting a 32-byte key.
37    InvalidKeyLength {
38        /// Name of the algorithm (e.g. "Ed25519", "Falcon-1024").
39        algorithm: &'static str,
40        /// Required length in bytes.
41        expected: usize,
42        /// Actual length received.
43        got: usize,
44    },
45
46    /// Key is structurally valid in length but fails algorithmic validation.
47    ///
48    /// Example: Ed25519 public key does not encode a valid curve point,
49    /// or Falcon secret key fails deserialization.
50    InvalidKey(String),
51
52    /// Nonce bytes are the wrong length.
53    InvalidNonceLength {
54        /// Name of the algorithm (e.g. "XChaCha20-Poly1305").
55        algorithm: &'static str,
56        /// Required nonce length in bytes.
57        expected: usize,
58        /// Actual length received.
59        got: usize,
60    },
61
62    // ── Verification failures ──
63    /// Authentication tag or signature verification failed.
64    ///
65    /// This is the normal "wrong key / tampered data" error. No further
66    /// detail is given to avoid oracle attacks.
67    AuthenticationFailed,
68
69    // ── Operation failures (string detail) ──
70    /// Encryption failed.
71    Encryption(String),
72
73    /// Decryption failed (wrong key, tampered ciphertext, etc.).
74    Decryption(String),
75
76    /// Key derivation failed.
77    Kdf(String),
78
79    /// Post-quantum primitive operation failed.
80    Pqc(String),
81
82    /// Reed-Solomon encode/decode error.
83    ReedSolomon(String),
84
85    /// Invalid input parameter.
86    InvalidParameter(String),
87
88    /// Serialization or deserialization error.
89    Serialization(String),
90
91    /// Compression or decompression error.
92    Compression(String),
93
94    /// Underlying I/O error.
95    Io(std::io::Error),
96
97    // ── Recovery phrase (unicode_cipher) errors ──
98    /// Recovery phrase truncated-SHA-3-256 checksum did not match the
99    /// encoded entropy.
100    ChecksumMismatch {
101        /// Expected checksum bits (`SHA3-256(entropy)[..n_bits]`).
102        expected: u32,
103        /// Checksum bits decoded from the phrase tail.
104        got: u32,
105    },
106
107    /// Recovery phrase contained a codepoint whose NFKC canonical form
108    /// differs from the input. We strictly reject — silent canonicalization
109    /// would enable malleability attacks.
110    InvalidNormalization {
111        /// The offending codepoint.
112        codepoint: char,
113        /// Why NFKC canonicalization failed.
114        reason: &'static str,
115    },
116
117    /// Recovery phrase contained a codepoint from a forbidden class
118    /// (ZWJ, Variation Selector, combining mark, BIDI mark,
119    /// default-ignorable, or control).
120    RejectedCodepointClass {
121        /// The offending codepoint.
122        codepoint: char,
123        /// The forbidden class: "ZWJ" | "VARIATION_SELECTOR"
124        /// | "COMBINING_MARK" | "BIDI_MARK" | "DEFAULT_IGNORABLE"
125        /// | "CONTROL".
126        class: &'static str,
127    },
128
129    /// Custom-alphabet phrase would encode fewer than the safety floor
130    /// of entropy bits.
131    InsufficientEntropy {
132        /// Entropy bits the requested phrase would encode.
133        got_bits: f64,
134        /// Minimum entropy bits required (currently 128).
135        required_min: f64,
136    },
137
138    /// A recovery-phrase operation failed in a way not captured by the
139    /// structured variants above. Reserved for future subclasses.
140    RecoveryPhrase(String),
141}
142
143impl fmt::Display for CryptoError {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            CryptoError::InvalidKeyLength {
147                algorithm,
148                expected,
149                got,
150            } => write!(
151                f,
152                "Invalid key length for {algorithm}: expected {expected} bytes, got {got}"
153            ),
154            CryptoError::InvalidKey(msg) => write!(f, "Invalid key: {msg}"),
155            CryptoError::InvalidNonceLength {
156                algorithm,
157                expected,
158                got,
159            } => write!(
160                f,
161                "Invalid nonce length for {algorithm}: expected {expected} bytes, got {got}"
162            ),
163            CryptoError::AuthenticationFailed => write!(
164                f,
165                "Authentication failed (wrong key, tampered data, or invalid signature)"
166            ),
167            CryptoError::Encryption(msg) => write!(f, "Encryption failed: {msg}"),
168            CryptoError::Decryption(msg) => write!(f, "Decryption failed: {msg}"),
169            CryptoError::Kdf(msg) => write!(f, "KDF error: {msg}"),
170            CryptoError::Pqc(msg) => write!(f, "PQC error: {msg}"),
171            CryptoError::ReedSolomon(msg) => write!(f, "Reed-Solomon error: {msg}"),
172            CryptoError::InvalidParameter(msg) => write!(f, "Invalid parameter: {msg}"),
173            CryptoError::Serialization(msg) => write!(f, "Serialization error: {msg}"),
174            CryptoError::Compression(msg) => write!(f, "Compression error: {msg}"),
175            CryptoError::Io(err) => write!(f, "I/O error: {err}"),
176            CryptoError::ChecksumMismatch { expected, got } => write!(
177                f,
178                "Recovery phrase checksum mismatch (expected 0x{expected:0width$x}, got 0x{got:0width$x})",
179                width = 8
180            ),
181            CryptoError::InvalidNormalization { codepoint, reason } => write!(
182                f,
183                "Recovery phrase rejected codepoint U+{:04X}: {reason}",
184                *codepoint as u32
185            ),
186            CryptoError::RejectedCodepointClass { codepoint, class } => write!(
187                f,
188                "Recovery phrase rejected codepoint U+{:04X}: forbidden class {class}",
189                *codepoint as u32
190            ),
191            CryptoError::InsufficientEntropy { got_bits, required_min } => write!(
192                f,
193                "Custom alphabet phrase yields {got_bits:.1} bits of entropy, below the {required_min:.1}-bit floor"
194            ),
195            CryptoError::RecoveryPhrase(msg) => write!(f, "Recovery phrase error: {msg}"),
196        }
197    }
198}
199
200impl std::error::Error for CryptoError {
201    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
202        match self {
203            CryptoError::Io(err) => Some(err),
204            _ => None,
205        }
206    }
207}
208
209impl From<std::io::Error> for CryptoError {
210    fn from(err: std::io::Error) -> Self {
211        CryptoError::Io(err)
212    }
213}
214
215impl Clone for CryptoError {
216    fn clone(&self) -> Self {
217        match self {
218            CryptoError::InvalidKeyLength {
219                algorithm,
220                expected,
221                got,
222            } => CryptoError::InvalidKeyLength {
223                algorithm,
224                expected: *expected,
225                got: *got,
226            },
227            CryptoError::InvalidKey(msg) => CryptoError::InvalidKey(msg.clone()),
228            CryptoError::InvalidNonceLength {
229                algorithm,
230                expected,
231                got,
232            } => CryptoError::InvalidNonceLength {
233                algorithm,
234                expected: *expected,
235                got: *got,
236            },
237            CryptoError::AuthenticationFailed => CryptoError::AuthenticationFailed,
238            CryptoError::Encryption(msg) => CryptoError::Encryption(msg.clone()),
239            CryptoError::Decryption(msg) => CryptoError::Decryption(msg.clone()),
240            CryptoError::Kdf(msg) => CryptoError::Kdf(msg.clone()),
241            CryptoError::Pqc(msg) => CryptoError::Pqc(msg.clone()),
242            CryptoError::ReedSolomon(msg) => CryptoError::ReedSolomon(msg.clone()),
243            CryptoError::InvalidParameter(msg) => CryptoError::InvalidParameter(msg.clone()),
244            CryptoError::Serialization(msg) => CryptoError::Serialization(msg.clone()),
245            CryptoError::Compression(msg) => CryptoError::Compression(msg.clone()),
246            CryptoError::Io(err) => {
247                CryptoError::Io(std::io::Error::new(err.kind(), err.to_string()))
248            }
249            CryptoError::ChecksumMismatch { expected, got } => CryptoError::ChecksumMismatch {
250                expected: *expected,
251                got: *got,
252            },
253            CryptoError::InvalidNormalization { codepoint, reason } => {
254                CryptoError::InvalidNormalization {
255                    codepoint: *codepoint,
256                    reason,
257                }
258            }
259            CryptoError::RejectedCodepointClass { codepoint, class } => {
260                CryptoError::RejectedCodepointClass {
261                    codepoint: *codepoint,
262                    class,
263                }
264            }
265            CryptoError::InsufficientEntropy {
266                got_bits,
267                required_min,
268            } => CryptoError::InsufficientEntropy {
269                got_bits: *got_bits,
270                required_min: *required_min,
271            },
272            CryptoError::RecoveryPhrase(msg) => CryptoError::RecoveryPhrase(msg.clone()),
273        }
274    }
275}
276
277/// Result alias for all fallible operations in this crate.
278pub type Result<T> = std::result::Result<T, CryptoError>;
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_invalid_key_length_display() {
286        let err = CryptoError::InvalidKeyLength {
287            algorithm: "Falcon-1024",
288            expected: 32,
289            got: 16,
290        };
291        let msg = err.to_string();
292        assert!(msg.contains("Falcon-1024"));
293        assert!(msg.contains("expected 32"));
294        assert!(msg.contains("got 16"));
295    }
296
297    #[test]
298    fn test_auth_failed_display() {
299        let err = CryptoError::AuthenticationFailed;
300        assert!(err.to_string().contains("Authentication failed"));
301    }
302
303    #[test]
304    fn test_clone_roundtrip() {
305        let err = CryptoError::InvalidKeyLength {
306            algorithm: "Ed25519",
307            expected: 32,
308            got: 31,
309        };
310        let cloned = err.clone();
311        assert_eq!(err.to_string(), cloned.to_string());
312    }
313
314    #[test]
315    fn test_io_from() {
316        let io_err = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "truncated");
317        let crypto_err: CryptoError = io_err.into();
318        assert!(matches!(crypto_err, CryptoError::Io(_)));
319        assert!(crypto_err.to_string().contains("truncated"));
320    }
321}