sopass 0.5.0

command line password manager using SOP
Documentation
use std::{error::Error, path::PathBuf, process::exit};

use clap::Parser;
use directories_next::ProjectDirs;
use log::info;

use sopass::{
    cmd,
    config::{Config, ConfigBuilder},
    LeafCommand,
};

const QUAL: &str = "";
const ORG: &str = "";
const APP: &str = "sopass";

fn main() {
    if let Err(err) = fallible_main() {
        eprintln!("ERROR: {err}");
        let mut err = err.source();
        while let Some(underlying) = err {
            eprintln!("caused by: {underlying}");
            err = underlying.source();
        }
        exit(1);
    }
}

fn fallible_main() -> Result<(), SopassError> {
    let dirs = directories()?;

    let args = Args::parse();
    env_logger::init_from_env("SOPASS_LOG");
    let config = args.config(&dirs)?;

    info!("sopass starts");
    info!("store is {}", config.store().display());

    info!("main command start");
    match &args.cmd {
        Command::Cert(x) => x.run(&config)?,
        Command::Config(x) => x.run(&config)?,
        Command::Export(x) => x.run(&config)?,
        Command::Import(x) => x.run(&config)?,
        Command::Init(x) => x.run(&config)?,
        Command::Key(x) => x.run(&config)?,
        Command::Value(x) => x.run(&config)?,
        Command::Version(x) => x.run(&config)?,
    }

    info!("main command OK");
    Ok(())
}

fn directories() -> Result<ProjectDirs, SopassError> {
    ProjectDirs::from(QUAL, ORG, APP).ok_or(SopassError::Dirs)
}

/// A command line password manager using Stateless OpenPGP.
///
/// sopass lets you store and manage passwords and other secrets. The
/// secrets are stored in an encrypted file.
///
/// sopass was inspired by the pass (or password-store) program, which
/// uses GnuPG. sopass is built around the SOP, to avoid being locked
/// into a specific cryptography implementation. Stateless OpenPGP (or
/// SOP) is a command line interface specification
/// (https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/)
/// for using OpenPGP keys. It has a number of implementations.
#[derive(Debug, Parser)]
#[command(version)]
struct Args {
    /// Use this configuration file instead of the default.
    #[clap(long)]
    config: Option<PathBuf>,

    /// Store encrypted passwords in this directory.
    #[clap(long)]
    store: Option<PathBuf>,

    /// Use this SOP implementation.
    #[clap(long)]
    sop: Option<PathBuf>,

    #[clap(subcommand)]
    cmd: Command,
}

impl Args {
    fn config(&self, dirs: &ProjectDirs) -> Result<Config, SopassError> {
        let mut builder = ConfigBuilder::new(APP, dirs.data_dir());

        if let Some(filename) = &self.config {
            builder.filename(filename);
        }

        if let Some(filename) = &self.store {
            builder.store(filename);
        }

        if let Some(filename) = &self.sop {
            builder.sop(filename);
        }

        Ok(builder.build()?)
    }
}

#[derive(Debug, Parser)]
enum Command {
    /// Manage certificates in the password store. The password store
    /// is encrypted with each certificate.
    Cert(CertCommand),

    /// Show the actual configuration used at run time, after all
    /// configuration values have been determined.
    Config(cmd::ConfigCommand),

    Export(cmd::ExportCommand),

    Import(cmd::ImportCommand),

    /// Initialize a new password store.
    Init(cmd::InitCommand),

    /// Manage encryption key.
    Key(KeyCommand),

    /// Manage secrets in the password store.
    Value(ValueCommand),

    /// Report the version of the sopass program.
    Version(cmd::VersionCommand),
}

#[derive(Debug, Parser)]
struct CertCommand {
    #[clap(subcommand)]
    cmd: CertSubcommand,
}

impl CertCommand {
    fn run(&self, config: &Config) -> Result<(), SopassError> {
        match &self.cmd {
            CertSubcommand::Add(x) => x.run(config)?,
            CertSubcommand::Remove(x) => x.run(config)?,
            CertSubcommand::List(x) => x.run(config)?,
        }
        Ok(())
    }
}

#[derive(Debug, Parser)]
enum CertSubcommand {
    Add(cmd::AddCert),
    Remove(cmd::RemoveCert),
    List(cmd::ListCerts),
}

#[derive(Debug, Parser)]
struct KeyCommand {
    #[clap(subcommand)]
    cmd: KeySubcommand,
}

impl KeyCommand {
    fn run(&self, config: &Config) -> Result<(), SopassError> {
        match &self.cmd {
            KeySubcommand::Extract(x) => x.run(config)?,
        }
        Ok(())
    }
}

#[derive(Debug, Parser)]
enum KeySubcommand {
    Extract(cmd::ExtractCert),
}

#[derive(Debug, Parser)]
struct ValueCommand {
    #[clap(subcommand)]
    cmd: ValueSubcommand,
}

impl ValueCommand {
    fn run(&self, config: &Config) -> Result<(), SopassError> {
        info!("value command start");
        match &self.cmd {
            ValueSubcommand::Add(x) => x.run(config)?,
            ValueSubcommand::List(x) => x.run(config)?,
            ValueSubcommand::Remove(x) => x.run(config)?,
            ValueSubcommand::Rename(x) => x.run(config)?,
            ValueSubcommand::Show(x) => x.run(config)?,
        }
        info!("value command OK");
        Ok(())
    }
}

#[derive(Debug, Parser)]
enum ValueSubcommand {
    Add(cmd::AddValue),
    List(cmd::ListValues),
    Remove(cmd::RemoveValue),
    Rename(cmd::RenameValue),
    Show(cmd::ShowValue),
}

#[derive(Debug, thiserror::Error)]
enum SopassError {
    #[error("failed to determine project directories")]
    Dirs,

    #[error(transparent)]
    Configuration(#[from] sopass::config::ConfigError),

    #[error(transparent)]
    Cert(#[from] cmd::CertError),

    #[error(transparent)]
    Config(#[from] cmd::ConfigError),

    #[error(transparent)]
    Export(#[from] cmd::ExportError),

    #[error(transparent)]
    Init(#[from] cmd::InitError),

    #[error(transparent)]
    Key(#[from] cmd::KeyError),

    #[error(transparent)]
    Value(#[from] cmd::ValueError),

    #[error(transparent)]
    Version(#[from] cmd::VersionError),
}