use super::{Provider, ProviderUrl};
use crate::{Result, SecretSpecError};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use std::process::Command;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct PassConfig {
pub folder_prefix: Option<String>,
}
impl TryFrom<&ProviderUrl> for PassConfig {
type Error = SecretSpecError;
fn try_from(url: &ProviderUrl) -> std::result::Result<Self, Self::Error> {
if url.scheme() != "pass" {
return Err(SecretSpecError::ProviderOperationFailed(format!(
"Invalid scheme '{}' for pass provider",
url.scheme()
)));
}
let mut config = Self::default();
if let Some(host) = url.host() {
let path = url.path();
config.folder_prefix = Some(format!("{}{}", host, path));
}
Ok(config)
}
}
pub struct PassProvider {
config: PassConfig,
}
crate::register_provider! {
struct: PassProvider,
config: PassConfig,
name: "pass",
description: "Unix password manager with GPG encryption",
schemes: ["pass"],
examples: ["pass://", "pass://secretspec/shared/{profile}/{key}"],
}
impl PassProvider {
pub fn new(config: PassConfig) -> Self {
Self { config }
}
fn format_entry_name(&self, project: &str, profile: &str, key: &str) -> String {
let format_string = self
.config
.folder_prefix
.as_deref()
.unwrap_or("secretspec/{project}/{profile}/{key}");
format_string
.replace("{project}", project)
.replace("{profile}", profile)
.replace("{key}", key)
}
}
impl Provider for PassProvider {
fn name(&self) -> &'static str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
if let Some(ref prefix) = self.config.folder_prefix {
format!("pass://{}", ProviderUrl::encode(prefix))
} else {
"pass".to_string()
}
}
fn get(&self, project: &str, key: &str, profile: &str) -> Result<Option<SecretString>> {
let entry_name = self.format_entry_name(project, profile, key);
let output = Command::new("pass")
.arg("show")
.arg(&entry_name)
.output()
.map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Failed to execute 'pass' command: {}. Is pass installed?",
e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.code() == Some(1) && stderr.contains("is not in the password store") {
return Ok(None);
}
return Err(SecretSpecError::ProviderOperationFailed(format!(
"pass command failed: {}",
stderr
)));
}
let content = String::from_utf8(output.stdout)
.map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Failed to parse pass output as UTF-8: {}",
e
))
})?
.trim()
.to_string();
Ok(Some(SecretString::new(content.into())))
}
fn set(&self, project: &str, key: &str, value: &SecretString, profile: &str) -> Result<()> {
let entry_name = self.format_entry_name(project, profile, key);
let mut child = Command::new("pass")
.args(["insert", "-m", "-f", &entry_name])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Failed to execute pass command: {}",
e
))
})?;
let mut stdin = child.stdin.take().ok_or_else(|| {
SecretSpecError::ProviderOperationFailed(
"Failed to obtain stdin for pass command".to_string(),
)
})?;
use std::io::Write;
stdin
.write_all(value.expose_secret().as_bytes())
.map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Failed to write to pass stdin: {}",
e
))
})?;
drop(stdin);
let output = child.wait_with_output().map_err(|e| {
SecretSpecError::ProviderOperationFailed(format!(
"Failed to wait for pass command: {}",
e
))
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(SecretSpecError::ProviderOperationFailed(format!(
"pass command failed: {}",
stderr
)));
}
Ok(())
}
}