diff options
-rw-r--r-- | Cargo.lock | 94 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | sop/Cargo.toml | 30 | ||||
-rw-r--r-- | sop/Makefile | 24 | ||||
-rw-r--r-- | sop/README.md | 7 | ||||
-rw-r--r-- | sop/src/cli.rs | 315 | ||||
-rw-r--r-- | sop/src/dates.rs | 95 | ||||
-rw-r--r-- | sop/src/errors.rs | 101 | ||||
-rw-r--r-- | sop/src/main.rs | 780 |
10 files changed, 1449 insertions, 0 deletions
@@ -695,6 +695,14 @@ dependencies = [ ] [[package]] +name = "heck" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "hermit-abi" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1253,6 +1261,30 @@ dependencies = [ ] [[package]] +name = "proc-macro-error" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", + "syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "proc-macro2" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1589,6 +1621,7 @@ dependencies = [ "sequoia-net 0.16.0", "sequoia-openpgp 0.16.0", "sequoia-openpgp-ffi 0.16.0", + "sequoia-sop 0.16.0", "sequoia-sqv 0.16.0", "sequoia-store 0.16.0", "sequoia-tool 0.16.0", @@ -1737,6 +1770,17 @@ dependencies = [ ] [[package]] +name = "sequoia-sop" +version = "0.16.0" +dependencies = [ + "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "sequoia-openpgp 0.16.0", + "structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 1.0.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "sequoia-sqv" version = "0.16.0" dependencies = [ @@ -1913,6 +1957,28 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "structopt" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt-derive 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "syn" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1923,6 +1989,16 @@ dependencies = [ ] [[package]] +name = "syn-mid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "synstructure" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2236,6 +2312,11 @@ dependencies = [ ] [[package]] +name = "unicode-segmentation" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "unicode-width" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2276,6 +2357,11 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "version_check" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "want" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2418,6 +2504,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum glob 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" "checksum h2 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "a5b34c246847f938a410a03c5458c7fee2274436675e76d8b903c08efc29c462" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" "checksum hermit-abi 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "725cf19794cf90aa94e65050cb4191ff5d8fa87a498383774c47b332e3af952e" "checksum http 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)" = "d6ccf5ede3a895d8856620237b2f02972c1bbc78d2965ad7fe8838d4a0ed41f0" "checksum http-body 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6741c859c1b2463a423a1dbce98d418e6c3c3fc720fb0d45528657320920292d" @@ -2479,6 +2566,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" "checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" "checksum prettytable-rs 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0fd04b170004fa2daccf418a7f8253aaf033c27760b5f225889024cf66d7ac2e" +"checksum proc-macro-error 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "98e9e4b82e0ef281812565ea4751049f1bdcdfccda7d3f459f2e138a40c08678" +"checksum proc-macro-error-attr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4f5444ead4e9935abd7f27dc51f7e852a0569ac888096d5ec2499470794e2e53" "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" "checksum quickcheck 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a44883e74aa97ad63db83c4bf8ca490f02b2fc02f92575e720c8551e843c945f" "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" @@ -2532,7 +2621,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum strsim 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" +"checksum structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)" = "ff6da2e8d107dfd7b74df5ef4d205c6aebee0706c647f6bc6a2d5789905c00fb" +"checksum structopt-derive 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a489c87c08fbaf12e386665109dd13470dcc9c4583ea3e10dd2b4523e5ebd9ac" "checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" +"checksum syn-mid 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7be3539f6c128a931cf19dcee741c1af532c7fd387baa739c03dd2e96479338a" "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" "checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" @@ -2561,6 +2653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum typenum 1.11.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6d2783fe2d6b8c1101136184eb41be8b1ad379e4657050b8aaff0c79ee7575f9" "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" "checksum unicode-normalization 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf" +"checksum unicode-segmentation 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" "checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" "checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" @@ -2568,6 +2661,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168" "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" "checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +"checksum version_check 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "078775d0255232fb988e6fccf26ddc9d1ac274299aaedcedce21c6f72cc533ce" "checksum want 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b6395efa4784b027708f7451087e647ec73cc74f5d9bc2e418404248d679a230" "checksum wasi 0.9.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" @@ -34,6 +34,7 @@ sequoia-ipc = { path = "ipc", version = "0.16" } sequoia-net = { path = "net", version = "0.16" } sequoia-store = { path = "store", version = "0.16" } sequoia-tool = { path = "tool", version = "0.16" } +sequoia-sop = { path = "sop", version = "0.16" } sequoia-sqv = { path = "sqv", version = "0.16" } sequoia-guide = { path = "guide", version = "0.16" } @@ -83,6 +83,7 @@ build-release: $(MAKE) -Copenpgp-ffi build-release $(MAKE) -Cffi build-release $(MAKE) -Csqv build-release + $(MAKE) -Csop build-release .PHONY: install install: build-release @@ -95,6 +96,7 @@ install: build-release $(MAKE) -Copenpgp-ffi install $(MAKE) -Cffi install $(MAKE) -Csqv install + $(MAKE) -Csop install $(INSTALL) -d $(DESTDIR)$(PREFIX)/share/zsh/site-functions $(INSTALL) -t $(DESTDIR)$(PREFIX)/share/zsh/site-functions \ $(CARGO_TARGET_DIR)/_sq diff --git a/sop/Cargo.toml b/sop/Cargo.toml new file mode 100644 index 00000000..4e30997f --- /dev/null +++ b/sop/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "sequoia-sop" +description = "An implementation of the Stateless OpenPGP Command Line Interface using Sequoia" +version = "0.16.0" +authors = [ + "Justus Winter <justus@sequoia-pgp.org>", +] +documentation = "https://docs.sequoia-pgp.org/0.16.0/sop" +homepage = "https://sequoia-pgp.org/" +repository = "https://gitlab.com/sequoia-pgp/sequoia" +readme = "README.md" +keywords = ["cryptography", "openpgp", "pgp", "sop", "stateless-openpgp-protocol"] +categories = ["cryptography", "command-line-utilities"] +license = "GPL-2.0-or-later" +edition = "2018" + +[badges] +gitlab = { repository = "sequoia-pgp/sequoia" } +maintenance = { status = "actively-developed" } + +[dependencies] +sequoia-openpgp = { path = "../openpgp", version = "0.16" } +anyhow = "1" +chrono = "0.4" +structopt = { version = "0.3", default-features = false } +thiserror = "1" + +[[bin]] +name = "sqop" +path = "src/main.rs" diff --git a/sop/Makefile b/sop/Makefile new file mode 100644 index 00000000..5ffb9b45 --- /dev/null +++ b/sop/Makefile @@ -0,0 +1,24 @@ +# Configuration. +CARGO_TARGET_DIR ?= $(shell pwd)/../target +# We currently only support absolute paths. +CARGO_TARGET_DIR := $(abspath $(CARGO_TARGET_DIR)) +SOP ?= $(CARGO_TARGET_DIR)/debug/sqop + +# Tools. +CARGO ?= cargo +ifeq ($(shell uname -s), Darwin) + INSTALL ?= ginstall +else + INSTALL ?= install +endif + +# Installation. +.PHONY: build-release +build-release: + CARGO_TARGET_DIR=$(CARGO_TARGET_DIR) \ + $(CARGO) build $(CARGO_FLAGS) --release --package sequoia-sop + +.PHONY: install +install: build-release + $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin + $(INSTALL) -t $(DESTDIR)$(PREFIX)/bin $(CARGO_TARGET_DIR)/release/sqop diff --git a/sop/README.md b/sop/README.md new file mode 100644 index 00000000..0b00731a --- /dev/null +++ b/sop/README.md @@ -0,0 +1,7 @@ +An implementation of the Stateless OpenPGP Command Line Interface +using Sequoia. + +This implements a subset of the [Stateless OpenPGP Command Line +Interface] using the Sequoia OpenPGP implementation. + + [Stateless OpenPGP Command Line Interface]: https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/ diff --git a/sop/src/cli.rs b/sop/src/cli.rs new file mode 100644 index 00000000..58bbdd2d --- /dev/null +++ b/sop/src/cli.rs @@ -0,0 +1,315 @@ +use std::fmt; +use std::path::Path; + +use anyhow::Context; +use chrono::{DateTime, offset::Utc}; +use structopt::StructOpt; + +use sequoia_openpgp as openpgp; +use openpgp::{ + Cert, + crypto::{ + Password, + }, + types::{ + SignatureType, + DataFormat, + }, + parse::Parse, +}; + +use super::{ + dates, + Error, + Result, +}; + +#[derive(StructOpt)] +#[structopt(about = "An implementation of the \ + Stateless OpenPGP Command Line Interface \ + using Sequoia")] +pub enum SOP { + /// Prints version information. + Version { + }, + /// Generates a Secret Key. + GenerateKey { + /// Don't ASCII-armor output. + #[structopt(long)] + no_armor: bool, + /// UserIDs for the generated key. + userids: Vec<String>, + }, + /// Extracts a Certificate from a Secret Key. + ExtractCert { + /// Don't ASCII-armor output. + #[structopt(long)] + no_armor: bool, + }, + /// Creates Detached Signatures. + Sign { + /// Don't ASCII-armor output. + #[structopt(long)] + no_armor: bool, + /// Sign binary data or UTF-8 text. + #[structopt(default_value = "binary", long = "as")] + as_: SignAs, + /// Keys for signing. + keys: Vec<String>, + }, + /// Verifies Detached Signatures. + Verify { + /// Consider signatures before this date invalid. + #[structopt(long, parse(try_from_str = dates::parse_bound_round_down))] + not_before: Option<DateTime<Utc>>, + /// Consider signatures after this date invalid. + #[structopt(long, parse(try_from_str = dates::parse_bound_round_up))] + not_after: Option<DateTime<Utc>>, + /// Signatures to verify. + signatures: String, + /// Certs for verification. + certs: Vec<String>, + }, + /// Encrypts a Message. + Encrypt { + /// Don't ASCII-armor output. + #[structopt(long)] + no_armor: bool, + /// Encrypt binary data, UTF-8 text, or MIME data. + #[structopt(default_value = "binary", long = "as")] + as_: EncryptAs, + /// Encrypt with passwords. + #[structopt(long)] + with_password: Vec<String>, + /// Keys for signing. + #[structopt(long)] + sign_with: Vec<String>, + /// Encrypt for these certs. + certs: Vec<String>, + }, + /// Decrypts a Message. + Decrypt { + /// Write the session key here. + #[structopt(long)] + session_key_out: Option<String>, + /// Try to decrypt with this session key. + #[structopt(long)] + with_session_key: Vec<String>, + /// Try to decrypt with this password. + #[structopt(long)] + with_password: Vec<String>, + /// Write verification result here. + #[structopt(long)] + verify_out: Option<String>, + /// Certs for verification. + #[structopt(long)] + verify_with: Vec<String>, + /// Consider signatures before this date invalid. + #[structopt(long, parse(try_from_str = dates::parse_bound_round_down))] + verify_not_before: Option<DateTime<Utc>>, + /// Consider signatures after this date invalid. + #[structopt(long, parse(try_from_str = dates::parse_bound_round_up))] + verify_not_after: Option<DateTime<Utc>>, + /// Try to decrypt with this key. + key: Vec<String>, + }, + /// Converts binary OpenPGP data to ASCII + Armor { + /// Indicates the kind of data + #[structopt(long, default_value = "auto")] + label: ArmorKind, + }, + /// Converts ASCII OpenPGP data to binary + Dearmor { + }, + /// Unsupported subcommand. + #[structopt(external_subcommand)] + Unsupported(Vec<String>), +} + +#[derive(Clone, Copy)] +pub enum SignAs { + Binary, + Text, +} + +impl std::str::FromStr for SignAs { + type Err = anyhow::Error; + fn from_str(s: &str) -> openpgp::Result<Self> { + match s { + "binary" => Ok(SignAs::Binary), + "text" => Ok(SignAs::Text), + _ => Err(anyhow::anyhow!( + "{:?}, expected one of {{binary|text}}", s)), + } + } +} + +impl fmt::Display for SignAs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SignAs::Binary => f.write_str("binary"), + SignAs::Text => f.write_str("text"), + } + } +} + +impl From<SignAs> for SignatureType { + fn from(a: SignAs) -> Self { + match a { + SignAs::Binary => SignatureType::Binary, + SignAs::Text => SignatureType::Text, + } + } +} + +#[derive(Clone, Copy)] +pub enum EncryptAs { + Binary, + Text, + MIME, +} + +impl std::str::FromStr for EncryptAs { + type Err = anyhow::Error; + fn from_str(s: &str) -> openpgp::Result<Self> { + match s { + "binary" => Ok(EncryptAs::Binary), + "text" => Ok(EncryptAs::Text), + "mime" => Ok(EncryptAs::MIME), + _ => Err(anyhow::anyhow!( + "{}, expected one of {{binary|text|mime}}", s)), + } + } +} + +impl fmt::Display for EncryptAs { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + EncryptAs::Binary => f.write_str("binary"), + EncryptAs::Text => f.write_str("text"), + EncryptAs::MIME => f.write_str("mime"), + } + } +} + +impl From<EncryptAs> for SignatureType { + fn from(a: EncryptAs) -> Self { + match a { + EncryptAs::Binary => SignatureType::Binary, + EncryptAs::Text => SignatureType::Text, + // XXX: We should inspect the serialized MIME structure + // and use Text if it is UTF-8, Binary otherwise. But, we + // cannot be bothered at this point. + EncryptAs::MIME => SignatureType::Binary, + } + } +} + +impl From<EncryptAs> for DataFormat { + fn from(a: EncryptAs) -> Self { + match a { + EncryptAs::Binary => DataFormat::Binary, + EncryptAs::Text => DataFormat::Text, + EncryptAs::MIME => DataFormat::MIME, + } + } +} + +#[derive(Clone, Copy)] +pub enum ArmorKind { + Auto, + Sig, + Key, + Cert, + Message, +} + +impl std::str::FromStr for ArmorKind { + type Err = anyhow::Error; + fn from_str(s: &str) -> openpgp::Result<Self> { + match s { + "auto" => Ok(ArmorKind::Auto), + "sig" => Ok(ArmorKind::Sig), + "key" => Ok(ArmorKind::Key), + "cert" => Ok(ArmorKind::Cert), + "message" => Ok(ArmorKind::Message), + _ => Err(anyhow::anyhow!( + "{:?}, expected one of \ + {{auto|sig|key|cert|message}}", s)), + } + } +} + +impl fmt::Display for ArmorKind { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArmorKind::Auto => f.write_str("auto"), + ArmorKind::Sig => f.write_str("sig"), + ArmorKind::Key => f.write_str("key"), + ArmorKind::Cert => f.write_str("cert"), + ArmorKind::Message => f.write_str("message"), + } + } +} + + +fn is_special_designator<S: AsRef<str>>(file: S) -> bool { + file.as_ref().starts_with("@") +} + +/// Loads the given (special) file. +pub fn load_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)); + } + + return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix)); + } + + std::fs::File::open(f).map_err(|_| Error::MissingInput) + .context(format!("Failed to open file {:?}", f)) +} + +/// Creates the given (special) file. +pub 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)); + } + + 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) // XXX + .context(format!("Failed to create file {:?}", f)) +} + +/// Loads the certs given by the (special) files. +pub fn load_certs(files: Vec<String>) -> Result<Vec<Cert>> { + let mut certs = vec![]; + for f in files { + let r = load_file(&f)?; + certs.push(Cert::from_reader(r).map_err(|_| Error::BadData) + .context(format!("Failed to load key from file {:?}", f))?); + } + Ok(certs) +} + +/// Frobnicates the strings and converts them to passwords. +pub fn frob_passwords(p: Vec<String>) -> Result<Vec<Password>> { + // XXX: Maybe do additional checks. + Ok(p.iter().map(|p| p.trim_end().into()).collect()) +} diff --git a/sop/src/dates.rs b/sop/src/dates.rs new file mode 100644 index 00000000..b76fe1fc --- /dev/null +++ b/sop/src/dates.rs @@ -0,0 +1,95 @@ +use chrono::{DateTime, offset::Utc}; + +use crate::Result; + +/// Parses the given string depicting a ISO 8601 timestamp, rounding down. +pub fn parse_bound_round_down(s: &str) -> Result<DateTime<Utc>> { + match s { + // XXX: parse "-" to None once we figure out how to do that + // with structopt. + "now" => Ok(Utc::now()), + _ => parse_iso8601(s, chrono::NaiveTime::from_hms(0, 0, 0)), + } +} + +/// Parses the given string depicting a ISO 8601 timestamp, rounding up. +pub fn parse_bound_round_up(s: &str) -> Result<DateTime<Utc>> { + match s { + // XXX: parse "-" to None once we figure out how to do that + // with structopt. + "now" => Ok(Utc::now()), + _ => parse_iso8601(s, chrono::NaiveTime::from_hms(23, 59, 59)), + } +} + +/// Parses the given string depicting a ISO 8601 timestamp. +fn parse_iso8601(s: &str, pad_date_with: chrono::NaiveTime) + -> Result<DateTime<Utc>> +{ + // 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(); |