use std::{
io::{self, Write},
path::Path,
};
use anyhow::{Context, Result};
use chrono::{DateTime, NaiveTime, offset::Utc};
use clap::{CommandFactory, FromArgMatches, Parser};
use crate::{
Save,
Load,
SOP,
};
#[cfg(feature = "cli")]
use crate::{
ArmorLabel,
EncryptAs,
InlineSignAs,
Password,
SignAs,
};
pub const LAST_UPDATE_TIMESTAMP: &'static str =
"February 2025";
#[derive(Debug, Parser)]
#[clap(about = "An implementation of the Stateless OpenPGP Command Line Interface")]
#[clap(disable_version_flag(true))]
struct Cli<O: clap::Subcommand> {
#[clap(long = "debug", global = true)]
debug: bool,
#[clap(subcommand)]
operation: O,
}
#[derive(Clone, Copy)]
pub enum Variant {
Full,
Verification,
}
impl<O: clap::Subcommand> Cli<O> {
fn with_version<'v, V>(v: &V, variant: Variant)
-> Result<clap::Command>
where
V: crate::ops::Version<'v> + ?Sized
{
let (name, version, about) = {
let v = v.frontend()?;
(
v.name.clone(),
v.version.clone(),
format!("An implementation of the {}\
Stateless OpenPGP Command \
Line Interface using {} {}",
match variant {
Variant::Full => "",
Variant::Verification =>
"verification-only subset of the ",
},
v.name, v.version),
)
};
Ok(Cli::<O>::command()
.name(Box::leak(name.into_boxed_str()) as &str)
.about(Box::leak(about.into_boxed_str()) as &str)
.version(Box::leak(version.into_boxed_str()) as &str))
}
}
#[derive(Debug, Parser)]
enum VerificationSubset {
#[clap(display_order = 100)]
#[cfg(any(feature = "cliv", feature = "cli"))]
Version {
#[clap(long, conflicts_with("extended"), conflicts_with("sop_spec"), conflicts_with("sopv"))]
backend: bool,
#[clap(long, conflicts_with("backend"), conflicts_with("sop_spec"), conflicts_with("sopv"))]
extended: bool,
#[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sopv"))]
sop_spec: bool,
#[clap(long, conflicts_with("backend"), conflicts_with("extended"), conflicts_with("sop_spec"))]
sopv: bool,
},
#[clap(display_order = 500)]
#[cfg(any(feature = "cli", feature = "cliv"))]
Verify {
#[clap(long)]
not_before: Option<NotBefore>,
#[clap(long)]
not_after: Option<NotAfter>,
signatures: String,
certs: Vec<String>,
},
#[clap(display_order = 751)]
#[cfg(any(feature = "cli", feature = "cliv"))]
InlineVerify {
#[clap(long)]
not_before: Option<NotBefore>,
#[clap(long)]
not_after: Option<NotAfter>,
#[clap(long)]
verifications_out: Option<String>,
certs: Vec<String>,
},
}
#[derive(Debug, Parser)]
enum Operation {
#[command(flatten)]
VerificationSubset(VerificationSubset),
#[clap(display_order = 150)]
#[cfg(feature = "cli")]
ListProfiles {
subcommand: String,
},
#[clap(display_order = 200)]
#[cfg(feature = "cli")]
GenerateKey {
#[clap(long)]
no_armor: bool,
#[clap(long)]
profile: Option<String>,
#[clap(long)]
signing_only: bool,
#[clap(long)]
with_key_password: Option<String>,
userids: Vec<String>,
},
#[clap(display_order = 210)]
#[cfg(feature = "cli")]
ChangeKeyPassword {
#[clap(long)]
no_armor: bool,
#[clap(long)]
new_key_password: Option<String>,
#[clap(long, number_of_values = 1)]
old_key_password: Vec<String>,
},
#[clap(display_order = 220)]
#[cfg(feature = "cli")]
RevokeKey {
#[clap(long)]
no_armor: bool,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
},
#[clap(display_order = 230)]
#[cfg(feature = "cli")]
ExtractCert {
#[clap(long)]
no_armor: bool,
},
#[clap(display_order = 240)]
#[cfg(feature = "cli")]
UpdateKey {
#[clap(long)]
no_armor: bool,
#[clap(long)]
signing_only: bool,
#[clap(long)]
no_added_capabilities: bool,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
#[clap(long, number_of_values = 1)]
merge_certs: Vec<String>,
},
#[clap(display_order = 250)]
#[cfg(feature = "cli")]
MergeCerts {
#[clap(long)]
no_armor: bool,
certs: Vec<String>,
},
#[clap(display_order = 300)]
#[cfg(feature = "cli")]
CertifyUserid {
#[clap(long)]
no_armor: bool,
#[clap(long, required = true, number_of_values = 1)]
userid: Vec<String>,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
#[clap(long)]
no_require_self_sig: bool,
keys: Vec<String>,
},
#[clap(display_order = 320)]
#[cfg(feature = "cli")]
ValidateUserid {
#[clap(long)]
addr_spec_only: bool,
#[clap(long)]
validate_at: Option<Timestamp>,
userid: String,
#[clap(required = true)]
certs: Vec<String>,
},
#[clap(display_order = 400)]
#[cfg(feature = "cli")]
Sign {
#[clap(long)]
no_armor: bool,
#[clap(default_value = "binary", long = "as")]
as_: SignAs,
#[clap(long)]
micalg_out: Option<String>,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
keys: Vec<String>,
},
#[clap(display_order = 600)]
#[cfg(feature = "cli")]
Encrypt {
#[clap(long)]
no_armor: bool,
#[clap(long)]
profile: Option<String>,
#[clap(default_value = "binary", long = "as")]
as_: EncryptAs,
#[clap(long, number_of_values = 1)]
with_password: Vec<String>,
#[clap(long, number_of_values = 1)]
sign_with: Vec<String>,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
#[clap(long)]
session_key_out: Option<String>,
certs: Vec<String>,
},
#[clap(display_order = 700)]
#[cfg(feature = "cli")]
Decrypt {
#[clap(long)]
session_key_out: Option<String>,
#[clap(long, number_of_values = 1)]
with_session_key: Vec<String>,
#[clap(long, number_of_values = 1)]
with_password: Vec<String>,
#[clap(long, alias("verify-out"))]
verifications_out: Option<String>,
#[clap(long, number_of_values = 1)]
verify_with: Vec<String>,
#[clap(long)]
verify_not_before: Option<NotBefore>,
#[clap(long)]
verify_not_after: Option<NotAfter>,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
keys: Vec<String>,
},
#[clap(display_order = 800)]
#[cfg(feature = "cli")]
Armor {
#[clap(long, default_value = "auto", hide(true))]
label: ArmorLabel,
},
#[clap(display_order = 900)]
#[cfg(feature = "cli")]
Dearmor {
},
#[clap(display_order = 750)]
#[cfg(feature = "cli")]
InlineDetach {
#[clap(long)]
no_armor: bool,
#[clap(long)]
signatures_out: String,
},
#[clap(display_order = 752)]
#[cfg(feature = "cli")]
InlineSign {
#[clap(long)]
no_armor: bool,
#[clap(default_value = "binary", long = "as")]
as_: InlineSignAs,
#[clap(long, number_of_values = 1)]
with_key_password: Vec<String>,
keys: Vec<String>,
},
#[clap(external_subcommand)]
Unsupported(Vec<String>),
}
#[deprecated(note="Use write_shell_completions2.")]
pub fn write_shell_completions<B, P>(binary_name: B,
out_path: P)
-> io::Result<()>
where B: AsRef<str>,
P: AsRef<Path>,
{
let variant = if binary_name.as_ref().ends_with("v") {
Variant::Verification
} else {
Variant::Full
};
write_shell_completions2(variant, binary_name, out_path)
}
pub fn write_shell_completions2<B, P>(variant: Variant,
binary_name: B,
out_path: P)
-> io::Result<()>
where
B: AsRef<str>,
P: AsRef<Path>,
{
use clap_complete::{generate_to, Shell};
let binary_name = binary_name.as_ref();
let out_path = out_path.as_ref();
std::fs::create_dir_all(&out_path)?;
const SHELLS: &[Shell] = &[
Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell,
Shell::Elvish,
];
match variant {
Variant::Verification => {
let mut clap = Cli::<VerificationSubset>::command();
for shell in SHELLS {
generate_to(*shell, &mut clap, binary_name, out_path)?;
}
},
Variant::Full => {
let mut clap = Cli::<Operation>::command();
for shell in SHELLS {
generate_to(*shell, &mut clap, binary_name, out_path)?;
}
},
}
println!("cargo:warning={}: shell completions written to {}",
binary_name,
out_path.display());
Ok(())
}
pub fn write_man_pages<A, P, V>(variant: Variant,
version: &V,
author: A,
out_path: P)
-> Result<()>
where
V: for <'a> crate::ops::Version<'a>,
A: ToString,
P: AsRef<Path>,
{
let out_path = out_path.as_ref();
std::fs::create_dir_all(&out_path)?;
match variant {
Variant::Verification => {
let cmd = Cli::<VerificationSubset>::with_version(version, variant)?
.author(Box::leak(author.to_string().into_boxed_str()) as &str);
generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?;
},
Variant::Full => {
let cmd = Cli::<Operation>::with_version(version, variant)?
.author(Box::leak(author.to_string().into_boxed_str()) as &str);
generate_to(cmd, LAST_UPDATE_TIMESTAMP, out_path)?;
},
}
println!("cargo:warning={}: man pages written to {}",
version.frontend()?.name,
out_path.display());
Ok(())
}
fn generate_to(
cmd: clap::Command,
date: &str,
out_dir: impl AsRef<std::path::Path>,
) -> Result<(), std::io::Error> {
fn generate(
cmd: clap::Command,
source: &str,
date: &str,
out_dir: &std::path::Path,
) -> Result<(), std::io::Error> {
for cmd in cmd.get_subcommands().filter(|s| !s.is_hide_set()).cloned() {
generate(cmd, source, date, out_dir)?;
}
clap_mangen::Man::new(cmd)
.source(source) .date(date) .generate_to(out_dir)?;
Ok(())
}
let source = format!(
"{} {}",
cmd.get_name(),
cmd.get_version().unwrap_or_default()
);
let mut cmd = cmd.disable_help_subcommand(true);
cmd.build();
generate(cmd, &source, date, out_dir.as_ref())
}
pub fn main<'s, S, C, K, Sigs>(sop: &'s mut S, variant: Variant)
-> !
where
S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>,
C: Load<'s, S> + Save,
K: Load<'s, S> + Save,
Sigs: Load<'s, S> + Save,
{
use std::process::exit;
match real_main::<S, C, K, Sigs>(sop, variant) {
Ok(()) => exit(0),
Err(e) => {
print_error_chain(&e);
let e = match e.downcast::<crate::Error>() {
Ok(e) => exit(Error::from(e).into()),
Err(e) => e,
};
let e = match e.downcast::<Error>() {
Ok(e) => exit(e.into()),
Err(e) => e,
};
let _ = e;
exit(1);
},
}
}
fn real_main<'s, S, C, K, Sigs>(sop: &'s mut S, variant: Variant)
-> Result<()>
where
S: SOP<'s, Certs = C, Keys = K, Sigs = Sigs>,
C: Load<'s, S> + Save,
K: Load<'s, S> + Save,
Sigs: Load<'s, S> + Save,
{
#[cfg(feature = "cli")]
if let Variant::Verification = variant {
let app = Cli::<VerificationSubset>::with_version(
sop.version()?.as_ref(), Variant::Verification)?;
let matches = match app.try_get_matches() {
Ok(v) => v,
Err(e) => {
use clap::error::ErrorKind;
return match e.kind() {
ErrorKind::DisplayHelp => {
e.exit()
},
ErrorKind::UnknownArgument =>
Err(anyhow::Error::from(Error::UnsupportedOption)
.context(format!("{}", e))),
_ => Err(e.into()),
};
},
};
Cli::<VerificationSubset>::from_arg_matches(&matches)?;
}
let app = Cli::<Operation>::with_version(sop.version()?.as_ref(), variant)?;
let matches = match app.clone().try_get_matches() {
Ok(v) => v,
Err(e) => {
use clap::error::ErrorKind;
return match e.kind() {
ErrorKind::DisplayHelp => {
e.exit()
},
ErrorKind::UnknownArgument =>
Err(anyhow::Error::from(Error::UnsupportedOption)
.context(format!("{}", e))),
_ => Err(e.into()),
};
},
};
let cli = Cli::from_arg_matches(&matches)?;
sop.debug(cli.debug);
match cli.operation {
Operation::VerificationSubset(VerificationSubset::Version {
backend, extended, sop_spec, sopv,
}) => {
let version = sop.version()?;
match (backend, extended, sop_spec, sopv) {
(false, false, false, false) => {
let v = version.frontend()?;
println!("{} {}", v.name, v.version);
},
(true, false, false, false) => {
let v = version.backend()?;
println!("{} {}", v.name, v.version);
},
(false, true, false, false) => {
let v = version.frontend()?;
println!("{} {}", v.name, v.version);
println!("sop-rs {}", env!("CARGO_PKG_VERSION"));
println!("{}", version.extended()?);
},
(false, false, true, false) => {
println!("{}", sop.spec_version());
},
(false, false, false, true) => {
println!("{}", sop.sopv_version()?);
},
_ => unreachable!("flags are mutually exclusive"),
}
},
#[cfg(feature = "cli")]
Operation::ListProfiles { subcommand, } => {
let profiles = match subcommand.as_str() {
"generate-key" => sop.generate_key()?.list_profiles(),
"encrypt" => sop.encrypt()?.list_profiles(),
_ => return Err(Error::UnsupportedProfile.into()),
};
for (p, d) in profiles {
println!("{}: {}", p, d);
}
},
#[cfg(feature = "cli")]
Operation::GenerateKey {
no_armor,
profile,
signing_only,
with_key_password,
userids,
} => {
let mut op = sop.generate_key()?;
if let Some(p) = profile {
op = op.profile(&p)?;
}
if signing_only {
op = op.signing_only();
}
if let Some(p) = with_key_password {
op = op.with_key_password(get_password(p)?)?;
}
for u in userids {
op = op.userid(&u);
}
op.generate()?
.to_stdout(! no_armor)?;
},
#[cfg(feature = "cli")]
Operation::ChangeKeyPassword {
no_armor,
new_key_password,
old_key_password,
} => {
let mut op = sop.change_key_password()?;
if let Some(p) = new_key_password {
op = op.new_key_password(get_password(p)?)?;
}
for p in old_key_password {
op = op.old_key_password(get_password(p)?)?;
}
op.keys(&K::from_stdin(sop)?)?
.to_stdout(! no_armor)?;
}
#[cfg(feature = "cli")]
Operation::RevokeKey { no_armor, with_key_password, } => {
let mut op = sop.revoke_key()?;
for p in with_key_password {
op = op.with_key_password(get_password(p)?)?;
}
op.keys(&K::from_stdin(sop)?)?
.to_stdout(! no_armor)?;
}
#[cfg(feature = "cli")]
Operation::ExtractCert { no_armor, } => {
sop.extract_cert()?
.keys(&K::from_stdin(sop)?)?
.to_stdout(! no_armor)?;
},
#[cfg(feature = "cli")]
Operation::UpdateKey {
no_armor, signing_only, no_added_capabilities,
with_key_password, merge_certs,
} => {
let mut op = sop.update_key()?;
if signing_only {
op = op.signing_only();
}
if no_added_capabilities {
op = op.no_added_capabilities();
}
for p in with_key_password {
op = op.with_key_password(get_password(p)?)?;
}
for (name, mut stream) in load_files(merge_certs)? {
op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
let keys = op.update(&K::from_stdin(sop)?)?;
keys.to_stdout(! no_armor)?;
},
#[cfg(feature = "cli")]
Operation::MergeCerts { no_armor, certs, } => {
let mut op = sop.merge_certs()?;
for (name, mut stream) in load_files(certs)? {
op = op.merge_updates(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
let certs = op.merge(&C::from_stdin(sop)?)?;
certs.to_stdout(! no_armor)?;
},
#[cfg(feature = "cli")]
Operation::CertifyUserid {
no_armor, userid, with_key_password, no_require_self_sig, keys,
} => {
let mut op = sop.certify_userid()?;
if no_require_self_sig {
op = op.no_require_self_sig();
}
for u in userid {
op = op.userid(u);
}
for p in with_key_password {
op = op.with_key_password(get_password(p)?)?;
}
for (name, mut stream) in load_files(keys)? {
op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
}
let certs = op.certify(&C::from_stdin(sop)?)?;
certs.to_stdout(! no_armor)?;
},
#[cfg(feature = "cli")]
Operation::ValidateUserid {
addr_spec_only, validate_at, userid, certs,
} => {
let mut op = sop.validate_userid()?;
if let Some(t) = validate_at {
op = op.validate_at(t.into())?;
}
for (name, mut stream) in load_files(certs)? {
op = op.trust_roots(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
op = op.target_certs(&C::from_stdin(sop)?)?;
if addr_spec_only {
op.email(&userid)?;
} else {
op.userid(&userid)?;
}
},
#[cfg(feature = "cli")]
Operation::Sign {
no_armor,
as_,
micalg_out,
with_key_password,
keys,
} => {
let mut op = sop.sign()?.mode(as_);
for p in with_key_password {
op = op.with_key_password(get_password_unchecked(p)?)?;
}
for (name, mut stream) in load_files(keys)? {
op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
}
let (micalg, sigs) = op.data(&mut io::stdin())?;
if let Some(path) = micalg_out {
let mut sink = create_file(path)?;
write!(sink, "{}", micalg)?;
}
sigs.to_stdout(! no_armor)?;
},
#[cfg(any(feature = "cli", feature = "cliv"))]
Operation::VerificationSubset(VerificationSubset::Verify {
not_before, not_after, signatures, certs,
}) => {
let mut op = sop.verify()?;
if let Some(t) = not_before {
op = op.not_before(t.into());
}
if let Some(t) = not_after {
op = op.not_after(t.into());
}
for (name, mut stream) in load_files(certs)? {
op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
let (name, mut stream) = load_file(signatures)?;
let verifications =
op.signatures(&Sigs::from_reader(sop, &mut stream, Some(name))?)?
.data(&mut io::stdin())?;
for v in verifications {
println!("{}", v);
}
},
#[cfg(feature = "cli")]
Operation::Encrypt {
no_armor,
profile,
as_,
with_password,
with_key_password,
sign_with,
session_key_out,
certs,
} =>
{
let session_key_out: Option<Box<dyn io::Write>> =
if let Some(f) = session_key_out {
Some(Box::new(create_file(f)?))
} else {
None
};
let mut op = sop.encrypt()?.mode(as_);
if no_armor {
op = op.no_armor();
}
if let Some(p) = profile {
op = op.profile(&p)?;
}
for p in with_key_password {
op = op.with_key_password(get_password_unchecked(p)?)?;
}
for (name, mut stream) in load_files(sign_with)? {
op = op.sign_with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
}
for pw in with_password.into_iter().map(get_password) {
op = op.with_password(pw?)?;
}
for (name, mut stream) in load_files(certs)? {
op = op.with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
let session_key =
op.plaintext(&mut io::stdin())?.to_writer(&mut io::stdout())?;
if let Some(mut sko) = session_key_out {
if let Some(sk) = session_key {
writeln!(sko, "{}", sk)?;
} else {
return Err(Error::UnsupportedSubcommand.into());
}
}
},
#[cfg(feature = "cli")]
Operation::Decrypt {
session_key_out,
with_session_key,
with_password,
verifications_out,
verify_with,
verify_not_before,
verify_not_after,
with_key_password,
keys,
} => {
let session_key_out: Option<Box<dyn io::Write>> =
if let Some(f) = session_key_out {
Some(Box::new(create_file(f)?))
} else {
None
};
if verifications_out.is_none() != verify_with.is_empty() {
return Err(anyhow::Error::from(Error::IncompleteVerification))
.context("--verifications-out and --verify-with \
must both be given");
}
let mut verify_out: Box<dyn io::Write> =
if let Some(f) = verifications_out {
Box::new(create_file(f)?)
} else {
Box::new(io::sink())
};
let mut op = sop.decrypt()?;
for (_name, mut stream) in load_files(with_session_key)? {
let mut sk = String::new();
stream.read_to_string(&mut sk)?;
op = op.with_session_key(sk.parse()?)?;
}
for pw in with_password.into_iter().map(get_password_unchecked) {
op = op.with_password(pw?)?;
}
for p in with_key_password {
op = op.with_key_password(get_password_unchecked(p)?)?;
}
for (name, mut stream) in load_files(keys)? {
op = op.with_keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
}
for (name, mut stream) in load_files(verify_with)? {
op = op.verify_with_certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
if let Some(t) = verify_not_before {
op = op.verify_not_before(t.into());
}
if let Some(t) = verify_not_after {
op = op.verify_not_after(t.into());
}
let (session_key, verifications) =
op.ciphertext(&mut io::stdin())?.to_writer(&mut io::stdout())?;
for v in verifications {
writeln!(verify_out, "{}", v)?;
}
if let Some(mut sko) = session_key_out {
if let Some(sk) = session_key {
writeln!(sko, "{}", sk)?;
} else {
return Err(Error::UnsupportedSubcommand.into());
}
}
},
#[cfg(feature = "cli")]
Operation::Armor { label, } => {
#[allow(deprecated)]
let op = sop.armor()?.label(label);
op.data(&mut io::stdin())?
.to_writer(&mut io::stdout())?;
},
#[cfg(feature = "cli")]
Operation::Dearmor {} => {
let op = sop.dearmor()?;
op.data(&mut io::stdin())?
.to_writer(&mut io::stdout())?;
},
#[cfg(feature = "cli")]
Operation::InlineDetach { no_armor, signatures_out, } => {
let op = sop.inline_detach()?;
let mut signatures_out = create_file(signatures_out)?;
let signatures =
op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?;
signatures.to_writer(! no_armor, &mut signatures_out)?;
},
#[cfg(any(feature = "cli", feature = "cliv"))]
Operation::VerificationSubset(VerificationSubset::InlineVerify {
not_before,
not_after,
verifications_out,
certs,
}) => {
let mut op = sop.inline_verify()?;
if let Some(t) = not_before {
op = op.not_before(t.into());
}
if let Some(t) = not_after {
op = op.not_after(t.into());
}
let mut verifications_out: Box<dyn io::Write> =
if let Some(f) = verifications_out {
Box::new(create_file(f)?)
} else {
Box::new(io::sink())
};
for (name, mut stream) in load_files(certs)? {
op = op.certs(&C::from_reader(sop, &mut stream, Some(name))?)?;
}
let verifications =
op.message(&mut io::stdin())?.to_writer(&mut io::stdout())?;
for v in verifications {
writeln!(&mut verifications_out, "{}", v)?;
}
},
#[cfg(feature = "cli")]
Operation::InlineSign { no_armor, as_, with_key_password, keys, } => {
let mut op = sop.inline_sign()?.mode(as_);
if no_armor {
op = op.no_armor();
}
for p in with_key_password {
op = op.with_key_password(get_password_unchecked(p)?)?;
}
for (name, mut stream) in load_files(keys)? {
op = op.keys(&K::from_reader(sop, &mut stream, Some(name))?)?;
}
op.data(&mut io::stdin())?.to_writer(&mut io::stdout())?;
},
Operation::Unsupported(args) => {
return Err(anyhow::Error::from(Error::UnsupportedSubcommand))
.context(format!("Subcommand {} is not supported", args[0]));
},
}
Ok(())
}
fn is_special_designator<S: AsRef<str>>(file: S) -> bool {
file.as_ref().starts_with("@")
}
#[cfg(unix)]
fn check_fd(fd: std::os::fd::RawFd) -> Result<()> {
unsafe {
let dup = libc::dup(fd);
if dup > 0 {
libc::close(dup);
Ok(())
} else {
Err(std::io::Error::last_os_error().into())
}
}
}
fn load_file<S: AsRef<str>>(file: S)
-> Result<(String, Box<dyn io::Read + Send + Sync>)> {
let f = file.as_ref();
let file_name = f.into();
if is_special_designator(f) {
if Path::new(f).exists() {
return Err(anyhow::Error::from(Error::AmbiguousInput))
.context(format!("File {:?} exists", f));
}
#[cfg(unix)]
{
if f.starts_with("@FD:")
&& f[4..].chars().all(|c| c.is_ascii_digit())
{
use std::os::unix::io::{RawFd, FromRawFd};
let fd: RawFd = f[4..].parse()
.map_err(|_| Error::UnsupportedSpecialPrefix)?;
check_fd(fd)?;
let f = unsafe {
std::fs::File::from_raw_fd(fd)
};
return Ok((file_name, Box::new(f)));
}
if f.starts_with("@ENV:") {
use std::os::unix::ffi::OsStringExt;
let key = &f[5..];
let value = std::env::var_os(key)
.ok_or(Error::UnsupportedSpecialPrefix)?;
std::env::remove_var(key);
return Ok((file_name,
Box::new(io::Cursor::new(value.into_vec()))));
}
}
#[cfg(windows)]
{
if f.starts_with("@ENV:") {
let key = &f[5..];
let value = std::env::var(key)
.map_err(|_| Error::UnsupportedSpecialPrefix)?;
std::env::remove_var(key);
return Ok((file_name,
Box::new(io::Cursor::new(value.into_bytes()))));
}
}
return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
}
std::fs::File::open(f)
.map(|f| -> (String, Box<dyn io::Read + Send + Sync>) {
(file_name, Box::new(f))
})
.map_err(|_| Error::MissingInput)
.context(format!("Failed to open file {:?}", f))
}
#[cfg(feature = "cli")]
fn get_password<S: AsRef<str>>(location: S) -> Result<Password> {
let mut stream = load_file(location)?.1;
let mut pw = Vec::new();
stream.read_to_end(&mut pw)?;
Ok(Password::new(pw)?)
}
#[cfg(feature = "cli")]
fn get_password_unchecked<S: AsRef<str>>(location: S) -> Result<Password> {
let mut stream = load_file(location)?.1;
let mut pw = Vec::new();
stream.read_to_end(&mut pw)?;
Ok(Password::new_unchecked(pw))
}
fn create_file<S: AsRef<str>>(file: S) -> Result<std::fs::File> {
let f = file.as_ref();
if is_special_designator(f) {
if Path::new(f).exists() {
return Err(anyhow::Error::from(Error::AmbiguousInput))
.context(format!("File {:?} exists", f));
}
#[cfg(unix)]
{
if f.starts_with("@FD:")
&& f[4..].chars().all(|c| c.is_ascii_digit())
{
use std::os::unix::io::{RawFd, FromRawFd};
let fd: RawFd = f[4..].parse()
.map_err(|_| Error::UnsupportedSpecialPrefix)?;
check_fd(fd)?;
let f = unsafe {
std::fs::File::from_raw_fd(fd)
};
return Ok(f);
}
}
return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix));
}
if Path::new(f).exists() {
return Err(anyhow::Error::from(Error::OutputExists))
.context(format!("File {:?} exists", f));
}
std::fs::File::create(f).map_err(|_| Error::MissingInput) .context(format!("Failed to create file {:?}", f))
}
fn load_files(files: Vec<String>)
-> Result<Vec<(String, Box<dyn io::Read + Send + Sync>)>> {
files.iter().map(load_file).collect()
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct NotBefore(DateTime<Utc>);
impl std::str::FromStr for NotBefore {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<NotBefore> {
match s {
"now" => Ok(Utc::now()),
_ => parse_iso8601(s, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
}.map(|t| NotBefore(t))
}
}
impl From<NotBefore> for std::time::SystemTime {
fn from(t: NotBefore) -> std::time::SystemTime {
t.0.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct NotAfter(DateTime<Utc>);
impl std::str::FromStr for NotAfter {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<NotAfter> {
match s {
"now" => Ok(Utc::now()),
_ => parse_iso8601(s, NaiveTime::from_hms_opt(23, 59, 59).unwrap()),
}.map(|t| NotAfter(t))
}
}
impl From<NotAfter> for std::time::SystemTime {
fn from(t: NotAfter) -> std::time::SystemTime {
t.0.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Timestamp(DateTime<Utc>);
impl std::str::FromStr for Timestamp {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Timestamp> {
match s {
"now" => Ok(Utc::now()),
_ => parse_iso8601(s, NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
}.map(|t| Timestamp(t))
}
}
impl From<Timestamp> for std::time::SystemTime {
fn from(t: Timestamp) -> std::time::SystemTime {
t.0.into()
}
}
fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime)
-> Result<DateTime<Utc>>
{
for f in &[
"%Y-%m-%dT%H:%M:%S%#z",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M%#z",
"%Y-%m-%dT%H:%M",
"%Y-%m-%dT%H%#z",
"%Y-%m-%dT%H",
"%Y%m%dT%H%M%S%#z",
"%Y%m%dT%H%M%S",
"%Y%m%dT%H%M%#z",
"%Y%m%dT%H%M",
"%Y%m%dT%H%#z",
"%Y%m%dT%H",
] {
if f.ends_with("%#z") {
if let Ok(d) = DateTime::parse_from_str(s, *f) {
return Ok(d.into());
}
} else {
if let Ok(d) = chrono::NaiveDateTime::parse_from_str(s, *f) {
return Ok(DateTime::from_utc(d, Utc));
}
}
}
for f in &[
"%Y-%m-%d",
"%Y-%m",
"%Y-%j",
"%Y%m%d",
"%Y%m",
"%Y%j",
"%Y",
] {
if let Ok(d) = chrono::NaiveDate::parse_from_str(s, *f) {
return Ok(DateTime::from_utc(d.and_time(pad_date_with), Utc));
}
}
Err(anyhow::anyhow!("Malformed ISO8601 timestamp: {}", s))
}
#[test]
fn test_parse_iso8601() {
let z = chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap();
parse_iso8601("2017-03-04T13:25:35Z", z).unwrap();
parse_iso8601("2017-03-04T13:25:35+08:30", z).unwrap();
parse_iso8601("2017-03-04T13:25:35", z).unwrap();
parse_iso8601("2017-03-04T13:25Z", z).unwrap();
parse_iso8601("2017-03-04T13:25", z).unwrap();
parse_iso8601("2017-03-04", z).unwrap();
parse_iso8601("2017-031", z).unwrap();
parse_iso8601("20170304T132535Z", z).unwrap();
parse_iso8601("20170304T132535+0830", z).unwrap();
parse_iso8601("20170304T132535", z).unwrap();
parse_iso8601("20170304T1325Z", z).unwrap();
parse_iso8601("20170304T1325", z).unwrap();
parse_iso8601("20170304", z).unwrap();
parse_iso8601("2017031", z).unwrap();
}
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("Unspecified failure")]
UnspecifiedFailure,
#[error("No acceptable signatures found")]
NoSignature,
#[error("Asymmetric algorithm unsupported")]
UnsupportedAsymmetricAlgo,
#[error("Certificate not encryption-capable")]
CertCannotEncrypt,
#[error("Key not signing-capable")]
KeyCannotSign,
#[error("Missing required argument")]
MissingArg,
#[error("Incomplete verification instructions")]
IncompleteVerification,
#[error("Unable to decrypt")]
CannotDecrypt,
#[error("Non-UTF-8 or otherwise unreliable password")]
PasswordNotHumanReadable,
#[error("Unsupported option")]
UnsupportedOption,
#[error("Invalid data type")]
BadData,
#[error("Non-text input where text expected")]
ExpectedText,
#[error("Output file already exists")]
OutputExists,
#[error("Input file does not exist")]
MissingInput,
#[error("A KEY input is protected with a password")]
KeyIsProtected,
#[error("Unsupported subcommand")]
UnsupportedSubcommand,
#[error("An indirect parameter is a special designator with unknown prefix")]
UnsupportedSpecialPrefix,
#[error("A indirect input parameter is a special designator matches file")]
AmbiguousInput,
#[error("Options were supplied that are incompatible with each other")]
IncompatibleOptions,
#[error("Profile not supported")]
UnsupportedProfile,
#[error("The primary key of a KEYS object is too weak or revoked")]
PrimaryKeyBad,
#[error("The CERTS object has no matching User ID")]
CertUseridNoMatch,
#[error(transparent)]
IoError(#[from] io::Error),
}
impl From<crate::Error> for Error {
fn from(e: crate::Error) -> Self {
use crate::Error as E;
use Error as CE;
match e {
E::UnspecifiedFailure => CE::UnspecifiedFailure,
E::NoSignature => CE::NoSignature,
E::UnsupportedAsymmetricAlgo => CE::UnsupportedAsymmetricAlgo,
E::CertCannotEncrypt => CE::CertCannotEncrypt,
E::KeyCannotSign => CE::KeyCannotSign,
E::MissingArg => CE::MissingArg,
E::IncompleteVerification => CE::IncompleteVerification,
E::CannotDecrypt => CE::CannotDecrypt,
E::PasswordNotHumanReadable => CE::PasswordNotHumanReadable,
E::UnsupportedOption => CE::UnsupportedOption,
E::BadData => CE::BadData,
E::ExpectedText => CE::ExpectedText,
E::OutputExists => CE::OutputExists,
E::MissingInput => CE::MissingInput,
E::KeyIsProtected => CE::KeyIsProtected,
E::AmbiguousInput => CE::AmbiguousInput,
E::IncompatibleOptions => CE::IncompatibleOptions,
E::NotImplemented => CE::UnsupportedSubcommand,
E::UnsupportedProfile => CE::UnsupportedProfile,
E::PrimaryKeyBad => CE::PrimaryKeyBad,
E::CertUseridNoMatch => CE::CertUseridNoMatch,
E::IoError(e) => CE::IoError(e),
}
}
}
impl From<Error> for i32 {
fn from(e: Error) -> Self {
use Error::*;
match e {
UnspecifiedFailure => 1,
NoSignature => 3,
UnsupportedAsymmetricAlgo => 13,
CertCannotEncrypt => 17,
MissingArg => 19,
IncompleteVerification => 23,
CannotDecrypt => 29,
PasswordNotHumanReadable => 31,
UnsupportedOption => 37,
BadData => 41,
ExpectedText => 53,
OutputExists => 59,
MissingInput => 61,
KeyIsProtected => 67,
UnsupportedSubcommand => 69,
UnsupportedSpecialPrefix => 71,
AmbiguousInput => 73,
KeyCannotSign => 79,
IncompatibleOptions => 83,
UnsupportedProfile => 89,
PrimaryKeyBad => 103,
CertUseridNoMatch => 107,
IoError(_) => 1,
}
}
}
fn print_error_chain(err: &anyhow::Error) {
eprintln!(" {}", err);
err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause));
}