kotoba_security/
config.rs

1//! Security configuration types
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use crate::capabilities::CapabilityConfig;
6
7/// Main security configuration
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SecurityConfig {
10    pub jwt_config: JwtConfig,
11    pub oauth2_config: Option<OAuth2Config>,
12    pub mfa_config: MfaConfig,
13    pub password_config: PasswordConfig,
14    pub session_config: SessionConfig,
15    pub capability_config: CapabilityConfig,
16    pub rate_limit_config: RateLimitConfig,
17    pub audit_config: AuditConfig,
18}
19
20impl Default for SecurityConfig {
21    fn default() -> Self {
22        Self {
23            jwt_config: JwtConfig::default(),
24            oauth2_config: None,
25            mfa_config: MfaConfig::default(),
26            password_config: PasswordConfig::default(),
27            session_config: SessionConfig::default(),
28            capability_config: CapabilityConfig::default(),
29            rate_limit_config: RateLimitConfig::default(),
30            audit_config: AuditConfig::default(),
31        }
32    }
33}
34
35/// JWT configuration
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct JwtConfig {
38    pub algorithm: JwtAlgorithm,
39    pub secret: String,
40    pub issuer: String,
41    pub audience: Vec<String>,
42    pub access_token_expiration: u64,  // seconds
43    pub refresh_token_expiration: u64, // seconds
44    pub leeway_seconds: u64,
45    pub validate_exp: bool,
46    pub validate_nbf: bool,
47    pub validate_aud: bool,
48    pub validate_iss: bool,
49}
50
51impl Default for JwtConfig {
52    fn default() -> Self {
53        Self {
54            algorithm: JwtAlgorithm::HS256,
55            secret: "your-secret-key-change-in-production".to_string(),
56            issuer: "kotoba".to_string(),
57            audience: vec!["kotoba-users".to_string()],
58            access_token_expiration: 900,    // 15 minutes
59            refresh_token_expiration: 86400, // 24 hours
60            leeway_seconds: 60,
61            validate_exp: true,
62            validate_nbf: false,
63            validate_aud: true,
64            validate_iss: true,
65        }
66    }
67}
68
69/// JWT algorithm types
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub enum JwtAlgorithm {
72    HS256,
73    HS384,
74    HS512,
75    RS256,
76    RS384,
77    RS512,
78    ES256,
79    ES384,
80    ES512,
81}
82
83impl JwtAlgorithm {
84    pub fn as_str(&self) -> &'static str {
85        match self {
86            JwtAlgorithm::HS256 => "HS256",
87            JwtAlgorithm::HS384 => "HS384",
88            JwtAlgorithm::HS512 => "HS512",
89            JwtAlgorithm::RS256 => "RS256",
90            JwtAlgorithm::RS384 => "RS384",
91            JwtAlgorithm::RS512 => "RS512",
92            JwtAlgorithm::ES256 => "ES256",
93            JwtAlgorithm::ES384 => "ES384",
94            JwtAlgorithm::ES512 => "ES512",
95        }
96    }
97}
98
99/// OAuth2 configuration
100#[derive(Debug, Clone, Serialize, Deserialize, Default)]
101pub struct OAuth2Config {
102    #[serde(default)]
103    pub providers: HashMap<String, OAuth2ProviderConfig>,
104    #[serde(default)]
105    pub redirect_uri: String,
106    #[serde(default)]
107    pub scopes: Vec<String>,
108    #[serde(default = "default_state_timeout")]
109    pub state_timeout_seconds: u64,
110}
111
112fn default_state_timeout() -> u64 {
113    600 // 10 minutes
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct OAuth2ProviderConfig {
118    pub client_id: String,
119    pub client_secret: String,
120    pub authorization_url: String,
121    pub token_url: String,
122    pub userinfo_url: Option<String>,
123    pub scope_separator: String,
124    pub additional_params: HashMap<String, String>,
125}
126
127/// MFA configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct MfaConfig {
130    pub issuer: String,
131    pub digits: u8,
132    pub skew: u8,
133    pub step: u64,
134    pub backup_codes_count: usize,
135    pub qr_code_size: u32,
136}
137
138impl Default for MfaConfig {
139    fn default() -> Self {
140        Self {
141            issuer: "Kotoba".to_string(),
142            digits: 6,
143            skew: 1,
144            step: 30,
145            backup_codes_count: 10,
146            qr_code_size: 200,
147        }
148    }
149}
150
151/// Password configuration
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PasswordConfig {
154    pub algorithm: PasswordAlgorithm,
155    pub min_length: usize,
156    pub require_uppercase: bool,
157    pub require_lowercase: bool,
158    pub require_digits: bool,
159    pub require_special_chars: bool,
160    pub argon2_config: Option<Argon2Config>,
161    pub pbkdf2_config: Option<Pbkdf2Config>,
162}
163
164impl Default for PasswordConfig {
165    fn default() -> Self {
166        Self {
167            algorithm: PasswordAlgorithm::Argon2,
168            min_length: 8,
169            require_uppercase: true,
170            require_lowercase: true,
171            require_digits: true,
172            require_special_chars: false,
173            argon2_config: Some(Argon2Config::default()),
174            pbkdf2_config: None,
175        }
176    }
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[derive(PartialEq)]
181pub enum PasswordAlgorithm {
182    Argon2,
183    Pbkdf2,
184    Bcrypt,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Argon2Config {
189    pub variant: Argon2Variant,
190    pub version: u32,
191    pub m_cost: u32,
192    pub t_cost: u32,
193    pub p_cost: u32,
194    pub output_len: usize,
195}
196
197impl Default for Argon2Config {
198    fn default() -> Self {
199        Self {
200            variant: Argon2Variant::Argon2id,
201            version: argon2::Version::V0x13 as u32,
202            m_cost: 65536, // 64 MB
203            t_cost: 3,
204            p_cost: 4,
205            output_len: 32,
206        }
207    }
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub enum Argon2Variant {
212    Argon2d,
213    Argon2i,
214    Argon2id,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct Pbkdf2Config {
219    pub iterations: u32,
220    pub output_len: usize,
221}
222
223/// Session configuration
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct SessionConfig {
226    pub store_type: SessionStoreType,
227    pub cookie_name: String,
228    pub cookie_secure: bool,
229    pub cookie_http_only: bool,
230    pub cookie_same_site: SameSitePolicy,
231    pub max_age_seconds: Option<u64>,
232    pub idle_timeout_seconds: Option<u64>,
233}
234
235impl Default for SessionConfig {
236    fn default() -> Self {
237        Self {
238            store_type: SessionStoreType::Memory,
239            cookie_name: "kotoba_session".to_string(),
240            cookie_secure: true,
241            cookie_http_only: true,
242            cookie_same_site: SameSitePolicy::Lax,
243            max_age_seconds: Some(86400), // 24 hours
244            idle_timeout_seconds: Some(3600), // 1 hour
245        }
246    }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub enum SessionStoreType {
251    Memory,
252    Redis,
253    Database,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
257pub enum SameSitePolicy {
258    Strict,
259    Lax,
260    None,
261}
262
263/// Rate limit configuration
264#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct RateLimitConfig {
266    pub enabled: bool,
267    pub max_requests: u32,
268    pub window_seconds: u64,
269    pub burst_size: u32,
270    pub exempt_ips: Vec<String>,
271}
272
273impl Default for RateLimitConfig {
274    fn default() -> Self {
275        Self {
276            enabled: true,
277            max_requests: 100,
278            window_seconds: 60,
279            burst_size: 10,
280            exempt_ips: Vec::new(),
281        }
282    }
283}
284
285/// Audit configuration
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct AuditConfig {
288    pub enabled: bool,
289    pub log_level: AuditLogLevel,
290    pub log_sensitive_data: bool,
291    pub retention_days: u64,
292    pub max_entries_per_day: usize,
293}
294
295impl Default for AuditConfig {
296    fn default() -> Self {
297        Self {
298            enabled: true,
299            log_level: AuditLogLevel::Info,
300            log_sensitive_data: false,
301            retention_days: 90,
302            max_entries_per_day: 10000,
303        }
304    }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub enum AuditLogLevel {
309    Debug,
310    Info,
311    Warn,
312    Error,
313}
314
315/// Authentication method enumeration
316#[derive(Debug, Clone, Serialize, Deserialize)]
317pub enum AuthMethod {
318    Local,
319    OAuth2(String), // provider name
320    Ldap,
321    Saml,
322    Custom(String),
323}
324
325/// Validation functions
326impl SecurityConfig {
327    pub fn validate(&self) -> Result<(), String> {
328        // Validate JWT config
329        if self.jwt_config.secret.is_empty() {
330            return Err("JWT secret cannot be empty".to_string());
331        }
332
333        if self.jwt_config.secret.len() < 32 {
334            return Err("JWT secret should be at least 32 characters long".to_string());
335        }
336
337        // Validate OAuth2 config if present
338        if let Some(oauth2) = &self.oauth2_config {
339            if oauth2.providers.is_empty() {
340                return Err("OAuth2 providers cannot be empty".to_string());
341            }
342
343            for (name, provider) in &oauth2.providers {
344                if provider.client_id.is_empty() {
345                    return Err(format!("OAuth2 provider '{}' client_id cannot be empty", name));
346                }
347                if provider.client_secret.is_empty() {
348                    return Err(format!("OAuth2 provider '{}' client_secret cannot be empty", name));
349                }
350            }
351        }
352
353        // Validate password config
354        if self.password_config.min_length < 8 {
355            return Err("Minimum password length should be at least 8".to_string());
356        }
357
358        Ok(())
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_default_config_validation() {
368        let config = SecurityConfig::default();
369        assert!(config.validate().is_err()); // Should fail due to weak secret
370    }
371
372    #[test]
373    fn test_config_validation_with_strong_secret() {
374        let mut config = SecurityConfig::default();
375        config.jwt_config.secret = "a".repeat(32);
376        assert!(config.validate().is_ok());
377    }
378
379    #[test]
380    fn test_jwt_algorithm_conversion() {
381        assert_eq!(JwtAlgorithm::HS256.as_str(), "HS256");
382        assert_eq!(JwtAlgorithm::RS256.as_str(), "RS256");
383        assert_eq!(JwtAlgorithm::ES256.as_str(), "ES256");
384    }
385}