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)?;
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,
{
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")?;
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)
}