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 crate::openpgp::{armor, Cert};
use crate::openpgp::crypto::Password;
use crate::openpgp::packet::prelude::*;
use crate::openpgp::parse::{Parse, PacketParser, PacketParserResult};
use crate::openpgp::packet::signature::subpacket::NotationData;
use crate::openpgp::packet::signature::subpacket::NotationDataFlags;
use crate::openpgp::serialize::{Serialize, stream::{Message, Armorer}};
use crate::openpgp::cert::prelude::*;
use crate::openpgp::policy::StandardPolicy as P;
use clap::FromArgMatches;
use crate::sq_cli::PacketSubcommands;
use sq_cli::SqSubcommands;
mod sq_cli;
mod commands;
mod output;
use output::{OutputFormat, OutputVersion};
fn open_or_stdin(f: Option<&str>)
-> Result<Box<dyn BufferedReader<()>>> {
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))),
}
}
#[deprecated(note = "Use the appropriate function on Config instead")]
fn create_or_stdout(f: Option<&str>, force: bool)
-> Result<Box<dyn io::Write + Sync + Send>> {
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)))
}
}
}
}
const SECONDS_IN_DAY : u64 = 24 * 60 * 60;
const SECONDS_IN_YEAR : u64 =
(365.2422222 * SECONDS_IN_DAY as f64) as u64;
fn parse_duration(expiry: &str) -> Result<Duration> {
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::<String>();
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::<String>();
if digits.is_empty() {
return Err(anyhow::anyhow!(
"--expiry: missing count \
(try: '2y' for 2 years)"));
}
let count = match digits.parse::<i32>() {
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))
}
fn load_keys<'a, I>(files: I) -> openpgp::Result<Vec<Cert>>
where I: Iterator<Item=&'a str>
{
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)
}
fn load_certs<'a, I>(files: I) -> openpgp::Result<Vec<Cert>>
where I: Iterator<Item=&'a str>
{
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)
}
#[allow(dead_code)]
fn serialize_keyring(mut output: &mut dyn io::Write, certs: &[Cert], binary: bool)
-> openpgp::Result<()> {
if binary {
for cert in certs {
cert.serialize(&mut output)?;
}
return Ok(());
}
if certs.len() == 1 {
return certs[0].armored().serialize(&mut output);
}
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(())
}
const ARMOR_DETECTION_LIMIT: u64 = 1 << 24;
#[allow(clippy::never_loop)]
fn detect_armor_kind(input: Box<dyn BufferedReader<()>>)
-> (Box<dyn BufferedReader<()>>, armor::Kind) {
let mut dup = Limitor::new(Dup::new(input), ARMOR_DETECTION_LIMIT).as_boxed();
let kind = 'detection: loop {
if let Ok(PacketParserResult::Some(pp)) =
PacketParser::from_reader(&mut dup)
{
let (packet, _) = match pp.next() {
Ok(v) => v,
Err(_) => break 'detection armor::Kind::File,
};
break 'detection match packet {
Packet::Signature(_) => armor::Kind::Signature,
Packet::SecretKey(_) => armor::Kind::SecretKey,
Packet::PublicKey(_) => armor::Kind::PublicKey,
Packet::PKESK(_) | Packet::SKESK(_) =>
armor::Kind::Message,
_ => armor::Kind::File,
};
}
break 'detection armor::Kind::File;
};
(dup.into_inner().unwrap().into_inner().unwrap(), kind)
}
fn decrypt_key<R>(key: Key<key::SecretParts, R>, passwords: &mut Vec<String>)
-> Result<Key<key::SecretParts, R>>
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 {
match rpassword::read_password_from_tty(
Some(&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() {
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()))
}
}
}
#[allow(dead_code)]
fn help_warning(arg: &str) {
if arg == "help" {
eprintln!("Warning: \"help\" is not a subcommand here. \
Did you mean --help?");
}
}
fn emit_unstable_cli_warning() {
if term_size::dimensions_stdout().is_some() {
return;
}
if std::env::var_os("COLUMNS").is_some() {
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<OutputVersion>,
policy: P<'a>,
unstable_cli_warning_emitted: bool,
}
impl Config<'_> {
fn create_or_stdout_safe(&self, f: Option<&str>)
-> Result<Box<dyn io::Write + Sync + Send>> {
#[allow(deprecated)]
create_or_stdout(f, self.force)
}
fn create_or_stdout_unsafe(&mut self, f: Option<&str>)
-> Result<Box<dyn io::Write + Sync + Send>> {
if ! self.unstable_cli_warning_emitted {
emit_unstable_cli_warning();
self.unstable_cli_warning_emitted = true;
}
#[allow(deprecated)]
create_or_stdout(f, self.force)
}
fn create_or_stdout_pgp<'a>(&self, f: Option<&str>,
binary: bool, kind: armor::Kind)
-> Result<Message<'a>> {
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)
}
}
fn main() -> Result<()> {
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::<Vec<&str>>();
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::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()),
)?;
let signatures = command.signatures.unwrap_or_else(|| {
if certs.is_empty() {
0
} else {
1
}
});
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) => {
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;
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)?;
},
SqSubcommands::Armor(command) => {
let input = open_or_stdin(command.io.input.as_deref())?;
let mut want_kind: Option<armor::Kind> = command.kind.into();
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)
{
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);
}
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 {
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) => {
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 {
PacketSubcommands::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)?;
},
PacketSubcommands::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()?;
},
PacketSubcommands::Split(command) => {
let mut input = open_or_stdin(command.input.as_deref())?;
let prefix =
command.prefix.unwrap_or(
command.input.and_then(|i| {
let p = PathBuf::from(i);
p.file_name().map(|f| String::from(f.to_string_lossy()))
})
.unwrap_or_else(|| String::from("output"))
+ "-");
commands::split(&mut input, &prefix)?;
},
PacketSubcommands::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::Certify(command) => {
commands::certify::certify(config, command)?
}
}
Ok(())
}
fn parse_notations(n: Vec<String>) -> Result<Vec<(bool, NotationData)>> {
assert_eq!(n.len() % 2, 0);
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)
}
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(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-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();
}
pub fn print_error_chain(err: &anyhow::Error) {
eprintln!(" {}", err);
err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause));
}