1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6pub const COMMON_SECRETS: &[&str] = &[
8 "",
9 "secret",
10 "password",
11 "1234",
12 "123456",
13 "admin",
14 "test",
15 "key",
16 "jwt",
17 "token",
18 "your-256-bit-secret",
19 "your-secret",
20 "mysecret",
21 "default",
22 "changeme",
23 "qwerty",
24 "abc123",
25 "letmein",
26 "welcome",
27 "monkey",
28];
29
30#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32pub struct Config {
33 pub default_secret: Option<String>,
35 pub default_algorithm: Option<String>,
37 pub default_wordlist: Option<PathBuf>,
39 pub default_private_key: Option<PathBuf>,
41}
42
43impl Config {
44 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
46 let content = fs::read_to_string(path.as_ref())
47 .with_context(|| format!("Failed to read config file: {}", path.as_ref().display()))?;
48
49 toml::from_str(&content)
50 .with_context(|| format!("Failed to parse config file: {}", path.as_ref().display()))
51 }
52
53 pub fn default_config_dir() -> Option<PathBuf> {
55 if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
57 let path = PathBuf::from(xdg_config_home).join("jwt-hack");
58 return Some(path);
59 }
60
61 dirs::config_dir().map(|config_dir| config_dir.join("jwt-hack"))
63 }
64
65 pub fn default_config_file() -> Option<PathBuf> {
67 Self::default_config_dir().map(|dir| dir.join("config.toml"))
68 }
69
70 pub fn load(config_path: Option<&Path>) -> Result<Self> {
75 if let Some(path) = config_path {
76 return Self::from_file(path);
78 }
79
80 if let Some(default_path) = Self::default_config_file() {
82 if default_path.exists() {
83 return Self::from_file(default_path);
84 }
85 }
86
87 Ok(Self::default())
89 }
90
91 pub fn ensure_config_dir() -> Result<Option<PathBuf>> {
93 if let Some(config_dir) = Self::default_config_dir() {
94 if !config_dir.exists() {
95 fs::create_dir_all(&config_dir).with_context(|| {
96 format!(
97 "Failed to create config directory: {}",
98 config_dir.display()
99 )
100 })?;
101 }
102 Ok(Some(config_dir))
103 } else {
104 Ok(None)
105 }
106 }
107
108 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
110 let content = toml::to_string_pretty(self).context("Failed to serialize config to TOML")?;
111
112 if let Some(parent) = path.as_ref().parent() {
114 fs::create_dir_all(parent)
115 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
116 }
117
118 fs::write(path.as_ref(), content)
119 .with_context(|| format!("Failed to write config file: {}", path.as_ref().display()))?;
120
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use tempfile::TempDir;
129
130 #[test]
131 fn test_default_config() {
132 let config = Config::default();
133 assert!(config.default_secret.is_none());
134 assert!(config.default_algorithm.is_none());
135 assert!(config.default_wordlist.is_none());
136 assert!(config.default_private_key.is_none());
137 }
138
139 #[test]
140 fn test_config_serialization() {
141 let config = Config {
142 default_secret: Some("test_secret".to_string()),
143 default_algorithm: Some("HS256".to_string()),
144 default_wordlist: Some(PathBuf::from("/path/to/wordlist.txt")),
145 default_private_key: Some(PathBuf::from("/path/to/key.pem")),
146 };
147
148 let toml_str = toml::to_string(&config).unwrap();
149 let deserialized: Config = toml::from_str(&toml_str).unwrap();
150
151 assert_eq!(config.default_secret, deserialized.default_secret);
152 assert_eq!(config.default_algorithm, deserialized.default_algorithm);
153 assert_eq!(config.default_wordlist, deserialized.default_wordlist);
154 assert_eq!(config.default_private_key, deserialized.default_private_key);
155 }
156
157 #[test]
158 fn test_config_from_file() {
159 let temp_dir = TempDir::new().unwrap();
160 let config_file = temp_dir.path().join("test_config.toml");
161
162 let config_content = r#"
163default_secret = "my_secret"
164default_algorithm = "HS512"
165default_wordlist = "/path/to/wordlist.txt"
166default_private_key = "/path/to/private.pem"
167"#;
168
169 fs::write(&config_file, config_content).unwrap();
170
171 let config = Config::from_file(&config_file).unwrap();
172 assert_eq!(config.default_secret, Some("my_secret".to_string()));
173 assert_eq!(config.default_algorithm, Some("HS512".to_string()));
174 assert_eq!(
175 config.default_wordlist,
176 Some(PathBuf::from("/path/to/wordlist.txt"))
177 );
178 assert_eq!(
179 config.default_private_key,
180 Some(PathBuf::from("/path/to/private.pem"))
181 );
182 }
183
184 #[test]
185 fn test_config_load_with_fallback() {
186 let config = Config::load(None).unwrap();
188 assert!(config.default_secret.is_none());
189 }
190
191 #[test]
192 fn test_save_to_file() {
193 let temp_dir = TempDir::new().unwrap();
194 let config_file = temp_dir.path().join("save_test.toml");
195
196 let config = Config {
197 default_secret: Some("saved_secret".to_string()),
198 default_algorithm: Some("HS256".to_string()),
199 default_wordlist: None,
200 default_private_key: None,
201 };
202
203 config.save_to_file(&config_file).unwrap();
204
205 let loaded_config = Config::from_file(&config_file).unwrap();
206 assert_eq!(config.default_secret, loaded_config.default_secret);
207 assert_eq!(config.default_algorithm, loaded_config.default_algorithm);
208 }
209
210 #[test]
211 fn test_default_config_dir() {
212 let _config_dir = Config::default_config_dir();
215 }
216
217 #[test]
218 fn test_ensure_config_dir() {
219 let _result = Config::ensure_config_dir();
221 }
222}