mod age;
mod exec;
mod gpg;
use anyhow::{Result, bail};
use std::path::PathBuf;
use std::str::FromStr;
const AGE_TAG_PREFIX: &str = "age:";
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum BackendKind {
#[default]
Gpg,
Age,
}
impl FromStr for BackendKind {
type Err = anyhow::Error;
fn from_str(value: &str) -> Result<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"gpg" => Ok(Self::Gpg),
"age" => Ok(Self::Age),
other => bail!("unknown secret backend \"{other}\"; expected \"gpg\" or \"age\""),
}
}
}
#[derive(Clone, Debug)]
pub enum EncryptRecipients {
Gpg(Vec<String>),
Age(Vec<String>),
}
impl EncryptRecipients {
pub fn backend(&self) -> BackendKind {
match self {
Self::Gpg(_) => BackendKind::Gpg,
Self::Age(_) => BackendKind::Age,
}
}
}
pub fn parse_tagged_ciphertext(ciphertext: &str) -> (BackendKind, &str) {
match ciphertext.strip_prefix(AGE_TAG_PREFIX) {
Some(rest) => (BackendKind::Age, rest),
None => (BackendKind::Gpg, ciphertext),
}
}
pub async fn encrypt_secret(plaintext: &[u8], recipients: &EncryptRecipients) -> Result<String> {
match recipients {
EncryptRecipients::Gpg(recipients) => {
gpg::encrypt_gpg_secret_to_base64(plaintext, recipients).await
}
EncryptRecipients::Age(recipients) => {
let encoded = age::encrypt_age_secret_to_base64(plaintext, recipients).await?;
Ok(format!("{AGE_TAG_PREFIX}{encoded}"))
}
}
}
pub async fn decrypt_secret(ciphertext: &str, age_identities: &[PathBuf]) -> Result<String> {
let (backend, payload) = parse_tagged_ciphertext(ciphertext);
match backend {
BackendKind::Gpg => gpg::decrypt_base64_gpg_secret(payload).await,
BackendKind::Age => age::decrypt_base64_age_secret(payload, age_identities).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn untagged_ciphertext_routes_to_gpg() {
let (backend, payload) = parse_tagged_ciphertext("aGVsbG8=");
assert_eq!(backend, BackendKind::Gpg);
assert_eq!(payload, "aGVsbG8=");
}
#[test]
fn age_tagged_ciphertext_routes_to_age_and_strips_tag() {
let (backend, payload) = parse_tagged_ciphertext("age:aGVsbG8=");
assert_eq!(backend, BackendKind::Age);
assert_eq!(payload, "aGVsbG8=");
}
#[test]
fn backend_kind_parses_case_insensitively() {
assert_eq!("GPG".parse::<BackendKind>().unwrap(), BackendKind::Gpg);
assert_eq!("Age".parse::<BackendKind>().unwrap(), BackendKind::Age);
}
#[test]
fn backend_kind_rejects_unknown_values() {
let err = "sops".parse::<BackendKind>().unwrap_err();
assert!(
err.to_string().contains("unknown secret backend"),
"{err:#}"
);
}
#[test]
fn backend_kind_defaults_to_gpg() {
assert_eq!(BackendKind::default(), BackendKind::Gpg);
}
#[test]
fn encrypt_recipients_report_their_backend() {
assert_eq!(
EncryptRecipients::Gpg(vec!["a@example.com".to_string()]).backend(),
BackendKind::Gpg
);
assert_eq!(
EncryptRecipients::Age(vec!["age1qexample".to_string()]).backend(),
BackendKind::Age
);
}
}