Skip to content

Commit

Permalink
implement decrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
glehmann committed Feb 1, 2024
1 parent 50aad3e commit ad2c292
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 6 deletions.
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ clap-verbosity-flag = "2.1.2"
log = "0.4.20"
ocli = "0.1.1"
serde_yaml = "0.9.31"
substring = "1.4.5"
thiserror = "1.0.56"
16 changes: 12 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,24 @@ pub struct EncryptArgs {
#[derive(Args, Debug)]
pub struct DecryptArgs {
/// Decrypt with the specified key
#[clap(short, long, env = "YAGE_KEY")]
pub key: Option<String>,
#[clap(short, long = "key", env = "YAGE_KEY")]
pub keys: Vec<String>,

/// Decrypt with the key at PATH
#[clap(short = 'K', long, name = "PATH", env = "YAGE_KEY_FILE")]
pub key_file: Option<PathBuf>,
#[clap(short = 'K', long = "key-file", name = "PATH", env = "YAGE_KEY_FILE")]
pub key_files: Vec<PathBuf>,

/// Decrypt in place
#[clap(short, long)]
pub inplace: bool,

/// The output path to the decrypted YAML file
#[clap(short, long, default_value = "-")]
pub output: PathBuf,

/// The YAML file to decrypt
#[arg()]
pub file: PathBuf,
}

/// Execute a command with decrypted values inserted into the environment
Expand Down
89 changes: 87 additions & 2 deletions src/decrypt.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,92 @@
use std::io::Read;
use std::str::FromStr;

use age::x25519;
use base64::prelude::*;
use substring::Substring;

use crate::cli::DecryptArgs;
use crate::error::Result;
use crate::error::{AppError, IOResultExt, Result};
use crate::util::{stdin_or_file, stdout_or_file};

pub fn decrypt(args: &DecryptArgs) -> Result<()> {
info!("Decrypting a file {args:?}");
let mut identities: Vec<x25519::Identity> = Vec::new();
for key in args.keys.iter() {
debug!("loading key: {key}");
let key = x25519::Identity::from_str(key)
.map_err(|e| AppError::KeyParseError { message: e.into() })?;
identities.push(key);
}
for key_file in args.key_files.iter() {
debug!("loading key file: {key_file:?}");
let input = stdin_or_file(key_file)?;
let keys = age::IdentityFile::from_buffer(input).path_ctx(key_file)?;
for key in keys.into_identities() {
let age::IdentityFileEntry::Native(key) = key;
identities.push(key);
}
}
debug!("loading yaml file: {:?}", args.file);
let input_data: serde_yaml::Value = serde_yaml::from_reader(stdin_or_file(&args.file)?)?;
let output_data = decrypt_yaml(&input_data, &identities)?;
let output = stdout_or_file(if args.inplace {
&args.file
} else {
&args.output
})?;
serde_yaml::to_writer(output, &output_data)?;
Ok(())
}

fn decrypt_yaml(
value: &serde_yaml::Value,
identities: &[x25519::Identity],
) -> Result<serde_yaml::Value> {
match value {
serde_yaml::Value::Mapping(mapping) => {
let mut output = serde_yaml::Mapping::new();
for (key, value) in mapping {
let key = key.clone();
let value = decrypt_yaml(value, identities)?;
output.insert(key, value);
}
Ok(serde_yaml::Value::Mapping(output))
}
serde_yaml::Value::Sequence(sequence) => {
let mut output = Vec::new();
for value in sequence {
let value = decrypt_yaml(value, identities)?;
output.push(value);
}
Ok(serde_yaml::Value::Sequence(output))
}
serde_yaml::Value::String(encrypted) => {
let decrypted = decrypt_value(encrypted, identities)?;
Ok(decrypted)
}
_ => Ok(value.clone()),
}
}

fn decrypt_value(s: &str, identities: &[x25519::Identity]) -> Result<serde_yaml::Value> {
if is_yage_encoded(s) {
// remove the yage[…] prefix and suffix
let encoded = s.substring(5, s.len() - 1);
let encrypted = BASE64_STANDARD.decode(encoded)?;
let decryptor = match age::Decryptor::new(&encrypted[..])? {
age::Decryptor::Recipients(d) => Ok(d),
_ => Err(AppError::PassphraseUnsupportedError),
}?;
let mut decrypted = vec![];
let mut reader = decryptor.decrypt(identities.iter().map(|i| i as &dyn age::Identity))?;
reader.read_to_end(&mut decrypted)?;
let value: serde_yaml::Value = serde_yaml::from_slice(&decrypted)?;
Ok(value)
} else {
Ok(serde_yaml::Value::String(s.to_owned()))
}
}

fn is_yage_encoded(s: &str) -> bool {
s.starts_with("yage[") && s.ends_with("]")
}
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ pub enum AppError {
YamlError(#[from] serde_yaml::Error),
#[error("can't parse recipient {recipient}: {message}")]
RecipientParseError { recipient: String, message: String },
#[error("can't parse key: {message}")]
KeyParseError { message: String },
#[error(transparent)]
DecryptError(#[from] age::DecryptError),
#[error(transparent)]
EncryptError(#[from] age::EncryptError),
#[error(transparent)]
Utf8Error(#[from] std::string::FromUtf8Error),
#[error(transparent)]
Base64DecodeError(#[from] base64::DecodeError),
#[error("no recipients provided")]
NoRecipientsError,
#[error("passphrase not supported")]
PassphraseUnsupportedError,
}

/// Alias for a `Result` with the error type `AppError`.
Expand Down

0 comments on commit ad2c292

Please sign in to comment.