Skip to main content

cas_lib/
error.rs

1use std::fmt;
2
3/// The unified error type returned by all fallible `cas-lib` operations.
4///
5/// Every cryptographic operation that can fail returns [`CasResult`] instead of
6/// panicking. This is important for FFI consumers: a panic unwinding across the
7/// FFI boundary is undefined behavior and typically aborts the host process. A
8/// malformed key, a tampered ciphertext, or a failed authentication tag are all
9/// recoverable conditions and are reported through this enum.
10///
11/// # ABI stability contract
12///
13/// The downstream FFI binding crates (`cas-core-lib`, `cas-typescript-sdk`)
14/// surface each variant to their callers as the stable numeric code returned by
15/// [`CasError::error_code`]. Those numbers are part of the ABI contract with
16/// every consumer SDK, so this enum is **append-only**:
17///
18/// - Never remove, rename, or renumber an existing variant.
19/// - Add new variants only at the end, and give them the next free code in
20///   [`CasError::error_code`].
21///
22/// The `error_code_contract_is_stable` test in this module pins the mapping so
23/// an accidental reorder or removal fails CI here rather than silently breaking
24/// a consumer's error handling.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum CasError {
27    /// A provided key had an invalid length or could not be parsed.
28    InvalidKey,
29    /// A provided nonce/IV had an invalid length.
30    InvalidNonce,
31    /// A provided signature had an invalid length or could not be parsed.
32    InvalidSignature,
33    /// Input bytes could not be decoded into the expected type.
34    InvalidInput,
35    /// PEM/DER decoding or encoding of a key failed.
36    InvalidPemKey,
37    /// Invalid algorithm parameters were supplied (e.g. an RSA key size that is
38    /// too small, or out-of-range password-hashing parameters).
39    InvalidParameters,
40    /// AEAD encryption failed.
41    EncryptionFailed,
42    /// AEAD decryption failed or the authentication tag did not verify.
43    DecryptionFailed,
44    /// A signing operation failed.
45    SigningFailed,
46    /// Key generation failed.
47    KeyGenerationFailed,
48    /// Password hashing or verification setup failed.
49    PasswordHashingFailed,
50    /// Compression or decompression failed.
51    CompressionFailed,
52}
53
54impl fmt::Display for CasError {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        let message = match self {
57            CasError::InvalidKey => "invalid key: wrong length or could not be parsed",
58            CasError::InvalidNonce => "invalid nonce: wrong length",
59            CasError::InvalidSignature => "invalid signature: wrong length or could not be parsed",
60            CasError::InvalidInput => "invalid input: could not be decoded",
61            CasError::InvalidPemKey => "invalid PEM key: could not be decoded or encoded",
62            CasError::InvalidParameters => "invalid algorithm parameters",
63            CasError::EncryptionFailed => "encryption failed",
64            CasError::DecryptionFailed => "decryption failed or authentication tag mismatch",
65            CasError::SigningFailed => "signing failed",
66            CasError::KeyGenerationFailed => "key generation failed",
67            CasError::PasswordHashingFailed => "password hashing failed",
68            CasError::CompressionFailed => "compression or decompression failed",
69        };
70        f.write_str(message)
71    }
72}
73
74impl std::error::Error for CasError {}
75
76impl CasError {
77    /// Maps this error to the stable numeric code surfaced through the FFI by
78    /// the downstream binding crates. `0` is reserved for success and is never
79    /// returned here.
80    ///
81    /// These values are part of the ABI contract described on [`CasError`]; see
82    /// the type-level documentation before changing them.
83    pub fn error_code(&self) -> i32 {
84        match self {
85            CasError::InvalidKey => 1,
86            CasError::InvalidNonce => 2,
87            CasError::InvalidSignature => 3,
88            CasError::InvalidInput => 4,
89            CasError::InvalidPemKey => 5,
90            CasError::InvalidParameters => 6,
91            CasError::EncryptionFailed => 7,
92            CasError::DecryptionFailed => 8,
93            CasError::SigningFailed => 9,
94            CasError::KeyGenerationFailed => 10,
95            CasError::PasswordHashingFailed => 11,
96            CasError::CompressionFailed => 12,
97        }
98    }
99}
100
101/// The result type returned by all fallible `cas-lib` operations.
102pub type CasResult<T> = Result<T, CasError>;
103
104#[cfg(test)]
105mod tests {
106    use super::CasError;
107
108    /// Golden test pinning the FFI error-code contract. If you are changing this
109    /// test you are changing the ABI surfaced by every downstream SDK — make
110    /// sure that is intentional and that `cas-core-lib` / `cas-typescript-sdk`
111    /// are updated in lockstep.
112    #[test]
113    fn error_code_contract_is_stable() {
114        let expected: &[(CasError, i32)] = &[
115            (CasError::InvalidKey, 1),
116            (CasError::InvalidNonce, 2),
117            (CasError::InvalidSignature, 3),
118            (CasError::InvalidInput, 4),
119            (CasError::InvalidPemKey, 5),
120            (CasError::InvalidParameters, 6),
121            (CasError::EncryptionFailed, 7),
122            (CasError::DecryptionFailed, 8),
123            (CasError::SigningFailed, 9),
124            (CasError::KeyGenerationFailed, 10),
125            (CasError::PasswordHashingFailed, 11),
126            (CasError::CompressionFailed, 12),
127        ];
128
129        for (error, code) in expected {
130            assert_eq!(
131                error.error_code(),
132                *code,
133                "error code for {error:?} changed; this breaks the FFI ABI contract"
134            );
135        }
136
137        // Compile-time guard against a variant being added (or removed) without
138        // updating the contract above: this match is intentionally exhaustive
139        // with no wildcard arm, so a new `CasError` variant fails to compile
140        // here until it is added to `expected` and to the downstream SDK
141        // mappings.
142        fn assert_all_variants_covered(error: &CasError) {
143            match error {
144                CasError::InvalidKey
145                | CasError::InvalidNonce
146                | CasError::InvalidSignature
147                | CasError::InvalidInput
148                | CasError::InvalidPemKey
149                | CasError::InvalidParameters
150                | CasError::EncryptionFailed
151                | CasError::DecryptionFailed
152                | CasError::SigningFailed
153                | CasError::KeyGenerationFailed
154                | CasError::PasswordHashingFailed
155                | CasError::CompressionFailed => {}
156            }
157        }
158        let _ = assert_all_variants_covered;
159    }
160}