1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum SecurityError {
8 #[error("invalid signature: {0}")]
10 InvalidSignature(String),
11
12 #[error("challenge expired: valid until {0}")]
14 ChallengeExpired(u64),
15
16 #[error("nonce mismatch: expected {expected}, got {actual}")]
18 NonceMismatch { expected: String, actual: String },
19
20 #[error("invalid public key: {0}")]
22 InvalidPublicKey(String),
23
24 #[error("invalid device ID: {0}")]
26 InvalidDeviceId(String),
27
28 #[error("keypair error: {0}")]
30 KeypairError(String),
31
32 #[error("peer not authenticated: {0}")]
34 PeerNotAuthenticated(String),
35
36 #[error("authentication failed: {0}")]
38 AuthenticationFailed(String),
39
40 #[error("IO error: {0}")]
42 IoError(#[from] std::io::Error),
43
44 #[error("serialization error: {0}")]
46 SerializationError(String),
47
48 #[error("internal security error: {0}")]
50 Internal(String),
51
52 #[error("peer not found: {0}")]
54 PeerNotFound(String),
55
56 #[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 #[error("certificate error: {0}")]
66 CertificateError(String),
67
68 #[error("invalid certificate chain: {0}")]
70 InvalidCertificateChain(String),
71
72 #[error("certificate expired: {0}")]
74 CertificateExpired(String),
75
76 #[error("certificate revoked: {0}")]
78 CertificateRevoked(String),
79
80 #[error("user not found: {username}")]
83 UserNotFound { username: String },
84
85 #[error("user already exists: {username}")]
87 UserAlreadyExists { username: String },
88
89 #[error("invalid credential for user: {username}")]
91 InvalidCredential { username: String },
92
93 #[error("invalid MFA code")]
95 InvalidMfaCode,
96
97 #[error("account locked: {username}")]
99 AccountLocked { username: String },
100
101 #[error("account disabled: {username}")]
103 AccountDisabled { username: String },
104
105 #[error("account pending activation: {username}")]
107 AccountPending { username: String },
108
109 #[error("session not found")]
111 SessionNotFound,
112
113 #[error("session expired")]
115 SessionExpired,
116
117 #[error("unsupported auth method: {method}")]
119 UnsupportedAuthMethod { method: String },
120
121 #[error("password hash error: {message}")]
123 PasswordHashError { message: String },
124
125 #[error("TOTP error: {message}")]
127 TotpError { message: String },
128
129 #[error("encryption error: {0}")]
132 EncryptionError(String),
133
134 #[error("decryption error: {0}")]
136 DecryptionError(String),
137
138 #[error("key exchange error: {0}")]
140 KeyExchangeError(String),
141
142 #[error("no group key for cell: {cell_id}")]
144 NoGroupKey { cell_id: String },
145
146 #[error("key generation mismatch: expected {expected}, got {actual}")]
148 KeyGenerationMismatch { expected: u64, actual: u64 },
149}
150
151impl SecurityError {
152 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 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 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 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 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}