sopass 0.5.0

command line password manager using SOP
Documentation
//! `sopass` configuration handling.
//!
//! [`Config`] holds the run-time configuration, computed from
//! defaults, plus a configuration file, plus command line options.

use std::path::{Path, PathBuf};

use log::debug;
use serde::{Deserialize, Serialize};

use crate::{sop::Sop, DEFAULT_CERT_FILENAME, DEFAULT_KEY_FILENAME};

const DEFAULT_SOP: &str = "rsop";
const CONFIG_FILE_BASENAME: &str = "sopass";

// A representation of the configuration file.
//
// We don't set the default values for store and sop here. The store
// default needs to be computed at run time, and that can fail. The
// sop default is static, but it seems easier to deal with both values
// in the same way, at least for now.
#[derive(Default, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct ConfigFile {
    store: Option<PathBuf>,
    sop: Option<PathBuf>,
    sop_decrypt: Option<PathBuf>,
}

impl ConfigFile {
    // Load configuration from a specific file, which must exist.
    fn load_path(filename: &Path) -> Result<Self, ConfigError> {
        if filename.exists() {
            debug!("load configuration file {}", filename.display());
            confy::load_path(filename).map_err(|err| ConfigError::Load(filename.into(), err))
        } else {
            debug!(
                "requested configuration file {} does not exist",
                filename.display()
            );
            Err(ConfigError::NoSuchFile(filename.into()))
        }
    }

    // Load the default configuration. If it doesn't exist, use
    // built-in defaults.
    fn load(app: &'static str) -> Result<Self, ConfigError> {
        let filename = confy::get_configuration_file_path(app, CONFIG_FILE_BASENAME)
            .map_err(ConfigError::Filename)?;
        if filename.exists() {
            debug!("load default configuration file {}", filename.display());
            confy::load(app, CONFIG_FILE_BASENAME).map_err(|err| ConfigError::Load(filename, err))
        } else {
            debug!(
                "configuration file {} does not exist, using defaults",
                filename.display()
            );
            Ok(Self::default())
        }
    }

    // Return the path to the password store directory to use.
    fn store(&self, default: &Path) -> Result<PathBuf, ConfigError> {
        self.get("store", self.store.as_deref(), default)
    }

    // Return the name of the SOP implementation to use.
    fn sop(&self, default: &Path) -> Result<PathBuf, ConfigError> {
        self.get("sop", self.sop.as_deref(), default)
    }

    // Return the name of the SOP implementation to use for decryption.
    fn sop_decrypt(&self, default: &Path) -> Result<PathBuf, ConfigError> {
        self.get("sop_decrypt", self.sop_decrypt.as_deref(), default)
    }

    // Get value of a configuration field, or the default value if not
    // set. Note that this only handles fields that contain filenames,
    // but that's all we need.
    fn get(
        &self,
        name: &'static str,
        path: Option<&Path>,
        default: &Path,
    ) -> Result<PathBuf, ConfigError> {
        let result = if path.is_none() {
            Ok(default.into())
        } else {
            path.ok_or(ConfigError::Missing(name))
                .map(|path| path.into())
        };

        debug!(
            "get: name={} path={:?} default={} => {:?}",
            name,
            path,
            default.display(),
            result
        );

        result
    }
}

/// Build a run time configuration from defaults, configuration file,
/// and command line options.
#[derive(Default, Debug)]
pub struct ConfigBuilder {
    app: &'static str,
    default_store: PathBuf,
    default_sop: PathBuf,
    filename: Option<PathBuf>,
    store: Option<PathBuf>,
    sop: Option<PathBuf>,
    sop_decrypt: Option<PathBuf>,
}

impl ConfigBuilder {
    /// Create a new builder. The `app` is used to compute the default
    /// location of the configuration file. `default_store` is the
    /// default location of the store, unless overridden by
    /// configuration file or command line option.
    pub fn new(app: &'static str, default_store: &Path) -> Self {
        Self {
            app,
            default_store: default_store.into(),
            default_sop: PathBuf::from(DEFAULT_SOP),
            ..Default::default()
        }
    }

    /// Build a new run time configuration. This can fail, because it
    /// may read the configuration file.
    pub fn build(self) -> Result<Config, ConfigError> {
        debug!("building configuration from {self:#?}");

        let file = if let Some(filename) = &self.filename {
            ConfigFile::load_path(filename)?
        } else {
            ConfigFile::load(self.app)?
        };
        debug!("loaded configuration {file:#?}");

        let store = self.store.unwrap_or(file.store(&self.default_store)?);

        let sop = self.sop.unwrap_or(file.sop(&self.default_sop)?);
        debug!("sop={sop:?}");

        let sop_decrypt = self.sop_decrypt.unwrap_or(file.sop_decrypt(&sop)?);
        debug!("sop_decrypt={sop_decrypt:?}");

        let config = Config::new(&store, &sop, &sop_decrypt);
        debug!("using configuration {config:#?}");
        Ok(config)
    }

    /// Read configuration from this file.
    pub fn filename(&mut self, filename: &Path) {
        self.filename = Some(filename.into());
    }

    /// Use this value for the store location. This overrides the
    /// value in the configuration file.
    pub fn store(&mut self, store: &Path) {
        self.store = Some(store.into());
    }

    /// Use this value for the SOP implementation. This overrides the
    /// value in the configuration file.
    pub fn sop(&mut self, sop: &Path) {
        self.sop = Some(sop.into());
    }

    /// Use this value for the SOP implementation for decryption. This
    /// overrides the value in the configuration file.
    pub fn sop_decrypt(&mut self, sop: &Path) {
        self.sop_decrypt = Some(sop.into());
    }
}

/// The run time configuration.
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    store: PathBuf,
    key_file: PathBuf,
    cert_file: PathBuf,
    sop: PathBuf,
    sop_decrypt: PathBuf,
}

impl Config {
    fn new(store: &Path, sop: &Path, sop_decrypt: &Path) -> Self {
        Self {
            store: store.into(),
            key_file: store.join(DEFAULT_KEY_FILENAME),
            cert_file: store.join(DEFAULT_CERT_FILENAME),
            sop: sop.into(),
            sop_decrypt: sop_decrypt.into(),
        }
    }

    /// The location of the store.
    pub fn store(&self) -> &Path {
        &self.store
    }

    /// The a way to use configured SOP implementation.
    pub fn sop(&self) -> Sop {
        if self.sop != self.sop_decrypt {
            Sop::hardware_key(&self.sop, &self.sop_decrypt, &self.cert_file)
        } else {
            Sop::software_key(&self.sop, &self.key_file)
        }
    }

    /// Format the configuration into pretty JSON.
    pub fn pretty_json(&self) -> Result<String, ConfigError> {
        serde_json::to_string_pretty(self).map_err(ConfigError::Json)
    }
}

/// Everything that can go wrong in this module.
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    /// Can't load configuration file.
    #[error("failed to load configuration file {0}")]
    Load(PathBuf, #[source] confy::ConfyError),

    /// The configuration file does not exist, but should.
    #[error("requested configuration file does not exist: {0}")]
    NoSuchFile(PathBuf),

    /// Can't figure out name or location of default configuration
    /// file.
    #[error("failed to determine default filename for configuration file")]
    Filename(#[source] confy::ConfyError),

    /// Configuration field is missing, even the default. This is a
    /// programming error.
    #[error("configuration does not specify field {0}")]
    Missing(&'static str),

    /// Can't turn configuration into JSON.
    #[error("failed to serialize configuration as JSON")]
    Json(#[source] serde_json::Error),
}