sopass 0.5.0

command line password manager using SOP
Documentation
//! Use the specified SOP implementation.
//!
//! This module makes it convenient to use the SOP implementation that
//! is meant to be used by `sopass`.

use std::{
    ffi::OsStr,
    fs::File,
    io::Write,
    os::unix::ffi::OsStrExt,
    path::{Path, PathBuf},
    process::{Command, Stdio},
    thread::spawn,
};

use log::{debug, info};
use tempfile::tempdir;

/// The SOP implementation and associated private key file to use.
pub struct Sop {
    kind: SopKind,
}

impl Sop {
    /// Create a new [`Sop`] using a kind that uses OpenPGP cards.
    pub fn hardware_key(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
        Self {
            kind: SopKind::HardwareKey(HardwareKeySop::new(sop, sop_decrypt, cert)),
        }
    }

    /// Create a new [`Sop`] using a kind that uses software keys only.
    pub fn software_key(sop: &Path, key: &Path) -> Self {
        Self {
            kind: SopKind::SoftwareKey(SoftwareKeySop::new(sop, key)),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.extract_cert(),
            SopKind::SoftwareKey(sop) => sop.extract_cert(),
        }
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output: &Path,
    ) -> Result<Vec<u8>, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.encrypt(data, certs, output),
            SopKind::SoftwareKey(sop) => sop.encrypt(data, certs, output),
        }
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        match &self.kind {
            SopKind::HardwareKey(sop) => sop.decrypt(data),
            SopKind::SoftwareKey(sop) => sop.decrypt(data),
        }
    }
}

enum SopKind {
    HardwareKey(HardwareKeySop),
    SoftwareKey(SoftwareKeySop),
}

struct SoftwareKeySop {
    sop: PathBuf,
    key: PathBuf,
}

impl SoftwareKeySop {
    /// Create a new [`Sop`].
    pub fn new(sop: &Path, key: &Path) -> Self {
        Self {
            sop: sop.into(),
            key: key.into(),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        let output = run_sop(
            &self.sop,
            &args(&["extract-cert"]),
            self.key()?.as_bytes().to_vec(),
            None,
        )?;
        Ok(Certificate::new(output))
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output_file: &Path,
    ) -> Result<Vec<u8>, SopError> {
        info!("encrypt data to {}", output_file.display());

        let tmp = tempdir().map_err(SopError::TempDir)?;

        let mut filenames = vec![];
        for (i, cert) in certs.iter().enumerate() {
            let filename = format!("cert-{i}");
            let filename = tmp.path().join(&filename);
            std::fs::write(&filename, cert.as_bytes()).map_err(SopError::TempWrite)?;
            filenames.push(PathBuf::from(&filename));
        }

        let mut args = args(&["encrypt"]);
        for filename in filenames.iter() {
            args.push(filename.as_os_str());
        }

        let stdout = File::create(output_file)
            .map_err(|err| SopError::CreateFile(output_file.into(), err))?;

        run_sop(&self.sop, &args, data, Some(stdout))
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        let mut args = args(&["decrypt"]);
        args.push(self.key.as_os_str());
        run_sop(&self.sop, &args, data, None)
    }

    fn key(&self) -> Result<Key, SopError> {
        let data =
            std::fs::read(&self.key).map_err(|err| SopError::ReadKey(self.key.clone(), err))?;
        Ok(Key::new(data))
    }
}

#[allow(dead_code)]
struct HardwareKeySop {
    sop: PathBuf,
    sop_decrypt: PathBuf,
    cert: PathBuf,
}

#[allow(unused_variables)]
impl HardwareKeySop {
    /// Create a new [`Sop`].
    pub fn new(sop: &Path, sop_decrypt: &Path, cert: &Path) -> Self {
        Self {
            sop: sop.into(),
            sop_decrypt: sop_decrypt.into(),
            cert: cert.into(),
        }
    }

    /// Extract a [`Certificate`] from the private key.
    pub fn extract_cert(&self) -> Result<Certificate, SopError> {
        info!("read certificate from {}", self.cert.display());
        let cert =
            std::fs::read(&self.cert).map_err(|err| SopError::ReadCert(self.cert.clone(), err))?;
        Ok(Certificate::new(cert))
    }

    /// Encrypt data using a set of certificates.
    pub fn encrypt(
        &self,
        data: Vec<u8>,
        certs: &[Certificate],
        output_file: &Path,
    ) -> Result<Vec<u8>, SopError> {
        info!("encrypt data to {}", output_file.display());

        let tmp = tempdir().map_err(SopError::TempDir)?;

        let mut filenames = vec![];
        for (i, cert) in certs.iter().enumerate() {
            let filename = format!("cert-{i}");
            let filename = tmp.path().join(&filename);
            std::fs::write(&filename, cert.as_bytes()).map_err(SopError::TempWrite)?;
            filenames.push(PathBuf::from(&filename));
        }

        let mut args = args(&["encrypt"]);
        for filename in filenames.iter() {
            args.push(filename.as_os_str());
        }

        let stdout = File::create(output_file)
            .map_err(|err| SopError::CreateFile(output_file.into(), err))?;

        run_sop(&self.sop, &args, data, Some(stdout))
    }

