twoken 0.4.0

Generate One-Time Passwords from stored token secrets.
Documentation
use crate::token::Token;
use anyhow::{anyhow, Context, Result};
use log;
use serde::Serialize;
use std::fs::File;
use std::process::{Child, Output};
use std::{
    io::Write,
    process::{Command, Stdio},
};

pub trait Store {
    fn put<T>(&mut self, id: &str, token: T) -> Result<()>
    where
        T: Token;
    fn get<T>(&self, id: &str) -> Result<T>
    where
        T: Token;
}

pub struct PassStore;
pub struct GopassStore;
pub struct FileStore;

#[derive(clap::ValueEnum, Default, Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GenericStore {
    #[default]
    Pass,
    Gopass,
    File,
}

impl Store for GenericStore {
    fn put<T>(&mut self, id: &str, token: T) -> Result<()>
    where
        T: Token,
    {
        match self {
            GenericStore::Pass => PassStore.put(id, token),
            GenericStore::Gopass => GopassStore.put(id, token),
            GenericStore::File => FileStore.put(id, token),
        }
    }
    fn get<T>(&self, id: &str) -> Result<T>
    where
        T: Token,
    {
        match self {
            GenericStore::Pass => PassStore.get(id),
            GenericStore::Gopass => GopassStore.get(id),
            GenericStore::File => FileStore.get(id),
        }
    }
}

impl Store for FileStore {
    fn put<T>(&mut self, id: &str, token: T) -> Result<()>
    where
        T: Token,
    {
        log::warn!("Storing token secrets in a plain file is insecure");
        let file = File::create(id)?;
        serde_json::to_writer_pretty(file, &token).context("could not write token to file")?;
        Ok(())
    }

    fn get<T>(&self, id: &str) -> Result<T>
    where
        T: Token,
    {
        let file = File::open(id)?;
        let token = serde_json::from_reader(file).context("could not decode token json")?;
        Ok(token)
    }
}

impl Store for PassStore {
    fn put<T>(&mut self, id: &str, token: T) -> Result<()>
    where
        T: Token,
    {
        let to_put = serde_json::to_string_pretty(&token)?;
        let child = Command::new("pass")
            .arg("insert")
            .arg("--multiline")
            .arg("--force")
            .arg(id)
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .spawn()?;
        send_token_to_stdin(child, to_put)
    }

    fn get<T>(&self, id: &str) -> Result<T>
    where
        T: Token,
    {
        let output = Command::new("pass")
            .arg(id)
            .stdin(Stdio::inherit())
            .output()?;
        get_token_from_stdout(output)
    }
}

impl Store for GopassStore {
    fn put<T>(&mut self, id: &str, token: T) -> Result<()>
    where
        T: Token,
    {
        let to_put = serde_json::to_string(&token)?;
        // insert into secret with token name as field
        let child = Command::new("gopass")
            .arg("insert")
            .arg(id)
            .arg(token.name())
            .stdin(Stdio::piped())
            .stdout(Stdio::null())
            .stderr(Stdio::piped())
            .spawn()?;
        send_token_to_stdin(child, to_put)
    }

    fn get<T>(&self, id: &str) -> Result<T>
    where
        T: Token,
    {
        // retrieve from secret with token name as field
        let output = Command::new("gopass")
            .arg("show")
            .arg(id)
            .arg(T::NAME)
            .stdin(Stdio::inherit())
            .output()?;
        get_token_from_stdout(output)
    }
}

fn send_token_to_stdin(mut child: Child, token: String) -> Result<()> {
    let mut stdin = child.stdin.take().context("could not open stdin")?;
    // write to stdin in separate thread to avoid deadlock
    std::thread::spawn(move || {
        write!(stdin, "{}", token).expect("error writing to stdin");
    });
    let output = child.wait_with_output()?;
    if !output.status.success() {
        return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)));
    }
    Ok(())
}

fn get_token_from_stdout<T>(output: Output) -> Result<T>
where
    T: Token,
{
    if !output.status.success() {
        return Err(anyhow!("{}", String::from_utf8_lossy(&output.stderr)));
    }
    let stdout = std::str::from_utf8(&output.stdout).context("could not decode pass stdout")?;
    let token = serde_json::from_str(stdout).context("could not decode token json")?;
    Ok(token)
}