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}