use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
use crate::error::CryptoError;
use crate::keys::{hardcoded_keys, parse_key_hex, parse_key_name};
pub struct KeyService {
keys: HashMap<u64, [u8; 16]>,
}
impl KeyService {
pub fn new() -> Self {
let keys = hardcoded_keys();
info!("Loaded {} hardcoded encryption keys", keys.len());
let mut service = Self { keys };
match service.load_from_standard_dirs() {
Ok(count) if count > 0 => {
info!("Loaded {} additional keys from standard directories", count);
}
Ok(_) => {
debug!("No additional keys found in standard directories");
}
Err(e) => {
warn!("Failed to load keys from standard directories: {}", e);
}
}
info!("Total keys available: {}", service.key_count());
service
}
pub fn empty() -> Self {
Self {
keys: HashMap::new(),
}
}
pub fn get_key(&self, key_id: u64) -> Option<&[u8; 16]> {
self.keys.get(&key_id)
}
pub fn add_key(&mut self, key_id: u64, key: [u8; 16]) {
self.keys.insert(key_id, key);
}
pub fn key_count(&self) -> usize {
self.keys.len()
}
pub fn load_key_file(&mut self, path: &Path) -> Result<usize, CryptoError> {
let content = fs::read_to_string(path)?;
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
match ext {
"csv" => self.load_csv_keys(&content),
"tsv" => self.load_tsv_keys(&content),
"txt" => self.load_txt_keys(&content),
_ => {
if content.contains(',') {
self.load_csv_keys(&content)
} else if content.contains('\t') {
self.load_tsv_keys(&content)
} else {
self.load_txt_keys(&content)
}
}
}
}
fn load_csv_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
let mut loaded = 0;
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
continue;
}
let parts: Vec<&str> = line.split(',').collect();
if parts.len() < 2 {
warn!("Skipping invalid CSV line {}: {}", line_num + 1, line);
continue;
}
let key_name = parts[0].trim();
let key_hex = parts[1].trim();
match (parse_key_name(key_name), parse_key_hex(key_hex)) {
(Ok(key_id), Ok(key)) => {
self.add_key(key_id, key);
loaded += 1;
}
(Err(e), _) => {
warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
}
(_, Err(e)) => {
warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
}
}
}
info!("Loaded {} keys from CSV file", loaded);
Ok(loaded)
}
fn load_tsv_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
let mut loaded = 0;
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
continue;
}
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() < 2 {
warn!("Skipping invalid TSV line {}: {}", line_num + 1, line);
continue;
}
let key_name = parts[0].trim();
let key_hex = parts[1].trim();
match (parse_key_name(key_name), parse_key_hex(key_hex)) {
(Ok(key_id), Ok(key)) => {
self.add_key(key_id, key);
loaded += 1;
}
(Err(e), _) => {
warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
}
(_, Err(e)) => {
warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
}
}
}
info!("Loaded {} keys from TSV file", loaded);
Ok(loaded)
}
fn load_txt_keys(&mut self, content: &str) -> Result<usize, CryptoError> {
let mut loaded = 0;
for (line_num, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
warn!("Skipping invalid TXT line {}: {}", line_num + 1, line);
continue;
}
let key_name = parts[0];
let key_hex = parts[1];
match (parse_key_name(key_name), parse_key_hex(key_hex)) {
(Ok(key_id), Ok(key)) => {
self.add_key(key_id, key);
loaded += 1;
}
(Err(e), _) => {
warn!("Failed to parse key name on line {}: {}", line_num + 1, e);
}
(_, Err(e)) => {
warn!("Failed to parse key hex on line {}: {}", line_num + 1, e);
}
}
}
info!("Loaded {} keys from TXT file", loaded);
Ok(loaded)
}
pub fn load_from_standard_dirs(&mut self) -> Result<usize, CryptoError> {
let mut total_loaded = 0;
if let Ok(path) = std::env::var("CASCETTE_KEYS_PATH") {
let path = PathBuf::from(path);
if path.exists() {
if path.is_file() {
match self.load_key_file(&path) {
Ok(count) => {
total_loaded += count;
info!("Loaded {} keys from CASCETTE_KEYS_PATH", count);
}
Err(e) => {
warn!("Failed to load keys from CASCETTE_KEYS_PATH: {}", e);
}
}
} else if path.is_dir() {
total_loaded += self.load_keys_from_dir(&path)?;
}
}
}
if let Some(home_dir) = dirs::home_dir() {
let config_dir = home_dir.join(".config").join("cascette");
if config_dir.exists() {
total_loaded += self.load_keys_from_dir(&config_dir)?;
}
let tactkeys_dir = home_dir.join(".tactkeys");
if tactkeys_dir.exists() {
total_loaded += self.load_keys_from_dir(&tactkeys_dir)?;
}
}
Ok(total_loaded)
}
fn load_keys_from_dir(&mut self, dir: &Path) -> Result<usize, CryptoError> {
let mut total_loaded = 0;
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if name.ends_with(".csv")
|| name.ends_with(".tsv")
|| name.ends_with(".txt")
|| name.contains("key")
{
match self.load_key_file(&path) {
Ok(count) => {
total_loaded += count;
debug!("Loaded {} keys from {:?}", count, path);
}
Err(e) => {
warn!("Failed to load keys from {:?}: {}", path, e);
}
}
}
}
}
Ok(total_loaded)
}
}
impl Default for KeyService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_hardcoded_keys() {
let service = KeyService::new();
assert!(service.key_count() > 0);
let key = service.get_key(0xFA505078126ACB3E);
assert!(key.is_some());
}
#[test]
fn test_add_key() {
let mut service = KeyService::empty();
let key_id = 0x1234567890ABCDEF;
let key = [0u8; 16];
service.add_key(key_id, key);
assert_eq!(service.get_key(key_id), Some(&key));
}
#[test]
fn test_load_csv() -> Result<(), Box<dyn std::error::Error>> {
let mut file = NamedTempFile::new()?;
writeln!(file, "# Comment line")?;
writeln!(file, "0x1234567890ABCDEF,00112233445566778899AABBCCDDEEFF")?;
writeln!(file, "FEDCBA0987654321,FFEEDDCCBBAA99887766554433221100")?;
let mut service = KeyService::empty();
let loaded = service.load_key_file(file.path())?;
assert_eq!(loaded, 2);
assert!(service.get_key(0x1234567890ABCDEF).is_some());
assert!(service.get_key(0xFEDCBA0987654321).is_some());
Ok(())
}
#[test]
fn test_load_txt() -> Result<(), Box<dyn std::error::Error>> {
let mut file = NamedTempFile::new()?;
writeln!(file, "# Comment line")?;
writeln!(
file,
"0x1234567890ABCDEF 00112233445566778899AABBCCDDEEFF Some description"
)?;
writeln!(file, "FEDCBA0987654321 FFEEDDCCBBAA99887766554433221100")?;
let mut service = KeyService::empty();
let loaded = service.load_key_file(file.path())?;
assert_eq!(loaded, 2);
assert!(service.get_key(0x1234567890ABCDEF).is_some());
assert!(service.get_key(0xFEDCBA0987654321).is_some());
Ok(())
}
}