Skip to main content

ngdp_crypto/
key_service.rs

1//! Key management service for TACT encryption.
2
3use 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
11/// Service for managing encryption keys.
12pub struct KeyService {
13    /// Map of key ID to encryption key.
14    keys: HashMap<u64, [u8; 16]>,
15}
16
17impl KeyService {
18    /// Create a new key service with hardcoded keys and keys from standard directories.
19    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        // Try to load additional keys from standard directories
26        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    /// Create a key service with no pre-loaded keys.
44    pub fn empty() -> Self {
45        Self {
46            keys: HashMap::new(),
47        }
48    }
49
50    /// Get a key by ID.
51    pub fn get_key(&self, key_id: u64) -> Option<&[u8; 16]> {
52        self.keys.get(&key_id)
53    }
54
55    /// Add a key to the service.
56    pub fn add_key(&mut self, key_id: u64, key: [u8; 16]) {
57        self.keys.insert(key_id, key);
58    }
59
60    /// Get the number of keys in the service.
61    pub fn key_count(&self) -> usize {
62        self.keys.len()
63    }
64
65    /// Load keys from a file.
66    pub fn load_key_file(&mut self, path: &Path) -> Result<usize, CryptoError> {
67        let content = fs::read_to_string(path)?;
68
69        // Detect format based on file extension or content
70        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                // Try to auto-detect format
78                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    /// Load keys from CSV format (keyname,keyhex).
90    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            // Skip empty lines and comments
97            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    /// Load keys from TSV format (keyname\tkeyhex).
129    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            // Skip empty lines and comments
136            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    /// Load keys from TXT format (keyname keyhex [description]).
168    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            // Skip empty lines and comments
175            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    /// Load keys from standard directories.
207    pub fn load_from_standard_dirs(&mut self) -> Result<usize, CryptoError> {
208        let mut total_loaded = 0;
209
210        // Check environment variable first
211        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        // Check home directory locations
231        if let Some(home_dir) = dirs::home_dir() {
232            // ~/.config/cascette/
233            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            // ~/.tactkeys/
239            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    /// Load all key files from a directory.
249    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                // Only load files with appropriate extensions
260                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        // Test a known key
300        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}