use anyhow::Context as _;
use std::fs::OpenOptions;
use std::io;
use std::path::{Path, PathBuf};
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::fmt::hex;
use crate::openpgp::types::KeyFlags;
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;
mod sq_cli;
mod commands;
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))),
}
}
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 --force to overwrite", p)))
}
}
}
}
fn create_or_stdout_pgp<'a>(f: Option<&str>, force: bool,
binary: bool, kind: armor::Kind)
-> Result<Message<'a>>
{
let sink = create_or_stdout(f, force)?;
let mut message = Message::new(sink);
if ! binary {
message = Armorer::new(message).kind(kind).build()?;
}
Ok(message)
}
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 == "" {
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,
None =>
return Err(anyhow::anyhow!(
"--expiry: missing suffix \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
digits, digits, digits, digits)),
Some(suffix) =>
return Err(anyhow::anyhow!(
"--expiry: invalid suffix '{}' \
(try: '{}y', '{}m', '{}w' or '{}d' instead)",
suffix, digits, digits, digits, digits)),
};
if junk != "" {
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() {
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(())
}
fn parse_armor_kind(kind: Option<&str>) -> Option<armor::Kind> {
match kind.expect("has default value") {
"auto" => None,
"message" => Some(armor::Kind::Message),
"cert" => Some(armor::Kind::PublicKey),
"key" => Some(armor::Kind::SecretKey),
"sig" => Some(armor::Kind::Signature),
"file" => Some(armor::Kind::File),
_ => unreachable!(),
}
}
const ARMOR_DETECTION_LIMIT: u64 = 1 << 24;
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(ppr) = PacketParser::from_reader(&mut dup) {
if let PacketParserResult::Some(pp) = ppr {
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 == "" {
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,
policy: P<'a>,
}
fn main() -> Result<()> {
let policy = &mut P::new();
use openpgp::packet::signature::subpacket::SubpacketTag;
policy.accept_critical_subpacket(SubpacketTag::TrustSignature);
policy.accept_critical_subpacket(SubpacketTag::RegularExpression);
let matches = sq_cli::build().get_matches();
emit_unstable_cli_warning();
let known_notations: Vec<&str> = matches.values_of("known-notation")
.unwrap_or_default()
.collect();
policy.good_critical_notations(&known_notations);
let force = matches.is_present("force");
let config = Config {
force,
policy: policy.clone(),
};
match matches.subcommand() {
("decrypt", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let mut output = create_or_stdout(m.value_of("output"), force)?;
let signatures: usize =
m.value_of("signatures").expect("has a default").parse()?;
let certs = m.values_of("sender-cert-file")
.map(load_certs)
.unwrap_or(Ok(vec![]))?;
let secrets = m.values_of("secret-key-file")
.map(load_keys)
.unwrap_or(Ok(vec![]))?;
commands::decrypt(config,
&mut input, &mut output,
signatures, certs, secrets,
m.is_present("dump-session-key"),
m.is_present("dump"), m.is_present("hex"))?;
},
("encrypt", Some(m)) => {
let recipients = m.values_of("recipients-cert-file")
.map(load_certs)
.unwrap_or(Ok(vec![]))?;
let mut input = open_or_stdin(m.value_of("input"))?;
let output =
create_or_stdout_pgp(m.value_of("output"), force,
m.is_present("binary"),
armor::Kind::Message)?;
let additional_secrets = m.values_of("signer-key-file")
.map(load_keys)
.unwrap_or(Ok(vec![]))?;
let mode = match m.value_of("mode").expect("has default") {
"rest" => KeyFlags::empty()
.set_storage_encryption(),
"transport" => KeyFlags::empty()
.set_transport_encryption(),
"all" => KeyFlags::empty()
.set_storage_encryption()
.set_transport_encryption(),
_ => unreachable!("uses possible_values"),
};
let time = if let Some(time) = m.value_of("time") {
Some(parse_iso8601(time, chrono::NaiveTime::from_hms(0, 0, 0))
.context(format!("Bad value passed to --time: {:?}",
time))?.into())
} else {
None
};
commands::encrypt(policy, &mut input, output,
m.occurrences_of("symmetric") as usize,
&recipients, additional_secrets,
mode,
m.value_of("compression").expect("has default"),
time.into(),
m.is_present("use-expired-subkey"),
)?;
},
("sign", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let output = m.value_of("output");
let detached = m.is_present("detached");
let binary = m.is_present("binary");
let append = m.is_present("append");
let notarize = m.is_present("notarize");
let secrets = m.values_of("secret-key-file")
.map(load_keys)
.unwrap_or(Ok(vec![]))?;
let time = if let Some(time) = m.value_of("time") {
Some(parse_iso8601(time, chrono::NaiveTime::from_hms(0, 0, 0))
.context(format!("Bad value passed to --time: {:?}",
time))?.into())
} else {
None
};
if let Some(merge) = m.value_of("merge") {
let output = create_or_stdout_pgp(output, force, binary,
armor::Kind::Message)?;
let mut input2 = open_or_stdin(Some(merge))?;
commands::merge_signatures(&mut input, &mut input2, output)?;
} else {
let mut notations: Vec<(bool, NotationData)> = Vec::new();
if let Some(mut n) = m.values_of("notation") {
while let Some(name) = n.next() {
let value = n.next().unwrap();
let (critical, name) = if name.len() > 0
&& Some('!') == name.chars().next()
{
(true, &name[1..])
} else {
(false, name)
};
notations.push(
(critical,
NotationData::new(
name, value,
NotationDataFlags::empty().set_human_readable())));
}
}
commands::sign(policy, &mut input, output, secrets, detached,
binary, append, notarize, time, ¬ations, force)?;
}
},
("verify", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let mut output = create_or_stdout(m.value_of("output"), force)?;
let mut detached = if let Some(f) = m.value_of("detached") {
Some(File::open(f)?)
} else {
None
};
let signatures: usize =
m.value_of("signatures").expect("has a default").parse()?;
let certs = m.values_of("sender-cert-file")
.map(load_certs)
.unwrap_or(Ok(vec![]))?;
commands::verify(config, &mut input,
detached.as_mut().map(|r| r as &mut (dyn io::Read + Sync + Send)),
&mut output, signatures, certs)?;
},
("armor", Some(m)) => {
let input = open_or_stdin(m.value_of("input"))?;
let mut want_kind = parse_armor_kind(m.value_of("kind"));
let mut dup = Limitor::new(Dup::new(input), ARMOR_DETECTION_LIMIT);
let (already_armored, have_kind) = {
let mut reader =
armor::Reader::new(&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 =
create_or_stdout(m.value_of("output"), force)?;
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 =
create_or_stdout_pgp(m.value_of("output"), force,
false, want_kind)?;
if already_armored {
let mut reader =
armor::Reader::new(input,
armor::ReaderMode::Tolerant(None));
io::copy(&mut reader, &mut output)?;
} else {
io::copy(&mut input, &mut output)?;
}
output.finalize()?;
},
("dearmor", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let mut output = create_or_stdout(m.value_of("output"), force)?;
let mut filter = armor::Reader::new(&mut input, None);
io::copy(&mut filter, &mut output)?;
},
#[cfg(feature = "autocrypt")]
("autocrypt", Some(m)) => commands::autocrypt::dispatch(config, m)?,
("inspect", Some(m)) => {
let mut output = create_or_stdout(m.value_of("output"), force)?;
commands::inspect(m, policy, &mut output)?;
},
("keyring", Some(m)) => commands::keyring::dispatch(m, force)?,
("packet", Some(m)) => match m.subcommand() {
("dump", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let mut output = create_or_stdout(m.value_of("output"), force)?;
let session_key: Option<openpgp::crypto::SessionKey> =
if let Some(sk) = m.value_of("session-key") {
Some(hex::decode_pretty(sk)?.into())
} else {
None
};
let width = term_size::dimensions_stdout().map(|(w, _)| w);
commands::dump(&mut input, &mut output,
m.is_present("mpis"), m.is_present("hex"),
session_key.as_ref(), width)?;
},
("decrypt", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let mut output =
create_or_stdout_pgp(m.value_of("output"), force,
m.is_present("binary"),
armor::Kind::Message)?;
let secrets = m.values_of("secret-key-file")
.map(load_keys)
.unwrap_or(Ok(vec![]))?;
commands::decrypt::decrypt_unwrap(
config,
&mut input, &mut output,
secrets, m.is_present("dump-session-key"))?;
output.finalize()?;
},
("split", Some(m)) => {
let mut input = open_or_stdin(m.value_of("input"))?;
let prefix =
m.value_of("prefix").map(|p| p.to_owned())
.unwrap_or(
m.value_of("input").and_then(|i| {
let p = PathBuf::from(i);
p.file_name().map(|f| String::from(f.to_string_lossy()))
})
.unwrap_or(String::from("output"))
+ "-");
commands::split(&mut input, &prefix)?;
},
("join", Some(m)) => commands::join(config, m)?,
_ => unreachable!(),
},
#[cfg(feature = "net")]
("keyserver", Some(m)) =>
commands::net::dispatch_keyserver(config, m)?,
("key", Some(m)) => commands::key::dispatch(config, m)?,
#[cfg(feature = "net")]
("wkd", Some(m)) => commands::net::dispatch_wkd(config, m)?,
("certify", Some(m)) => {
commands::certify::certify(config, m)?;
},
_ => unreachable!(),
}
return Ok(())
}
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();
}