#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
#![doc = include_str!("../sq-usage.md")]
use anyhow::Context as _;
use std::fs::OpenOptions;
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::time::Duration;
use chrono::{DateTime, offset::Utc};
use itertools::Itertools;
use buffered_reader::{BufferedReader, Dup, File, Generic, Limitor};
use sequoia_openpgp as openpgp;
use openpgp::{
Result,
};
use openpgp::{armor, Cert};
use openpgp::crypto::Password;
use openpgp::packet::prelude::*;
use openpgp::parse::{Parse, PacketParser, PacketParserResult};
use openpgp::packet::signature::subpacket::NotationData;
use openpgp::packet::signature::subpacket::NotationDataFlags;
use openpgp::serialize::{Serialize, stream::{Message, Armorer}};
use openpgp::cert::prelude::*;
use openpgp::policy::StandardPolicy as P;
use clap::FromArgMatches;
use crate::sq_cli::packet;
use sq_cli::SqSubcommands;
mod sq_cli;
mod man;
mod commands;
pub mod output;
pub use output::{wkd::WkdUrlVariant, Model, OutputFormat, OutputVersion};
fn open_or_stdin(f: Option<&str>)
-> Result>> {
match f {
Some(f) => Ok(Box::new(File::open(f)
.context("Failed to open input file")?)),
None => Ok(Box::new(Generic::new(io::stdin(), None))),
}
}
const SECONDS_IN_DAY : u64 = 24 * 60 * 60;
const SECONDS_IN_YEAR : u64 =
// Average number of days in a year.
(365.2422222 * SECONDS_IN_DAY as f64) as u64;
fn parse_duration(expiry: &str) -> Result {
let mut expiry = expiry.chars().peekable();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let digits = expiry.by_ref()
.peeking_take_while(|c| {
*c == '+' || *c == '-' || c.is_digit(10)
}).collect::();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let suffix = expiry.next();
let _ = expiry.by_ref()
.peeking_take_while(|c| c.is_whitespace())
.for_each(|_| ());
let junk = expiry.collect::();
if digits.is_empty() {
return Err(anyhow::anyhow!(
"--expiry: missing count \
(try: '2y' for 2 years)"));
}
let count = match digits.parse::() {
Ok(count) if count < 0 =>
return Err(anyhow::anyhow!(
"--expiry: Expiration can't be in the past")),
Ok(count) => count as u64,
Err(err) =>
return Err(err).context("--expiry: count is out of range"),
};
let factor = match suffix {
Some('y') | Some('Y') => SECONDS_IN_YEAR,
Some('m') | Some('M') => SECONDS_IN_YEAR / 12,
Some('w') | Some('W') => 7 * SECONDS_IN_DAY,
Some('d') | Some('D') => SECONDS_IN_DAY,
Some('s') | Some('S') => 1,
None =>
return Err(anyhow::anyhow!(
"--expiry: missing suffix \
(try: '{}y', '{}m', '{}w', '{}d' or '{}s' instead)",
digits, digits, digits, digits, digits)),
Some(suffix) =>
return Err(anyhow::anyhow!(
"--expiry: invalid suffix '{}' \
(try: '{}y', '{}m', '{}w', '{}d' or '{}s' instead)",
suffix, digits, digits, digits, digits, digits)),
};
if !junk.is_empty() {
return Err(anyhow::anyhow!(
"--expiry: contains trailing junk ('{:?}') \
(try: '{}{}')",
junk, count, factor));
}
Ok(Duration::new(count * factor, 0))
}
/// Loads one TSK from every given file.
fn load_keys<'a, I>(files: I) -> openpgp::Result>
where I: Iterator-
{
let mut certs = vec![];
for f in files {
let cert = Cert::from_file(f)
.context(format!("Failed to load key from file {:?}", f))?;
if ! cert.is_tsk() {
return Err(anyhow::anyhow!(
"Cert in file {:?} does not contain secret keys", f));
}
certs.push(cert);
}
Ok(certs)
}
/// Loads one or more certs from every given file.
fn load_certs<'a, I>(files: I) -> openpgp::Result>
where I: Iterator
-
{
let mut certs = vec![];
for f in files {
for maybe_cert in CertParser::from_file(f)
.context(format!("Failed to load certs from file {:?}", f))?
{
certs.push(maybe_cert.context(
format!("A cert from file {:?} is bad", f)
)?);
}
}
Ok(certs)
}
/// Serializes a keyring, adding descriptive headers if armored.
#[allow(dead_code)]
fn serialize_keyring(mut output: &mut dyn io::Write, certs: &[Cert], binary: bool)
-> openpgp::Result<()> {
// Handle the easy options first. No armor no cry:
if binary {
for cert in certs {
cert.serialize(&mut output)?;
}
return Ok(());
}
// Just one Cert? Ez:
if certs.len() == 1 {
return certs[0].armored().serialize(&mut output);
}
// Otherwise, collect the headers first:
let mut headers = Vec::new();
for (i, cert) in certs.iter().enumerate() {
headers.push(format!("Key #{}", i));
headers.append(&mut cert.armor_headers());
}
let headers: Vec<_> = headers.iter()
.map(|value| ("Comment", value.as_str()))
.collect();
let mut output = armor::Writer::with_headers(&mut output,
armor::Kind::PublicKey,
headers)?;
for cert in certs {
cert.serialize(&mut output)?;
}
output.finalize()?;
Ok(())
}
/// How much data to look at when detecting armor kinds.
const ARMOR_DETECTION_LIMIT: u64 = 1 << 24;
/// Peeks at the first packet to guess the type.
///
/// Returns the given reader unchanged. If the detection fails,
/// armor::Kind::File is returned as safe default.
fn detect_armor_kind(
input: Box>,
) -> (Box>, armor::Kind) {
let mut dup =
Limitor::new(Dup::new(input), ARMOR_DETECTION_LIMIT).as_boxed();
let kind = match PacketParser::from_reader(&mut dup) {
Ok(PacketParserResult::Some(pp)) => match pp.next() {
Ok((Packet::Signature(_), _)) => armor::Kind::Signature,
Ok((Packet::SecretKey(_), _)) => armor::Kind::SecretKey,
Ok((Packet::PublicKey(_), _)) => armor::Kind::PublicKey,
Ok((Packet::PKESK(_), _)) => armor::Kind::Message,
Ok((Packet::SKESK(_), _)) => armor::Kind::Message,
_ => armor::Kind::File,
},
_ => armor::Kind::File,
};
(dup.into_inner().unwrap().into_inner().unwrap(), kind)
}
// Decrypts a key, if possible.
//
// The passwords in `passwords` are tried first. If the key can't be
// decrypted using those, the user is prompted. If a valid password
// is entered, it is added to `passwords`.
fn decrypt_key(key: Key, passwords: &mut Vec)
-> Result>
where R: key::KeyRole + Clone
{
let key = key.parts_as_secret()?;
match key.secret() {
SecretKeyMaterial::Unencrypted(_) => {
Ok(key.clone())
}
SecretKeyMaterial::Encrypted(_) => {
for p in passwords.iter() {
if let Ok(key)
= key.clone().decrypt_secret(&Password::from(&p[..]))
{
return Ok(key);
}
}
let mut first = true;
loop {
// Prompt the user.
match rpassword::prompt_password(&format!(
"{}Enter password to unlock {} (blank to skip): ",
if first { "" } else { "Invalid password. " },
key.keyid().to_hex()
)) {
Ok(p) => {
first = false;
if p.is_empty() {
// Give up.
break;
}
if let Ok(key) = key
.clone()
.decrypt_secret(&Password::from(&p[..]))
{
passwords.push(p);
return Ok(key);
}
}
Err(err) => {
eprintln!("While reading password: {}", err);
break;
}
}
}
Err(anyhow::anyhow!("Key {}: Unable to decrypt secret key material",
key.keyid().to_hex()))
}
}
}
/// Prints a warning if the user supplied "help" or "-help" to an
/// positional argument.
///
/// This should be used wherever a positional argument is followed by
/// an optional positional argument.
#[allow(dead_code)]
fn help_warning(arg: &str) {
if arg == "help" {
eprintln!("Warning: \"help\" is not a subcommand here. \
Did you mean --help?");
}
}
/// Prints a warning if sq is run in a non-interactive setting without
/// a terminal.
///
/// Detecting non-interactive use is done using a heuristic.
fn emit_unstable_cli_warning() {
if term_size::dimensions_stdout().is_some() {
// stdout is connected to a terminal, assume interactive use.
return;
}
// For bash shells, we can use a very simple heuristic. We simply
// look at whether the COLUMNS variable is defined in our
// environment.
if std::env::var_os("COLUMNS").is_some() {
// Heuristic detected interactive use.
return;
}
eprintln!("\nWARNING: sq does not have a stable CLI interface. \
Use with caution in scripts.\n");
}
#[derive(Clone)]
pub struct Config<'a> {
force: bool,
output_format: OutputFormat,
output_version: Option,
policy: P<'a>,
/// Have we emitted the warning yet?
unstable_cli_warning_emitted: bool,
}
impl Config<'_> {
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use.
///
/// This is suitable for any kind of OpenPGP data, or decrypted or
/// authenticated payloads.
fn create_or_stdout_safe(&self, f: Option<&str>)
-> Result> {
Config::create_or_stdout(f, self.force)
}
/// Opens the file (or stdout) for writing data that is NOT safe
/// for non-interactive use.
///
/// If our heuristic detects non-interactive use, we will emit a
/// warning.
fn create_or_stdout_unsafe(&mut self, f: Option<&str>)
-> Result> {
if ! self.unstable_cli_warning_emitted {
emit_unstable_cli_warning();
self.unstable_cli_warning_emitted = true;
}
Config::create_or_stdout(f, self.force)
}
/// Opens the file (or stdout) for writing data that is safe for
/// non-interactive use because it is an OpenPGP data stream.
fn create_or_stdout_pgp<'a>(&self, f: Option<&str>,
binary: bool, kind: armor::Kind)
-> Result> {
let sink = self.create_or_stdout_safe(f)?;
let mut message = Message::new(sink);
if ! binary {
message = Armorer::new(message).kind(kind).build()?;
}
Ok(message)
}
/// Helper function, do not use directly. Instead, use create_or_stdout_safe
/// or create_or_stdout_unsafe.
fn create_or_stdout(
f: Option<&str>,
force: bool,
) -> Result> {
match f {
None => Ok(Box::new(io::stdout())),
Some(p) if p == "-" => Ok(Box::new(io::stdout())),
Some(f) => {
let p = Path::new(f);
if !p.exists() || force {
Ok(Box::new(
OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(f)
.context("Failed to create output file")?,
))
} else {
Err(anyhow::anyhow!(format!(
"File {:?} exists, use \"sq --force ...\" to \
overwrite",
p
)))
}
}
}
}
}
// TODO: Use `derive`d command structs. No more values_of
// TODO: Handling (and cli position) of global arguments
fn main() -> Result<()> {
if let Ok(dirname) = std::env::var("SQ_MAN") {
let dirname = PathBuf::from(dirname);
if !dirname.exists() {
std::fs::create_dir(&dirname)?;
}
for man in man::manpages(&sq_cli::build()) {
std::fs::write(dirname.join(man.filename()), man.troff_source())?;
}
return Ok(())
}
let policy = &mut P::new();
let c = sq_cli::SqCommand::from_arg_matches(&sq_cli::build().get_matches())?;
let known_notations = c.known_notation
.iter()
.map(|n| n.as_str())
.collect::>();
policy.good_critical_notations(&known_notations);
let force = c.force;
let output_format = OutputFormat::from_str(&c.output_format)?;
let output_version = if let Some(v) = c.output_version {
Some(OutputVersion::from_str(&v)?)
} else {
None
};
let mut config = Config {
force,
output_format,
output_version,
policy: policy.clone(),
unstable_cli_warning_emitted: false,
};
match c.subcommand {
SqSubcommands::OutputVersions(command) => {
if command.default {
println!("{}", output::DEFAULT_OUTPUT_VERSION);
} else {
for v in output::OUTPUT_VERSIONS {
println!("{}", v);
}
}
}
SqSubcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let certs = load_certs(
command.sender_cert_file.iter().map(|s| s.as_ref()),
)?;
// Fancy default for --signatures. If you change this,
// also change the description in the CLI definition.
let signatures = command.signatures.unwrap_or_else(|| {
if certs.is_empty() {
// No certs are given for verification, use 0 as
// threshold so we handle only-encrypted messages
// gracefully.
0
} else {
// At least one cert given, expect at least one
// valid signature.
1
}
});
// TODO: should this be load_keys?
let secrets =
load_certs(command.secret_key_file.iter().map(|s| s.as_ref()))?;
let private_key_store = command.private_key_store;
let session_keys = command.session_key;
commands::decrypt(config, private_key_store.as_deref(),
&mut input, &mut output,
signatures, certs, secrets,
command.dump_session_key,
session_keys,
command.dump, command.hex)?;
},
SqSubcommands::Encrypt(command) => {
let recipients = load_certs(
command.recipients_cert_file.iter().map(|s| s.as_ref()),
)?;
let mut input = open_or_stdin(command.io.input.as_deref())?;
let output = config.create_or_stdout_pgp(
command.io.output.as_deref(),
command.binary,
armor::Kind::Message,
)?;
let additional_secrets =
load_certs(command.signer_key_file.iter().map(|s| s.as_ref()))?;
let time = command.time.map(|t| t.time.into());
let private_key_store = command.private_key_store.as_deref();
commands::encrypt(commands::EncryptOpts {
policy,
private_key_store,
input: &mut input,
message: output,
npasswords: command.symmetric,
recipients: &recipients,
signers: additional_secrets,
mode: command.mode,
compression: command.compression,
time,
use_expired_subkey: command.use_expired_subkey,
})?;
},
SqSubcommands::Sign(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let output = command.io.output.as_deref();
let detached = command.detached;
let binary = command.binary;
let append = command.append;
let notarize = command.notarize;
let private_key_store = command.private_key_store.as_deref();
let secrets =
load_certs(command.secret_key_file.iter().map(|s| s.as_ref()))?;
let time = command.time.map(|t| t.time.into());
let notations = parse_notations(command.notation.unwrap_or_default())?;
if let Some(merge) = command.merge {
let output = config.create_or_stdout_pgp(output, binary,
armor::Kind::Message)?;
let mut input2 = open_or_stdin(Some(&merge))?;
commands::merge_signatures(&mut input, &mut input2, output)?;
} else if command.clearsign {
let output = config.create_or_stdout_safe(output)?;
commands::sign::clearsign(config, private_key_store, input, output, secrets,
time, ¬ations)?;
} else {
commands::sign(commands::sign::SignOpts {
config,
private_key_store,
input: &mut input,
output_path: output,
secrets,
detached,
binary,
append,
notarize,
time,
notations: ¬ations
})?;
}
},
SqSubcommands::Verify(command) => {
// TODO: Fix interface of open_or_stdin, create_or_stdout_safe, etc.
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut detached = if let Some(f) = command.detached {
Some(File::open(f)?)
} else {
None
};
let signatures = command.signatures;
// TODO ugly adaptation to load_certs' signature, fix later
let certs = load_certs(command.sender_cert_file.iter().map(|s| s.as_ref()))?;
commands::verify(config, &mut input,
detached.as_mut().map(|r| r as &mut (dyn io::Read + Sync + Send)),
&mut output, signatures, certs)?;
},
// TODO: Extract body to commands/armor.rs
SqSubcommands::Armor(command) => {
let input = open_or_stdin(command.io.input.as_deref())?;
let mut want_kind: Option = command.kind.into();
// Peek at the data. If it looks like it is armored
// data, avoid armoring it again.
let mut dup = Limitor::new(Dup::new(input), ARMOR_DETECTION_LIMIT);
let (already_armored, have_kind) = {
let mut reader =
armor::Reader::from_reader(&mut dup,
armor::ReaderMode::Tolerant(None));
(reader.data(8).is_ok(), reader.kind())
};
let mut input =
dup.as_boxed().into_inner().unwrap().into_inner().unwrap();
if already_armored
&& (want_kind.is_none() || want_kind == have_kind)
{
// It is already armored and has the correct kind.
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
io::copy(&mut input, &mut output)?;
return Ok(());
}
if want_kind.is_none() {
let (tmp, kind) = detect_armor_kind(input);
input = tmp;
want_kind = Some(kind);
}
// At this point, want_kind is determined.
let want_kind = want_kind.expect("given or detected");
let mut output =
config.create_or_stdout_pgp(command.io.output.as_deref(),
false, want_kind)?;
if already_armored {
// Dearmor and copy to change the type.
let mut reader =
armor::Reader::from_reader(input,
armor::ReaderMode::Tolerant(None));
io::copy(&mut reader, &mut output)?;
} else {
io::copy(&mut input, &mut output)?;
}
output.finalize()?;
},
SqSubcommands::Dearmor(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output =
config.create_or_stdout_safe(command.io.output.as_deref())?;
let mut filter = armor::Reader::from_reader(&mut input, None);
io::copy(&mut filter, &mut output)?;
},
#[cfg(feature = "autocrypt")]
SqSubcommands::Autocrypt(command) => {
commands::autocrypt::dispatch(config, &command)?;
},
SqSubcommands::Inspect(command) => {
// sq inspect does not have --output, but commands::inspect does.
// Work around this mismatch by always creating a stdout output.
let mut output = config.create_or_stdout_unsafe(None)?;
commands::inspect(command, policy, &mut output)?;
},
SqSubcommands::Keyring(command) => {
commands::keyring::dispatch(config, command)?
},
SqSubcommands::Packet(command) => match command.subcommand {
packet::Subcommands::Dump(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output = config.create_or_stdout_unsafe(
command.io.output.as_deref(),
)?;
let session_key = command.session_key;
let width = term_size::dimensions_stdout().map(|(w, _)| w);
commands::dump(&mut input, &mut output,
command.mpis, command.hex,
session_key.as_ref(), width)?;
},
packet::Subcommands::Decrypt(command) => {
let mut input = open_or_stdin(command.io.input.as_deref())?;
let mut output = config.create_or_stdout_pgp(
command.io.output.as_deref(),
command.binary,
armor::Kind::Message,
)?;
let secrets =
load_keys(command.secret_key_file.iter().map(|s| s.as_ref()))?;
let session_keys = command.session_key;
commands::decrypt::decrypt_unwrap(
config,
&mut input, &mut output,
secrets,
session_keys,
command.dump_session_key)?;
output.finalize()?;
},
packet::Subcommands::Split(command) => {
let mut input = open_or_stdin(command.input.as_deref())?;
let prefix =
// The prefix is either specified explicitly...
command.prefix.unwrap_or(
// ... or we derive it from the input file...
command.input.and_then(|i| {
let p = PathBuf::from(i);
// (but only use the filename)
p.file_name().map(|f| String::from(f.to_string_lossy()))
})
// ... or we use a generic prefix...
.unwrap_or_else(|| String::from("output"))
// ... finally, add a hyphen to the derived prefix.
+ "-");
commands::split(&mut input, &prefix)?;
},
packet::Subcommands::Join(command) => commands::join(config, command)?,
},
SqSubcommands::Keyserver(command) => {
commands::net::dispatch_keyserver(config, command)?
}
SqSubcommands::Key(command) => {
commands::key::dispatch(config, command)?
}
SqSubcommands::Revoke(command) => {
commands::revoke::dispatch(config, command)?
}
SqSubcommands::Wkd(command) => {
commands::net::dispatch_wkd(config, command)?
}
SqSubcommands::Dane(command) => {
commands::net::dispatch_dane(config, command)?
}
SqSubcommands::Certify(command) => {
commands::certify::certify(config, command)?
}
}
Ok(())
}
fn parse_notations(n: Vec) -> Result> {
// TODO I'm not sure what to do about this requirement. Setting
// number_of_values = 2 for the argument already makes clap bail if the
// length of the vec is odd.
assert_eq!(n.len() % 2, 0);
// Each --notation takes two values. Iterate over them in chunks of 2.
let notations: Vec<(bool, NotationData)> = n
.chunks(2)
.map(|arg_pair| {
let name = &arg_pair[0];
let value = &arg_pair[1];
let (critical, name) = match name.strip_prefix('!') {
Some(name) => (true, name),
None => (false, name.as_str()),
};
let notation_data = NotationData::new(
name,
value,
NotationDataFlags::empty().set_human_readable(),
);
(critical, notation_data)
})
.collect();
Ok(notations)
}
// TODO: Replace all uses with CliTime argument type
/// Parses the given string depicting a ISO 8601 timestamp.
fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime)
-> Result>
{
// If you modify this function this function, synchronize the
// changes with the copy in sqv.rs!
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(0, 0, 0);
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-04T13Z", z).unwrap(); // XXX: chrono doesn't like
// parse_iso8601("2017-03-04T13", z).unwrap(); // ditto
parse_iso8601("2017-03-04", z).unwrap();
// parse_iso8601("2017-03", z).unwrap(); // ditto
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("20170304T13Z", z).unwrap(); // ditto
// parse_iso8601("20170304T13", z).unwrap(); // ditto
parse_iso8601("20170304", z).unwrap();
// parse_iso8601("201703", z).unwrap(); // ditto
parse_iso8601("2017031", z).unwrap();
// parse_iso8601("2017", z).unwrap(); // ditto
}
/// Prints the error and causes, if any.
pub fn print_error_chain(err: &anyhow::Error) {
eprintln!(" {}", err);
err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause));
}