1use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6use tracing::{debug, info, warn};
7
8use crate::error::CryptoError;
9use crate::keys::{hardcoded_keys, parse_key_hex, parse_key_name};
10
11pub struct KeyService {
13 keys: HashMap<u64, [u8; 16]>,
15}
16
17impl KeyService {
18 pub fn new() -> Self {
20 let keys = hardcoded_keys();
21 info!("Loaded {} hardcoded encryption keys", keys.len());
22
23 let mut service = Self { keys };
24
25 match service.load_from_standard_dirs() {
27 Ok(count) if count > 0 => {
28 info!("Loaded {} additional keys from standard directories", count);
29 }
30 Ok(_) => {
31 debug!("No additional keys found in standard directories");
32 }
33 Err(e) => {
34 warn!("Failed to load keys from standard directories: {}", e);
35 }
36 }
37
38 info!("Total keys available: {}", service.key_count());
39
40 service
41 }
42
43 pub fn empty() -> Self {
45 Self {
46 keys: HashMap::new(),
47 }
48 }
49
50 pub fn get_key(&self, key_id: u64) -> Option<&[u8; 16]> {
52 self.keys.get(&key_id)
53 }
54
55 pub fn add_key(&mut self, key_id: u64, key: [u8; 16]) {
57 self.keys.insert(key_id, key);
58 }
59
60 pub fn key_count(&self) -> usize {
62 self.keys.len()
63 }
64
65 pub fn load_key_file(&mut self, path: &Path) -> Result<usize, CryptoError> {
67 let content = fs::read_to_string(path)?;
68
69 let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
71
72 match ext {
73 "csv" => self.load_csv_keys(&content),
74 "tsv" => self.load_tsv_keys(&content),
75 "txt" => self.load_txt_keys(&content),
76 _ => {
77 if content.contains(',') {
79 self.load_csv_keys(&content)
80 } else if content.contains('\t') {
81 self.load_tsv_keys(&content)
82 } else {
83 self.load_txt_keys(&content)
84 }
85 }
86 }
87 }
88
89 fn load_csv_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
91 let mut loaded = 0;
92
93 for (line_num, line) in content.lines().enumerate() {
94 let line = line.trim();
95
96 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
98 continue;
99 }
100
101 let parts: Vec<&str> = line.split(',').collect();
102 if parts.len() < 2 {
103 warn!("Skipping invalid CSV line {}: {}", line_num + 1, line);
104 continue;
105 }
106
107 let key_name = parts[0].trim();
108 let key_hex = parts[1].trim();
109
110 match (parse_key_name(key_name), parse_key_hex(key_hex)) {
111 (Ok(key_id), Ok(key)) => {
112 self.add_key(key_id, key);
113 loaded += 1;
114 }
115 (Err(e), _) => {
116 warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
117 }
118 (_, Err(e)) => {
119 warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
120 }
121 }
122 }
123
124 info!("Loaded {} keys from CSV file", loaded);
125 Ok(loaded)
126 }
127
128 fn load_tsv_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
130 let mut loaded = 0;
131
132 for (line_num, line) in content.lines().enumerate() {
133 let line = line.trim();
134
135 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
137 continue;
138 }
139
140 let parts: Vec<&str> = line.split('\t').collect();
141 if parts.len() < 2 {
142 warn!("Skipping invalid TSV line {}: {}", line_num + 1, line);
143 continue;
144 }
145
146 let key_name = parts[0].trim();
147 let key_hex = parts[1].trim();
148
149 match (parse_key_name(key_name), parse_key_hex(key_hex)) {
150 (Ok(key_id), Ok(key)) => {
151 self.add_key(key_id, key);
152 loaded += 1;
153 }
154 (Err(e), _) => {
155 warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
156 }
157 (_, Err(e)) => {
158 warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
159 }
160 }
161 }
162
163 info!("Loaded {} keys from TSV file", loaded);
164 Ok(loaded)
165 }
166
167 fn load_txt_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
169 let mut loaded = 0;
170
171 for (line_num, line) in content.lines().enumerate() {
172 let line = line.trim();
173
174 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
176 continue;
177 }
178
179 let parts: Vec<&str> = line.split_whitespace().collect();
180 if parts.len() < 2 {
181 warn!("Skipping invalid TXT line {}: {}", line_num + 1, line);
182 continue;
183 }
184
185 let key_name = parts[0];
186 let key_hex = parts[1];
187
188 match (parse_key_name(key_name), parse_key_hex(key_hex)) {
189 (Ok(key_id), Ok(key)) => {
190 self.add_key(key_id, key);
191 loaded += 1;
192 }
193 (Err(e), _) => {
194 warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
195 }
196 (_, Err(e)) => {
197 warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
198 }
199 }
200 }
201
202 info!("Loaded {} keys from TXT file", loaded);
203 Ok(loaded)
204 }
205
206 pub fn load_from_standard_dirs(&mut self) -> Result<usize, CryptoError> {
208 let mut total_loaded = 0;
209
210 if let Ok(path) = std::env::var("CASCETTE_KEYS_PATH") {
212 let path = PathBuf::from(path);
213 if path.exists() {
214 if path.is_file() {
215 match self.load_key_file(&path) {
216 Ok(count) => {
217 total_loaded += count;
218 info!("Loaded {} keys from CASCETTE_KEYS_PATH", count);
219 }
220 Err(e) => {
221 warn!("Failed to load keys from CASCETTE_KEYS_PATH: {}", e);
222 }
223 }
224 } else if path.is_dir() {
225 total_loaded += self.load_keys_from_dir(&path)?;
226 }
227 }
228 }
229
230 if let Some(home_dir) = dirs::home_dir() {
232 let config_dir = home_dir.join(".config").join("cascette");
234 if config_dir.exists() {
235 total_loaded += self.load_keys_from_dir(&config_dir)?;
236 }
237
238 let tactkeys_dir = home_dir.join(".tactkeys");
240 if tactkeys_dir.exists() {
241 total_loaded += self.load_keys_from_dir(&tactkeys_dir)?;
242 }
243 }
244
245 Ok(total_loaded)
246 }
247
248 fn load_keys_from_dir(&mut self, dir: &Path) -> Result<usize, CryptoError> {
250 let mut total_loaded = 0;
251
252 for entry in fs::read_dir(dir)? {
253 let entry = entry?;
254 let path = entry.path();
255
256 if path.is_file() {
257 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
258
259 if name.ends_with(".csv")
261 || name.ends_with(".tsv")
262 || name.ends_with(".txt")
263 || name.contains("key")
264 {
265 match self.load_key_file(&path) {
266 Ok(count) => {
267 total_loaded += count;
268 debug!("Loaded {} keys from {:?}", count, path);
269 }
270 Err(e) => {
271 warn!("Failed to load keys from {:?}: {}", path, e);
272 }
273 }
274 }
275 }
276 }
277
278 Ok(total_loaded)
279 }
280}
281
282impl Default for KeyService {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use std::io::Write;
292 use tempfile::NamedTempFile;
293
294 #[test]
295 fn test_hardcoded_keys() {
296 let service = KeyService::new();
297 assert!(service.key_count() > 0);
298
299 let key = service.get_key(0xFA505078126ACB3E);
301 assert!(key.is_some());
302 }
303
304 #[test]
305 fn test_add_key() {
306 let mut service = KeyService::empty();
307 let key_id = 0x1234567890ABCDEF;
308 let key = [0u8; 16];
309
310 service.add_key(key_id, key);
311 assert_eq!(service.get_key(key_id), Some(&key));
312 }
313
314 #[test]
315 fn test_load_csv() -> Result<(), Box<dyn std::error::Error>> {
316 let mut file = NamedTempFile::new()?;
317 writeln!(file, "# Comment line")?;
318 writeln!(file, "0x1234567890ABCDEF,00112233445566778899AABBCCDDEEFF")?;
319 writeln!(file, "FEDCBA0987654321,FFEEDDCCBBAA99887766554433221100")?;
320
321 let mut service = KeyService::empty();
322 let loaded = service.load_key_file(file.path())?;
323 assert_eq!(loaded, 2);
324
325 assert!(service.get_key(0x1234567890ABCDEF).is_some());
326 assert!(service.get_key(0xFEDCBA0987654321).is_some());
327
328 Ok(())
329 }
330
331 #[test]
332 fn test_load_txt() -> Result<(), Box<dyn std::error::Error>> {
333 let mut file = NamedTempFile::new()?;
334 writeln!(file, "# Comment line")?;
335 writeln!(
336 file,
337 "0x1234567890ABCDEF 00112233445566778899AABBCCDDEEFF Some description"
338 )?;
339 writeln!(file, "FEDCBA0987654321 FFEEDDCCBBAA99887766554433221100")?;
340
341 let mut service = KeyService::empty();
342 let loaded = service.load_key_file(file.path())?;
343 assert_eq!(loaded, 2);
344
345 assert!(service.get_key(0x1234567890ABCDEF).is_some());
346 assert!(service.get_key(0xFEDCBA0987654321).is_some());
347
348 Ok(())
349 }
350}