Skip to main content

peat_mesh/security/
error.rs

1//! Security error types for mesh security primitives.
2
3use thiserror::Error;
4
5/// Errors that can occur during security operations.
6#[derive(Error, Debug)]
7pub enum SecurityError {
8    /// Invalid signature - verification failed
9    #[error("invalid signature: {0}")]
10    InvalidSignature(String),
11
12    /// Challenge has expired
13    #[error("challenge expired: valid until {0}")]
14    ChallengeExpired(u64),
15
16    /// Challenge nonce mismatch
17    #[error("nonce mismatch: expected {expected}, got {actual}")]
18    NonceMismatch { expected: String, actual: String },
19
20    /// Invalid public key format
21    #[error("invalid public key: {0}")]
22    InvalidPublicKey(String),
23
24    /// Invalid device ID format
25    #[error("invalid device ID: {0}")]
26    InvalidDeviceId(String),
27
28    /// Keypair error (generation, loading, saving)
29    #[error("keypair error: {0}")]
30    KeypairError(String),
31
32    /// Peer not authenticated
33    #[error("peer not authenticated: {0}")]
34    PeerNotAuthenticated(String),
35
36    /// Authentication failed
37    #[error("authentication failed: {0}")]
38    AuthenticationFailed(String),
39
40    /// IO error (file operations)
41    #[error("IO error: {0}")]
42    IoError(#[from] std::io::Error),
43
44    /// Serialization error
45    #[error("serialization error: {0}")]
46    SerializationError(String),
47
48    /// Internal error
49    #[error("internal security error: {0}")]
50    Internal(String),
51
52    /// Peer not found
53    #[error("peer not found: {0}")]
54    PeerNotFound(String),
55
56    /// Permission denied for operation
57    #[error("permission denied: {permission} for entity {entity_id} with roles [{roles:?}]")]
58    PermissionDenied {
59        permission: String,
60        entity_id: String,
61        roles: Vec<String>,
62    },
63
64    /// Certificate validation failed
65    #[error("certificate error: {0}")]
66    CertificateError(String),
67
68    /// Certificate chain invalid
69    #[error("invalid certificate chain: {0}")]
70    InvalidCertificateChain(String),
71
72    /// Certificate expired
73    #[error("certificate expired: {0}")]
74    CertificateExpired(String),
75
76    /// Certificate revoked
77    #[error("certificate revoked: {0}")]
78    CertificateRevoked(String),
79
80    // User Authentication errors (Phase 3)
81    /// User not found in database
82    #[error("user not found: {username}")]
83    UserNotFound { username: String },
84
85    /// User already exists
86    #[error("user already exists: {username}")]
87    UserAlreadyExists { username: String },
88
89    /// Invalid credential (wrong password)
90    #[error("invalid credential for user: {username}")]
91    InvalidCredential { username: String },
92
93    /// Invalid MFA code (TOTP)
94    #[error("invalid MFA code")]
95    InvalidMfaCode,
96
97    /// Account is locked (too many failed attempts)
98    #[error("account locked: {username}")]
99    AccountLocked { username: String },
100
101    /// Account is disabled by admin
102    #[error("account disabled: {username}")]
103    AccountDisabled { username: String },
104
105    /// Account is pending activation
106    #[error("account pending activation: {username}")]
107    AccountPending { username: String },
108
109    /// Session not found
110    #[error("session not found")]
111    SessionNotFound,
112
113    /// Session expired
114    #[error("session expired")]
115    SessionExpired,
116
117    /// Unsupported authentication method
118    #[error("unsupported auth method: {method}")]
119    UnsupportedAuthMethod { method: String },
120
121    /// Password hashing error
122    #[error("password hash error: {message}")]
123    PasswordHashError { message: String },
124
125    /// TOTP generation/verification error
126    #[error("TOTP error: {message}")]
127    TotpError { message: String },
128
129    // Encryption errors (Phase 4: Encryption & Audit)
130    /// Encryption operation failed
131    #[error("encryption error: {0}")]
132    EncryptionError(String),
133
134    /// Decryption operation failed
135    #[error("decryption error: {0}")]
136    DecryptionError(String),
137
138    /// Key exchange failed
139    #[error("key exchange error: {0}")]
140    KeyExchangeError(String),
141
142    /// No group key for cell
143    #[error("no group key for cell: {cell_id}")]
144    NoGroupKey { cell_id: String },
145
146    /// Key generation mismatch
147    #[error("key generation mismatch: expected {expected}, got {actual}")]
148    KeyGenerationMismatch { expected: u64, actual: u64 },
149}
150
151impl SecurityError {
152    /// Get the error code for protocol messages
153    pub fn code(&self) -> &'static str {
154        match self {
155            SecurityError::InvalidSignature(_) => "INVALID_SIGNATURE",
156            SecurityError::ChallengeExpired(_) => "CHALLENGE_EXPIRED",
157            SecurityError::NonceMismatch { .. } => "NONCE_MISMATCH",
158            SecurityError::InvalidPublicKey(_) => "INVALID_PUBLIC_KEY",
159            SecurityError::InvalidDeviceId(_) => "INVALID_DEVICE_ID",
160            SecurityError::KeypairError(_) => "KEYPAIR_ERROR",
161            SecurityError::PeerNotAuthenticated(_) => "PEER_NOT_AUTHENTICATED",
162            SecurityError::AuthenticationFailed(_) => "AUTH_FAILED",
163            SecurityError::IoError(_) => "IO_ERROR",
164            SecurityError::SerializationError(_) => "SERIALIZATION_ERROR",
165            SecurityError::Internal(_) => "INTERNAL_ERROR",
166            SecurityError::PeerNotFound(_) => "PEER_NOT_FOUND",
167            SecurityError::PermissionDenied { .. } => "PERMISSION_DENIED",
168            SecurityError::CertificateError(_) => "CERTIFICATE_ERROR",
169            SecurityError::InvalidCertificateChain(_) => "INVALID_CERT_CHAIN",
170            SecurityError::CertificateExpired(_) => "CERTIFICATE_EXPIRED",
171            SecurityError::CertificateRevoked(_) => "CERTIFICATE_REVOKED",
172            // User auth errors
173            SecurityError::UserNotFound { .. } => "USER_NOT_FOUND",
174            SecurityError::UserAlreadyExists { .. } => "USER_EXISTS",
175            SecurityError::InvalidCredential { .. } => "INVALID_CREDENTIAL",
176            SecurityError::InvalidMfaCode => "INVALID_MFA",
177            SecurityError::AccountLocked { .. } => "ACCOUNT_LOCKED",
178            SecurityError::AccountDisabled { .. } => "ACCOUNT_DISABLED",
179            SecurityError::AccountPending { .. } => "ACCOUNT_PENDING",
180            SecurityError::SessionNotFound => "SESSION_NOT_FOUND",
181            SecurityError::SessionExpired => "SESSION_EXPIRED",
182            SecurityError::UnsupportedAuthMethod { .. } => "UNSUPPORTED_AUTH",
183            SecurityError::PasswordHashError { .. } => "PASSWORD_HASH_ERROR",
184            SecurityError::TotpError { .. } => "TOTP_ERROR",
185            // Encryption errors
186            SecurityError::EncryptionError(_) => "ENCRYPTION_ERROR",
187            SecurityError::DecryptionError(_) => "DECRYPTION_ERROR",
188            SecurityError::KeyExchangeError(_) => "KEY_EXCHANGE_ERROR",
189            SecurityError::NoGroupKey { .. } => "NO_GROUP_KEY",
190            SecurityError::KeyGenerationMismatch { .. } => "KEY_GENERATION_MISMATCH",
191        }
192    }
193
194    /// Check if this error is recoverable
195    pub fn is_recoverable(&self) -> bool {
196        matches!(self, SecurityError::ChallengeExpired(_))
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_all_error_codes() {
206        let cases: Vec<(SecurityError, &str)> = vec![
207            (
208                SecurityError::InvalidSignature("x".into()),
209                "INVALID_SIGNATURE",
210            ),
211            (SecurityError::ChallengeExpired(0), "CHALLENGE_EXPIRED"),
212            (
213                SecurityError::NonceMismatch {
214                    expected: "a".into(),
215                    actual: "b".into(),
216                },
217                "NONCE_MISMATCH",
218            ),
219            (
220                SecurityError::InvalidPublicKey("x".into()),
221                "INVALID_PUBLIC_KEY",
222            ),
223            (
224                SecurityError::InvalidDeviceId("x".into()),
225                "INVALID_DEVICE_ID",
226            ),
227            (SecurityError::KeypairError("x".into()), "KEYPAIR_ERROR"),
228            (
229                SecurityError::PeerNotAuthenticated("x".into()),
230                "PEER_NOT_AUTHENTICATED",
231            ),
232            (
233                SecurityError::AuthenticationFailed("x".into()),
234                "AUTH_FAILED",
235            ),
236            (
237                SecurityError::IoError(std::io::Error::new(std::io::ErrorKind::Other, "x")),
238                "IO_ERROR",
239            ),
240            (
241                SecurityError::SerializationError("x".into()),
242                "SERIALIZATION_ERROR",
243            ),
244            (SecurityError::Internal("x".into()), "INTERNAL_ERROR"),
245            (SecurityError::PeerNotFound("x".into()), "PEER_NOT_FOUND"),
246            (
247                SecurityError::PermissionDenied {
248                    permission: "w".into(),
249                    entity_id: "e".into(),
250                    roles: vec!["r".into()],
251                },
252                "PERMISSION_DENIED",
253            ),
254            (
255                SecurityError::CertificateError("x".into()),
256                "CERTIFICATE_ERROR",
257            ),
258            (
259                SecurityError::InvalidCertificateChain("x".into()),
260                "INVALID_CERT_CHAIN",
261            ),
262            (
263                SecurityError::CertificateExpired("x".into()),
264                "CERTIFICATE_EXPIRED",
265            ),
266            (
267                SecurityError::CertificateRevoked("x".into()),
268                "CERTIFICATE_REVOKED",
269            ),
270            (
271                SecurityError::UserNotFound {
272                    username: "u".into(),
273                },
274                "USER_NOT_FOUND",
275            ),
276            (
277                SecurityError::UserAlreadyExists {
278                    username: "u".into(),
279                },
280                "USER_EXISTS",
281            ),
282            (
283                SecurityError::InvalidCredential {
284                    username: "u".into(),
285                },
286                "INVALID_CREDENTIAL",
287            ),
288            (SecurityError::InvalidMfaCode, "INVALID_MFA"),
289            (
290                SecurityError::AccountLocked {
291                    username: "u".into(),
292                },
293                "ACCOUNT_LOCKED",
294            ),
295            (
296                SecurityError::AccountDisabled {
297                    username: "u".into(),
298                },
299                "ACCOUNT_DISABLED",
300            ),
301            (
302                SecurityError::AccountPending {
303                    username: "u".into(),
304                },
305                "ACCOUNT_PENDING",
306            ),
307            (SecurityError::SessionNotFound, "SESSION_NOT_FOUND"),
308            (SecurityError::SessionExpired, "SESSION_EXPIRED"),
309            (
310                SecurityError::UnsupportedAuthMethod { method: "m".into() },
311                "UNSUPPORTED_AUTH",
312            ),
313            (
314                SecurityError::PasswordHashError {
315                    message: "m".into(),
316                },
317                "PASSWORD_HASH_ERROR",
318            ),
319            (
320                SecurityError::TotpError {
321                    message: "m".into(),
322                },
323                "TOTP_ERROR",
324            ),
325            (
326                SecurityError::EncryptionError("x".into()),
327                "ENCRYPTION_ERROR",
328            ),
329            (
330                SecurityError::DecryptionError("x".into()),
331                "DECRYPTION_ERROR",
332            ),
333            (
334                SecurityError::KeyExchangeError("x".into()),
335                "KEY_EXCHANGE_ERROR",
336            ),
337            (
338                SecurityError::NoGroupKey {
339                    cell_id: "c".into(),
340                },
341                "NO_GROUP_KEY",
342            ),
343            (
344                SecurityError::KeyGenerationMismatch {
345                    expected: 1,
346                    actual: 2,
347                },
348                "KEY_GENERATION_MISMATCH",
349            ),
350        ];
351
352        for (err, expected_code) in cases {
353            assert_eq!(err.code(), expected_code, "wrong code for {}", err);
354        }
355    }
356
357    #[test]
358    fn test_is_recoverable() {
359        assert!(SecurityError::ChallengeExpired(0).is_recoverable());
360
361        // All others should NOT be recoverable
362        assert!(!SecurityError::InvalidSignature("x".into()).is_recoverable());
363        assert!(!SecurityError::NonceMismatch {
364            expected: "a".into(),
365            actual: "b".into()
366        }
367        .is_recoverable());
368        assert!(!SecurityError::Internal("x".into()).is_recoverable());
369        assert!(!SecurityError::SessionExpired.is_recoverable());
370        assert!(!SecurityError::EncryptionError("x".into()).is_recoverable());
371        assert!(!SecurityError::AccountLocked {
372            username: "u".into()
373        }
374        .is_recoverable());
375    }
376
377    #[test]
378    fn test_error_display_messages() {
379        assert_eq!(
380            SecurityError::InvalidSignature("bad sig".into()).to_string(),
381            "invalid signature: bad sig"
382        );
383        assert_eq!(
384            SecurityError::NonceMismatch {
385                expected: "aaa".into(),
386                actual: "bbb".into()
387            }
388            .to_string(),
389            "nonce mismatch: expected aaa, got bbb"
390        );
391        let pd = SecurityError::PermissionDenied {
392            permission: "write".into(),
393            entity_id: "e1".into(),
394            roles: vec!["admin".into()],
395        };
396        assert!(pd
397            .to_string()
398            .contains("permission denied: write for entity e1"));
399        assert_eq!(
400            SecurityError::UserNotFound {
401                username: "alice".into()
402            }
403            .to_string(),
404            "user not found: alice"
405        );
406        assert_eq!(
407            SecurityError::KeyGenerationMismatch {
408                expected: 3,
409                actual: 5
410            }
411            .to_string(),
412            "key generation mismatch: expected 3, got 5"
413        );
414        assert_eq!(
415            SecurityError::NoGroupKey {
416                cell_id: "cell-1".into()
417            }
418            .to_string(),
419            "no group key for cell: cell-1"
420        );
421        assert_eq!(
422            SecurityError::InvalidMfaCode.to_string(),
423            "invalid MFA code"
424        );
425        assert_eq!(
426            SecurityError::SessionNotFound.to_string(),
427            "session not found"
428        );
429        assert_eq!(SecurityError::SessionExpired.to_string(), "session expired");
430    }
431
432    #[test]
433    fn test_io_error_from() {
434        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
435        let sec_err: SecurityError = io_err.into();
436        assert_eq!(sec_err.code(), "IO_ERROR");
437        assert!(sec_err.to_string().contains("file missing"));
438    }
439}