libspmg 0.2.1

Secure password manager library
Documentation

use std::fs;
use std::fmt;
use std::error::Error;


use yaml_rust2::{YamlLoader, Yaml};

mod defaults {
    pub fn cache_expiration_time() -> u64 { 150 }
    pub fn argon_salt() -> String { String::from("0123456789012345") }
    pub fn hkdf_salt() -> String { String::from("012345678901234") }
    pub fn lock_token() -> String { String::from("5up3r5ubtl353cr3t") }
}

#[derive(Debug)]
pub struct Config {
    pub cache_expiration_time: u64,
    pub argon_salt: String,
    pub hkdf_salt: String,
    pub lock_token: String,
    pub spmg_path: String
}

#[derive(Debug)]
pub enum ConfigError {
    Io(std::io::Error),
    Yaml(yaml_rust2::ScanError),
    Structure(String),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Io(e) => write!(f, "I/O error: {}", e),
            ConfigError::Yaml(e) => write!(f, "YAML parse error: {}", e),
            ConfigError::Structure(msg) => write!(f, "YAML structure error: {}", msg),
        }
    }
}

impl Error for ConfigError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            ConfigError::Io(e) => Some(e),
            ConfigError::Yaml(e) => Some(e),
            ConfigError::Structure(_) => None,
        }
    }
}

// Manually implement From conversions for easy error propagation with `?`
impl From<std::io::Error> for ConfigError {
    fn from(err: std::io::Error) -> ConfigError {
        ConfigError::Io(err)
    }
}

impl From<yaml_rust2::ScanError> for ConfigError {
    fn from(err: yaml_rust2::ScanError) -> ConfigError {
        ConfigError::Yaml(err)
    }
}

fn load_config(spmg_path: &str) -> Result<Config, ConfigError> {

    let path = format!("{}/config.yaml", spmg_path);
    let content = fs::read_to_string(path)?;
    let docs = YamlLoader::load_from_str(&content)?;
    let doc = docs.get(0).ok_or_else(|| ConfigError::Structure("Empty YAML document".into()))?;

    let cache_expiration_time = get_u64(doc, "cache_expiration_time")
        .unwrap_or_else(defaults::cache_expiration_time);

    let argon_salt = get_string(doc, "argon_salt")
        .unwrap_or_else(defaults::argon_salt);
    
    let hkdf_salt = get_string(doc, "hkdf_salt")
        .unwrap_or_else(defaults::hkdf_salt);

    let lock_token = get_string(doc, "lock_token")
        .unwrap_or_else(defaults::lock_token);

    Ok(Config {
        cache_expiration_time,
        argon_salt,
        hkdf_salt,
        lock_token,
        spmg_path: spmg_path.to_string(),
    })
}

fn get_u64(doc: &Yaml, key: &str) -> Option<u64> {
    doc[key].as_i64().and_then(|v| if v >= 0 { Some(v as u64) } else { None })
}

fn get_string(doc: &Yaml, key: &str) -> Option<String> {
    doc[key].as_str().map(|s| s.to_string())
}


impl Config {
    pub fn new(spmg_path: String) -> Self {
        match load_config(&spmg_path) {
            Ok(cfg) => {
                cfg
            }
            Err(e) => {
                eprintln!("Failed to load config: {}. Falling back to default.", e);
                Config {
                    cache_expiration_time: defaults::cache_expiration_time(),
                    argon_salt: defaults::argon_salt(),
                    hkdf_salt: defaults::hkdf_salt(),
                    lock_token: defaults::lock_token(),
                    spmg_path: spmg_path,
                }
            }
        }

    }
}