    /// Decrypt data using the private key specified.
    pub fn decrypt(&self, data: Vec<u8>) -> Result<Vec<u8>, SopError> {
        let mut args = args(&["decrypt"]);
        args.push(self.cert.as_os_str());
        run_sop(&self.sop_decrypt, &args, data, None)
    }
}

fn args<'a>(strs: &'a [&str]) -> Vec<&'a OsStr> {
    strs.iter()
        .map(|s| OsStr::from_bytes(s.as_bytes()))
        .collect()
}

fn run_sop(
    bin: &Path,
    args: &[&OsStr],
    feed_stdin: Vec<u8>,
    stdout: Option<File>,
) -> Result<Vec<u8>, SopError> {
    fn s(os: &OsStr) -> String {
        os.to_str().unwrap().to_string()
    }

    let mut cmd = Command::new(bin);
    cmd.args(args)
        .stdin(Stdio::piped())
        .stdout(stdout.map(Stdio::from).unwrap_or(Stdio::piped()))
        .stderr(Stdio::piped());

    let mut argv = vec![s(cmd.get_program())];
    argv.append(&mut cmd.get_args().map(s).collect());
    debug!("run SOP: {:?}", argv);

    let mut child = cmd
        .spawn()
        .map_err(|err| SopError::Invoke(bin.to_path_buf(), err))?;

    let mut stdin = child.stdin.take().ok_or(SopError::TakeStdin)?;
    let writer = spawn(move || stdin.write_all(&feed_stdin));
    writer
        .join()
        .map_err(|_| SopError::JoinThread)?
        .map_err(SopError::WriteStdin)?;

    let output = child.wait_with_output().map_err(SopError::WaitChild)?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
        return Err(SopError::Failed(
            bin.to_path_buf(),
            output.status.code().unwrap_or(999),
            stderr,
        ));
    }

    Ok(output.stdout)
}

/// A private key in memory.
pub struct Key {
    data: Vec<u8>,
}

impl Key {
    /// Create a [`Key`] from key materiel.
    pub fn new(data: Vec<u8>) -> Self {
        Self { data }
    }

    /// The key materiel as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.data
    }
}

/// A certificate in memory.
#[derive(Debug)]
pub struct Certificate {
    data: Vec<u8>,
}

impl Certificate {
    /// Create a [`Certificate`] from bytes.
    pub fn new(data: Vec<u8>) -> Self {
        Self { data }
    }

    /// Load a certificate from a file.
    pub fn load(filename: &Path) -> Result<Self, SopError> {
        let bytes =
            std::fs::read(filename).map_err(|err| SopError::ReadCert(filename.into(), err))?;
        Ok(Self::new(bytes))
    }

    /// Access a [`Certificate`] as a byte slice.
    pub fn as_bytes(&self) -> &[u8] {
        &self.data
    }
}

/// Possible errors from using [`Sop`].
#[derive(Debug, thiserror::Error)]
pub enum SopError {
    /// Failed to invoke the SOP implementation, for example, if the
    /// executable doesn't exist.
    #[error("failed to run {0}")]
    Invoke(PathBuf, #[source] std::io::Error),

    /// The SOP implementation failed to do what was requested.
    #[error("{0} failed with exit code {1}, stderr:\n{2}")]
    Failed(PathBuf, i32, String),

    /// Failed to redirect stdin of SOP implementation.
    #[error("failed to get stdin from child process handle")]
    TakeStdin,

    /// Failed to read the private key file.
    #[error("failed to read key from {0}")]
    ReadKey(PathBuf, #[source] std::io::Error),

    /// Failed to join thread.
    #[error("failed to join thread that writes to child stdin")]
    JoinThread,

    /// Failed to write to SOP implementation stdin.
    #[error("failed to write key to child stdin")]
    WriteStdin(#[source] std::io::Error),

    /// Failed to wait for SOP implementation to finish.
    #[error("failed when waiting for child process to end")]
    WaitChild(#[source] std::io::Error),

    /// Failed to create file to run SOP.
    #[error("failed to create file {0}")]
    CreateFile(PathBuf, #[source] std::io::Error),

    /// Failed to create directory to run SOP.
    #[error("failed to create a temporary directory")]
    TempDir(#[source] std::io::Error),

    /// Failed to write to temporary file to run SOP.
    #[error("failed to write to temporary file")]
    TempWrite(#[source] std::io::Error),

    /// Failed to read certificate from a file.
    #[error("failed to read certificate from file {0}")]
    ReadCert(PathBuf, #[source] std::io::Error),
}