sop-0.3.1/.cargo_vcs_info.json0000644000000001120000000000100116300ustar { "git": { "sha1": "f3f0887fde854211909d6a2ea116565cd6307442" } } sop-0.3.1/.gitignore000064400000000000000000000000230072674642500124410ustar 00000000000000/target Cargo.lock sop-0.3.1/.gitlab-ci.yml000064400000000000000000000034350072674642500131170ustar 00000000000000stages: - pre-check - test - deploy codespell: tags: - linux stage: pre-check image: registry.gitlab.com/sequoia-pgp/build-docker-image/bullseye:latest before_script: - codespell --version script: - > codespell --disable-colors -L "crate,ede,iff,mut,nd,te,uint,KeyServer,keyserver,Keyserver,keyservers,Keyservers,keypair,keypairs,KeyPair,fpr,dedup" -S "*.bin,*.gpg,*.pgp,./.git,data,highlight.js,*/target,Makefile" src after_script: [] bullseye: tags: - linux stage: test image: registry.gitlab.com/sequoia-pgp/build-docker-image/bullseye:latest script: - if [ -d target ]; then find target | wc --lines; du -sh target; fi - if [ -d cargo ]; then find cargo | wc --lines; du -sh cargo; fi - rustc --version - cargo --version - cargo test --all-features - du -sh target - du -sh cargo windows-msvc: tags: - win - win2019 stage: test image: registry.gitlab.com/sequoia-pgp/build-docker-image/windows-msvc before_script: - rustc --version --verbose - cargo --version script: - cargo test --all-features after_script: [] pages: tags: - linux stage: deploy image: registry.gitlab.com/sequoia-pgp/build-docker-image/bullseye:latest script: - if [ -d target ]; then find target | wc --lines; du -sh target; fi - if [ -d cargo ]; then find cargo | wc --lines; du -sh cargo; fi - rustc --version - cargo --version - cargo doc --no-deps --features=cli - mv target/doc public - echo "/sop-rs/ /sop-rs/sop/index.html 302" > public/_redirects artifacts: paths: - public only: - main cache: paths: - Cargo.lock - target/ - cargo/ variables: CARGO_HOME: $CI_PROJECT_DIR/cargo CARGO_FLAGS: --color always CARGO_INCREMENTAL: 0 sop-0.3.1/Cargo.toml0000644000000027310000000000100076370ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "sop" version = "0.3.1" authors = ["Justus Winter "] description = "Rust Interface for the Stateless OpenPGP Protocol" homepage = "https://sequoia-pgp.org/" documentation = "https://docs.rs/sop" readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "encryption", "signing"] categories = ["cryptography", "authentication", "email", "api-bindings", "command-line-utilities"] license = "MIT" repository = "https://gitlab.com/sequoia-pgp/sop-rs" [package.metadata.docs.rs] all-features = true [dependencies.anyhow] version = "1" optional = true [dependencies.chrono] version = "0.4.10" optional = true [dependencies.memsec] version = ">=0.5" default-features = false [dependencies.structopt] version = "0.3.11" optional = true default-features = false [dependencies.thiserror] version = "1" [features] cli = ["anyhow", "chrono", "structopt"] default = [] [badges.gitlab] repository = "sequoia-pgp/sequoia" [badges.maintenance] status = "actively-developed" sop-0.3.1/Cargo.toml.orig000064400000000000000000000020200072674642500133370ustar 00000000000000[package] name = "sop" description = "Rust Interface for the Stateless OpenPGP Protocol" version = "0.3.1" authors = ["Justus Winter "] documentation = "https://docs.rs/sop" homepage = "https://sequoia-pgp.org/" repository = "https://gitlab.com/sequoia-pgp/sop-rs" readme = "README.md" keywords = ["cryptography", "openpgp", "pgp", "encryption", "signing"] categories = ["cryptography", "authentication", "email", "api-bindings", "command-line-utilities"] license = "MIT" edition = "2018" [badges] gitlab = { repository = "sequoia-pgp/sequoia" } maintenance = { status = "actively-developed" } [dependencies] memsec = { version = ">=0.5", default-features = false } thiserror = "1" # These dependencies are for the CLI frontend. anyhow = { version = "1", optional = true } chrono = { version = "0.4.10", optional = true } structopt = { version = "0.3.11", default-features = false, optional = true } [features] default = [] cli = ["anyhow", "chrono", "structopt"] [package.metadata.docs.rs] all-features = true sop-0.3.1/LICENSE.txt000064400000000000000000000017770072674642500123150ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sop-0.3.1/README.md000064400000000000000000000020020072674642500117270ustar 00000000000000Rust Interface for the Stateless OpenPGP Protocol ================================================= A set of types and traits formalizing the [Stateless OpenPGP Protocol]. Currently, SOP is only defined as a command line interface, but we are working on a [C Interface]. This interface is the Rust equivalent of the yet to be defined C API. [Stateless OpenPGP Protocol]: https://gitlab.com/dkg/openpgp-stateless-cli [C Interface]: https://gitlab.com/dkg/openpgp-stateless-cli/-/issues/32 Command Line Interface ---------------------- This crate contains an implementation of the Stateless OpenPGP Command Line Interface in terms of the Rust types and traits. Once you implemented the traits, you get the command line interface basically for free by adding this snippet to your `Cargo.toml`: ```toml [[bin]] path = "src/main.rs" required-features = ["cli"] [features] cli = ["sop/cli"] ``` And creating `src/main.rs` along the lines of: ```rust fn main() { sop::cli::main(&MySOPImplementation::default()); } ``` sop-0.3.1/rust-toolchain000064400000000000000000000000070072674642500133510ustar 000000000000001.46.0 sop-0.3.1/src/cli.rs000064400000000000000000000606120072674642500123670ustar 00000000000000//! Command-line frontend for SOP. //! //! This is an implementation of the SOP Command-Line protocol in //! terms of the trait [`crate::SOP`], hence it can be shared between //! SOP implementations. //! //! To use, add this snippet to `Cargo.toml`: //! //! ```toml //! [[bin]] //! path = "src/main.rs" //! required-features = ["cli"] //! //! [features] //! cli = ["sop/cli"] //! ``` //! //! And create `src/main.rs` along the lines of: //! //! ```rust,ignore //! fn main() { //! sop::cli::main(&MySOPImplementation::default()); //! } //! ``` //! //! ## Generating shell completions //! //! To create shell completions, add this snippet to `Cargo.toml`: //! //! ```toml //! [build-dependencies] //! sop = "..." //! ``` //! //! And create `build.rs` along the lines of: //! //! ```rust,no_run //! #[cfg(feature = "cli")] //! fn main() { //! let outdir = std::env::var_os("CARGO_TARGET_DIR") //! .or(std::env::var_os("OUT_DIR")) //! .expect("cargo to set OUT_DIR"); //! sop::cli::write_shell_completions("sqop", outdir).unwrap(); //! } //! //! #[cfg(not(feature = "cli"))] //! fn main() {} //! ``` //! //! # Features and limitations //! //! - The special designator `@FD:` is only available on UNIX-like systems. //! //! - On Windows, certs and keys provided via the `@ENV:` special //! designator must be ASCII armored and well-formed UTF-8. use std::{ io::{self, Write}, path::Path, }; use anyhow::{Context, Result}; use chrono::{DateTime, offset::Utc}; use structopt::{StructOpt, clap::AppSettings}; use crate::{ ArmorLabel, EncryptAs, SignAs, }; #[derive(Debug, StructOpt)] #[structopt(about = "An implementation of the \ Stateless OpenPGP Command Line Interface", settings(&[AppSettings::DisableVersion, AppSettings::VersionlessSubcommands]), )] enum Operation { /// Prints version information. /// /// Invoked without arguments, returns name and version of the SOP /// implementation. #[structopt(display_order = 100)] Version { /// Returns name and version of the primary underlying OpenPGP /// toolkit. #[structopt(long, conflicts_with("extended"))] backend: bool, /// Returns multiple lines of name and version information. /// /// The first line is the name and version of the SOP /// implementation, but the rest have no defined structure. #[structopt(long, conflicts_with("backend"))] extended: bool, }, /// Generates a Secret Key. #[structopt(display_order = 200)] GenerateKey { /// Don't ASCII-armor output. #[structopt(long)] no_armor: bool, /// UserIDs for the generated key. userids: Vec, }, /// Extracts a Certificate from a Secret Key. #[structopt(display_order = 300)] ExtractCert { /// Don't ASCII-armor output. #[structopt(long)] no_armor: bool, }, /// Creates Detached Signatures. #[structopt(display_order = 400)] 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, /// Emit the digest algorithm used to the specified file. #[structopt(long)] micalg_out: Option, /// Keys for signing. keys: Vec, }, /// Verifies Detached Signatures. #[structopt(display_order = 500)] Verify { /// Consider signatures before this date invalid. #[structopt(long, parse(try_from_str = parse_bound_round_down))] not_before: Option>, /// Consider signatures after this date invalid. #[structopt(long, parse(try_from_str = parse_bound_round_up))] not_after: Option>, /// Signatures to verify. signatures: String, /// Certs for verification. certs: Vec, }, /// Encrypts a Message. #[structopt(display_order = 600)] 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, number_of_values = 1)] with_password: Vec, /// Keys for signing. #[structopt(long, number_of_values = 1)] sign_with: Vec, /// Encrypt for these certs. certs: Vec, }, /// Decrypts a Message. #[structopt(display_order = 700)] Decrypt { /// Write the session key here. #[structopt(long)] session_key_out: Option, /// Try to decrypt with this session key. #[structopt(long, number_of_values = 1)] with_session_key: Vec, /// Try to decrypt with this password. #[structopt(long, number_of_values = 1)] with_password: Vec, /// Write verification result here. #[structopt(long)] verify_out: Option, /// Certs for verification. #[structopt(long, number_of_values = 1)] verify_with: Vec, /// Consider signatures before this date invalid. #[structopt(long, parse(try_from_str = parse_bound_round_down))] verify_not_before: Option>, /// Consider signatures after this date invalid. #[structopt(long, parse(try_from_str = parse_bound_round_up))] verify_not_after: Option>, /// Try to decrypt with these keys. keys: Vec, }, /// Converts binary OpenPGP data to ASCII. #[structopt(display_order = 800)] Armor { /// Indicates the kind of data. #[structopt(long, default_value = "auto")] label: ArmorLabel, }, /// Converts ASCII OpenPGP data to binary. #[structopt(display_order = 900)] Dearmor { }, /// Unsupported subcommand. #[structopt(external_subcommand)] Unsupported(Vec), } /// Generates shell completions. pub fn write_shell_completions(binary_name: B, out_path: P) -> io::Result<()> where B: AsRef, P: AsRef, { use structopt::clap::Shell; let binary_name = binary_name.as_ref(); let out_path = out_path.as_ref(); std::fs::create_dir_all(&out_path)?; let mut clap = Operation::clap(); for shell in &[Shell::Bash, Shell::Fish, Shell::Zsh, Shell::PowerShell, Shell::Elvish] { clap.gen_completions(binary_name, *shell, out_path); } Ok(()) } /// Implements the SOP command-line interface. pub fn main(sop: &dyn crate::SOP) -> ! { use std::process::exit; match real_main(sop) { Ok(()) => exit(0), Err(e) => { print_error_chain(&e); if let Ok(e) = e.downcast::() { exit(e.into()) } exit(1); }, } } fn real_main(sop: &dyn crate::SOP) -> Result<()> { // Do a little dance to supply name and version to structopt. let v = sop.version()?.frontend()?; let version = v.version.clone(); let about = format!("An implementation of the Stateless OpenPGP Command \ Line Interface using {} {}", v.name, v.version); let app = Operation::clap() .name(format!("SOP {}", v.name)) .about(&about[..]) .version(&version[..]); match Operation::from_clap(&app.get_matches()) { Operation::Version { backend, extended, } => { let version = sop.version()?; match (backend, extended) { (false, false) => { let v = version.frontend()?; println!("{} {}", v.name, v.version); }, (true, false) => { let v = version.backend()?; println!("{} {}", v.name, v.version); }, (false, true) => { for v in version.extended()? { println!("{} {}", v.name, v.version); } }, (true, true) => unreachable!("mutually exclusive"), } }, Operation::GenerateKey { no_armor, userids, } => { let mut op = sop.generate_key()?; if no_armor { op = op.no_armor(); } for u in userids { op = op.userid(&u); } op.generate()?.write_to(&mut io::stdout())?; }, Operation::ExtractCert { no_armor, } => { let mut op = sop.extract_cert()?; if no_armor { op = op.no_armor(); } op.keys(&mut io::stdin())?.write_to(&mut io::stdout())?; }, Operation::Sign { no_armor, as_, micalg_out, keys, } => { let mut op = sop.sign()?.mode(as_); if no_armor { op = op.no_armor(); } for mut key in load_files(keys)? { op = op.key(&mut key)?; } let micalg = op.data(&mut io::stdin())?.write_to(&mut io::stdout())?; if let Some(path) = micalg_out { let mut sink = create_file(path)?; write!(sink, "{}", micalg)?; } }, Operation::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 mut cert in load_files(certs)? { op = op.certs(&mut cert)?; } let verifications = op.signatures(&mut load_file(signatures)?)? .data(&mut io::stdin())?; for v in verifications { println!("{}", v); } }, Operation::Encrypt { no_armor, as_, with_password, sign_with, certs, } => { let mut op = sop.encrypt()?.mode(as_); if no_armor { op = op.no_armor(); } for mut key in load_files(sign_with)? { op = op.sign_with_key(&mut key)?; } for mut stream in load_files(with_password)? { let mut pw = String::new(); stream.read_to_string(&mut pw)?; op = op.with_password(pw.into())?; } for mut cert in load_files(certs)? { op = op.with_cert(&mut cert)?; } op.plaintext(&mut io::stdin())?.write_to(&mut io::stdout())?; }, Operation::Decrypt { session_key_out, with_session_key, with_password, verify_out, verify_with, verify_not_before, verify_not_after, keys, } => { let session_key_out: Option> = if let Some(f) = session_key_out { Some(Box::new(create_file(f)?)) } else { None }; if verify_out.is_none() != verify_with.is_empty() { return Err(anyhow::Error::from(Error::IncompleteVerification)) .context("--verify-out and --verify-with \ must both be given"); } let mut verify_out: Box = if let Some(f) = verify_out { Box::new(create_file(f)?) } else { Box::new(io::sink()) }; let mut op = sop.decrypt()?; for 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 mut stream in load_files(with_password)? { let mut pw = String::new(); stream.read_to_string(&mut pw)?; op = op.with_password(pw.into())?; } for mut key in load_files(keys)? { op = op.with_keys(&mut key)?; } for mut cert in load_files(verify_with)? { op = op.verify_with_certs(&mut cert)?; } 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())?.write_to(&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()); } } }, Operation::Armor { label, } => { let op = sop.armor()?.label(label); op.data(&mut io::stdin())? .write_to(&mut io::stdout())?; }, Operation::Dearmor {} => { let op = sop.dearmor()?; op.data(&mut io::stdin())? .write_to(&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>(file: S) -> bool { file.as_ref().starts_with("@") } /// Loads the given (special) file. fn load_file>(file: S) -> Result> { 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)?; let f = unsafe { std::fs::File::from_raw_fd(fd) }; return Ok(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)?; // Prevent leak to child processes. std::env::remove_var(key); return Ok(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)?; // Prevent leak to child processes. std::env::remove_var(key); return Ok(Box::new(io::Cursor::new(value.into_bytes()))); } } return Err(anyhow::Error::from(Error::UnsupportedSpecialPrefix)); } std::fs::File::open(f) .map(|f| -> Box { Box::new(f) }) .map_err(|_| Error::MissingInput) .context(format!("Failed to open file {:?}", f)) } /// Creates the given (special) file. fn create_file>(file: S) -> Result { 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 the (special) files. fn load_files(files: Vec) -> Result>> { files.iter().map(load_file).collect() } /// Parses the given string depicting a ISO 8601 timestamp, rounding down. fn parse_bound_round_down(s: &str) -> Result> { 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. fn parse_bound_round_up(s: &str) -> Result> { 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> { // 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 } /// Errors defined by the Stateless OpenPGP Command-Line Protocol. #[derive(thiserror::Error, Debug)] enum Error { /// No acceptable signatures found ("sop verify"). #[error("No acceptable signatures found")] NoSignature, /// Asymmetric algorithm unsupported ("sop encrypt"). #[error("Asymmetric algorithm unsupported")] UnsupportedAsymmetricAlgo, /// Certificate not encryption-capable (e.g., expired, revoked, /// unacceptable usage flags) ("sop encrypt"). #[error("Certificate not encryption-capable")] CertCannotEncrypt, /// Key not signature-capable (e.g., expired, revoked, /// unacceptable usage flags) (`sop sign` and `sop encrypt` with /// `--sign-with`). #[error("Key not signing-capable")] KeyCannotSign, /// Missing required argument. #[error("Missing required argument")] MissingArg, /// Incomplete verification instructions ("sop decrypt"). #[error("Incomplete verification instructions")] IncompleteVerification, /// Unable to decrypt ("sop decrypt"). #[error("Unable to decrypt")] CannotDecrypt, /// Non-"UTF-8" or otherwise unreliable password ("sop encrypt"). #[error("Non-UTF-8 or otherwise unreliable password")] PasswordNotHumanReadable, /// Unsupported option. #[error("Unsupported option")] UnsupportedOption, /// Invalid data type (no secret key where "KEY" expected, etc). #[error("Invalid data type")] BadData, /// Non-text input where text expected. #[error("Non-text input where text expected")] ExpectedText, /// Output file already exists. #[error("Output file already exists")] OutputExists, /// Input file does not exist. #[error("Input file does not exist")] MissingInput, /// A "KEY" input is protected (locked) with a password, and "sop" cannot /// unlock it. #[error("A KEY input is protected with a password")] KeyIsProtected, /// Unsupported subcommand. #[error("Unsupported subcommand")] UnsupportedSubcommand, /// An indirect parameter is a special designator (it starts with "@") but /// "sop" does not know how to handle the prefix. #[error("An indirect parameter is a special designator with unknown prefix")] UnsupportedSpecialPrefix, /// A indirect input parameter is a special designator (it starts with /// "@"), and a filename matching the designator is actually present. #[error("A indirect input parameter is a special designator matches file")] AmbiguousInput, /// An I/O operation failed. #[error("I/O operation failed")] IoError(#[source] io::Error), } impl From for Error { fn from(e: crate::Error) -> Self { use crate::Error as E; use Error as CE; match e { 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::NotImplemented => CE::UnsupportedSubcommand, E::IoError(e) => CE::IoError(e), } } } impl From for i32 { fn from(e: Error) -> Self { use Error::*; match e { 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, IoError(_) => 1, } } } /// Prints the error and causes, if any. fn print_error_chain(err: &anyhow::Error) { eprintln!(" {}", err); err.chain().skip(1).for_each(|cause| eprintln!(" because: {}", cause)); } sop-0.3.1/src/lib.rs000064400000000000000000000662610072674642500123740ustar 00000000000000//! A Rust implementation of the Stateless OpenPGP Protocol. use std::{ fmt, io, time::SystemTime, }; #[cfg(feature = "cli")] pub mod cli; /// The Stateless OpenPGP Protocol. pub trait SOP { /// Gets version information. fn version<'a>(&'a self) -> Result>; /// Generates a Secret Key. /// /// Customize the operation using the builder [`GenerateKey`]. fn generate_key<'a>(&'a self) -> Result>; /// Extracts a Certificate from a Secret Key. /// /// Customize the operation using the builder [`ExtractCert`]. fn extract_cert<'a>(&'a self) -> Result>; /// Creates Detached Signatures. /// /// Customize the operation using the builder [`Sign`]. fn sign<'a>(&'a self) -> Result>; /// Verifies Detached Signatures. /// /// Customize the operation using the builder [`Verify`]. fn verify<'a>(&'a self) -> Result>; /// Encrypts a Message. /// /// Customize the operation using the builder [`Encrypt`]. fn encrypt<'a>(&'a self) -> Result>; /// Decrypts a Message. /// /// Customize the operation using the builder [`Decrypt`]. fn decrypt<'a>(&'a self) -> Result>; /// Converts binary OpenPGP data to ASCII. /// /// Customize the operation using the builder [`Armor`]. fn armor<'a>(&'a self) -> Result>; /// Converts ASCII OpenPGP data to binary. /// /// Customize the operation using the builder [`Dearmor`]. fn dearmor<'a>(&'a self) -> Result>; } pub trait Version<'a> { /// Returns name and version of the SOP implementation. fn frontend(&self) -> Result; /// Returns name and version of the primary underlying OpenPGP /// toolkit. fn backend(&self) -> Result; /// Returns multiple name and version pairs. /// /// The first one MUST match the information produced by /// [`Version::frontend`], but the rest have no defined structure. /// This is what the default implementation does. fn extended(&self) -> Result> { Ok(vec![ self.frontend()?, self.backend()?, ]) } } /// Represents a name and version tuple. pub struct VersionInfo { /// Name of the implementation, library, or additional component. pub name: String, /// Version string. pub version: String, } /// Builder for [`SOP::generate_key`]. pub trait GenerateKey<'a> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 'a>; /// Adds a User ID. fn userid(self: Box, userid: &str) -> Box + 'a>; /// Generates the OpenPGP key. fn generate(self: Box) -> Result>; } /// Builder for [`SOP::extract_cert`]. pub trait ExtractCert<'a> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 'a>; /// Extracts the cert from `key`. fn key(self: Box, key: &mut (dyn io::Read + Send + Sync)) -> Result>; /// Extracts the certs from `keys`. fn keys(self: Box, keys: &mut (dyn io::Read + Send + Sync)) -> Result>; } /// Builder for [`SOP::sign`]. pub trait Sign<'a> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 'a>; /// Sets signature mode. fn mode(self: Box, mode: SignAs) -> Box + 'a>; /// Adds the signer key. fn key(self: Box, key: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Adds the signer keys. /// /// Like [`Sign::key`], but for multiple keys. fn keys(self: Box, keys: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Signs data. fn data(self: Box, data: &'a mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; } /// Builder for [`SOP::verify`]. pub trait Verify<'a> { /// Makes SOP consider signatures before this date invalid. fn not_before(self: Box, t: SystemTime) -> Box + 'a>; /// Makes SOP consider signatures after this date invalid. fn not_after(self: Box, t: SystemTime) -> Box + 'a>; /// Adds the verification cert. fn cert(self: Box, cert: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Adds the verification certs. /// /// Like [`Verify::cert`], but for multiple certs. fn certs(self: Box, certs: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Provides the signatures. fn signatures(self: Box, signatures: &'a mut (dyn io::Read + Send + Sync)) -> Result>; } /// Builder for [`SOP::verify`]. pub trait VerifySignatures { /// Verifies the authenticity of `data`. fn data(self: Box, data: &mut (dyn io::Read + Send + Sync)) -> Result>; } /// A successful signature verification. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Verification { creation_time: SystemTime, signing_key_fingerprint: String, signing_cert_fingerprint: String, message: Option, } #[cfg(feature = "cli")] impl fmt::Display for Verification { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} {} {}{}", chrono::DateTime::::from(self.creation_time()) .format("%Y-%m-%dT%H:%M:%SZ"), self.signing_key_fingerprint(), self.signing_cert_fingerprint(), if let Some(m) = self.message() { format!(" {}", m) } else { "".into() }) } } impl Verification { /// Creates a `Verification` object. pub fn new<'m, T, K, C, M>(creation_time: T, signing_key_fingerprint: K, signing_cert_fingerprint: C, message: M) -> Result where T: Into, K: ToString, C: ToString, M: Into>, { fn normalize(s: String) -> Result { // XXX Ok(s) } let signing_key_fingerprint = normalize(signing_key_fingerprint.to_string())?; let signing_cert_fingerprint = normalize(signing_cert_fingerprint.to_string())?; Ok(Verification { creation_time: creation_time.into(), signing_key_fingerprint, signing_cert_fingerprint, message: message.into().map(Into::into), }) } /// Returns the signature's creation time. pub fn creation_time(&self) -> SystemTime { self.creation_time } /// Returns the fingerprint of the signing (sub)key. pub fn signing_key_fingerprint(&self) -> &str { &self.signing_key_fingerprint } /// Returns the fingerprint of the signing certificate. pub fn signing_cert_fingerprint(&self) -> &str { &self.signing_cert_fingerprint } /// Returns a free-form message describing the verification. pub fn message(&self) -> Option<&str> { self.message.as_ref().map(AsRef::as_ref) } } /// Builder for [`SOP::encrypt`]. pub trait Encrypt<'a> { /// Disables armor encoding. fn no_armor(self: Box) -> Box + 'a>; /// Sets encryption mode. fn mode(self: Box, mode: EncryptAs) -> Box + 'a>; /// Adds the signer key. fn sign_with_key(self: Box, key: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Adds the signer keys. /// /// Like [`Encrypt::sign_with_key`], but for multiple keys. fn sign_with_keys(self: Box, keys: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Encrypts with the given password. fn with_password(self: Box, password: Password) -> Result + 'a>>; /// Encrypts with the given cert. fn with_cert(self: Box, cert: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Encrypts with the given certs. /// /// Like [`Encrypt::with_cert`], but for multiple certs. fn with_certs(self: Box, certs: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Encrypts the given data yielding the ciphertext. fn plaintext(self: Box, plaintext: &'a mut (dyn io::Read + Send + Sync)) -> Result>; } /// Builder for [`SOP::decrypt`]. pub trait Decrypt<'a> { /// Makes SOP consider signatures before this date invalid. fn verify_not_before(self: Box, t: SystemTime) -> Box + 'a>; /// Makes SOP consider signatures after this date invalid. fn verify_not_after(self: Box, t: SystemTime) -> Box + 'a>; /// Adds the verification cert. fn verify_with_cert(self: Box, cert: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Adds the verification certs. /// /// Like [`Decrypt::verify_with_cert`], but for multiple certs. fn verify_with_certs(self: Box, certs: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Tries to decrypt with the given session key. fn with_session_key(self: Box, sk: SessionKey) -> Result + 'a>>; /// Tries to decrypt with the given password. fn with_password(self: Box, password: Password) -> Result + 'a>>; /// Adds the decryption key. fn with_key(self: Box, key: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Adds the decryption keys. fn with_keys(self: Box, key: &mut (dyn io::Read + Send + Sync)) -> Result + 'a>>; /// Decrypts `ciphertext`, returning verification results and /// plaintext. fn ciphertext(self: Box, ciphertext: &'a mut (dyn io::Read + Send + Sync)) -> Result, Vec)> + 'a>>; } /// Builder for [`SOP::armor`]. pub trait Armor<'a> { /// Overrides automatic detection of label. fn label(self: Box, label: ArmorLabel) -> Box + 'a>; /// Armors `data`. fn data(self: Box, data: &'a mut (dyn io::Read + Send + Sync)) -> Result>; } /// Builder for [`SOP::dearmor`]. pub trait Dearmor<'a> { /// Dearmors `data`. fn data(self: Box, data: &'a mut (dyn io::Read + Send + Sync)) -> Result>; } /// A normalized password. /// /// See [Passwords are Human-Readable] in the SOP spec. /// /// [Passwords are Human-Readable]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-02#section-7.8 pub struct Password(Box<[u8]>); impl From<&str> for Password { fn from(p: &str) -> Self { p.to_string().into() } } impl From for Password { fn from(p: String) -> Self { Password(Self::normalize(p).into_bytes().into_boxed_slice()) } } impl AsRef<[u8]> for Password { fn as_ref(&self) -> &[u8] { &self.0 } } impl Password { fn normalize(p: String) -> String { // XXX: Maybe do additional checks. let normalized = p.trim_end().to_string(); // Wipe original string. let mut p = p.into_bytes(); unsafe { memsec::memzero(p.as_mut_ptr(), p.len()); } normalized } } impl Drop for Password { fn drop(&mut self) { unsafe { memsec::memzero(self.0.as_mut_ptr(), self.0.len()); } } } /// An operation ready to be executed. /// /// To execute the operation, either supply an [`std::io::Write`]r /// using [`Ready::write_to`] to write the resulting data to, or use /// [`Ready::to_vec`] to write to a `Vec`. pub trait Ready { /// Executes the operation writing the result to `sink`. fn write_to(self: Box, sink: &mut (dyn io::Write + Send + Sync)) -> Result<()>; /// Executes the operation writing the result into a `Vec`. fn to_vec(self: Box) -> Result> { let mut v = Vec::new(); self.write_to(&mut v)?; Ok(v) } } /// An operation that returns a value ready to be executed. /// /// To execute the operation, either supply an [`std::io::Write`]r /// using [`Ready::write_to`] to write the resulting data to, or use /// [`Ready::to_vec`] to write to a `Vec`. pub trait ReadyWithResult { /// Executes the operation writing the result to `sink`. fn write_to(self: Box, sink: &mut (dyn io::Write + Send + Sync)) -> Result; /// Executes the operation writing the result into a `Vec`. fn to_vec(self: Box) -> Result<(T, Vec)> { let mut v = Vec::new(); let r = self.write_to(&mut v)?; Ok((r, v)) } } /// A session key. pub struct SessionKey { algorithm: u8, key: Box<[u8]>, } impl SessionKey { /// Creates a new session key object. pub fn new(algorithm: A, key: K) -> Result where A: Into, K: AsRef<[u8]>, { // XXX: Maybe sanity check key lengths. Ok(SessionKey { algorithm: algorithm.into(), key: key.as_ref().to_vec().into(), }) } /// Returns the symmetric algorithm octet. pub fn algorithm(&self) -> u8 { self.algorithm } /// Returns the session key. pub fn key(&self) -> &[u8] { &self.key } } impl fmt::Display for SessionKey { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}:", self.algorithm)?; for b in &self.key[..] { write!(f, "{:02X}", b)? } Ok(()) } } impl Drop for SessionKey { fn drop(&mut self) { unsafe { memsec::memzero(self.key.as_mut_ptr(), self.key.len()); } } } impl std::str::FromStr for SessionKey { type Err = ParseError; fn from_str(sk: &str) -> ParseResult { // The SOP format is: // // ":" // // We most likely will change the first field, so we split // from the end of the string using `rsplit`, which puts the // last segment first. This is rather unexpected. Reverse // it. let fields = sk.rsplit(':').rev().collect::>(); if fields.len() != 2 { return Err(ParseError(format!( "Expected two colon-separated fields, got {:?}", fields))); } let algo: u8 = fields[0].parse().map_err( |e| ParseError(format!("Failed to parse algorithm: {}", e)))?; let sk = from_hex(&fields[1], true)?; Self::new(algo, sk).map_err( |e| ParseError(format!("Bad session key: {}", e))) } } /// A helpful function for converting a hexadecimal string to binary. /// This function skips whitespace if `pretty` is set. fn from_hex(hex: &str, pretty: bool) -> ParseResult> { const BAD: u8 = 255u8; const X: u8 = 'x' as u8; let mut nibbles = hex.chars().filter_map(|x| { match x { '0' => Some(0u8), '1' => Some(1u8), '2' => Some(2u8), '3' => Some(3u8), '4' => Some(4u8), '5' => Some(5u8), '6' => Some(6u8), '7' => Some(7u8), '8' => Some(8u8), '9' => Some(9u8), 'a' | 'A' => Some(10u8), 'b' | 'B' => Some(11u8), 'c' | 'C' => Some(12u8), 'd' | 'D' => Some(13u8), 'e' | 'E' => Some(14u8), 'f' | 'F' => Some(15u8), 'x' | 'X' if pretty => Some(X), _ if pretty && x.is_whitespace() => None, _ => Some(BAD), } }).collect::>(); if pretty && nibbles.len() >= 2 && nibbles[0] == 0 && nibbles[1] == X { // Drop '0x' prefix. nibbles.remove(0); nibbles.remove(0); } if nibbles.iter().any(|&b| b == BAD || b == X) { // Not a hex character. return Err(ParseError("Invalid characters".into())); } // We need an even number of nibbles. if nibbles.len() % 2 != 0 { return Err(ParseError("Odd number of nibbles".into())); } let bytes = nibbles.chunks(2).map(|nibbles| { (nibbles[0] << 4) | nibbles[1] }).collect::>(); Ok(bytes) } /// Signature type. /// /// This is used by [`SOP::sign`] to select the signature type. See /// [`sop sign`]. /// /// [`sop sign`]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-02#section-3.4 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum SignAs { Binary, Text, } impl Default for SignAs { fn default() -> Self { SignAs::Binary } } impl From for SignAs { fn from(a: EncryptAs) -> Self { match a { EncryptAs::Binary => SignAs::Binary, EncryptAs::Text => SignAs::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 => SignAs::Binary, } } } impl std::str::FromStr for SignAs { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "binary" => Ok(SignAs::Binary), "text" => Ok(SignAs::Text), _ => Err(ParseError(format!( "{:?}, 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"), } } } /// Plaintext data format. /// /// This is used by [`SOP::encrypt`] to select the data format. See /// [`sop encrypt`]. /// /// [`sop encrypt`]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-02#section-3.6 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum EncryptAs { Binary, Text, MIME, } impl Default for EncryptAs { fn default() -> Self { EncryptAs::Binary } } impl std::str::FromStr for EncryptAs { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "binary" => Ok(EncryptAs::Binary), "text" => Ok(EncryptAs::Text), "mime" => Ok(EncryptAs::MIME), _ => Err(ParseError(format!( "{}, 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"), } } } /// The ASCII Armor Label. /// /// This is used by [`SOP::armor`] to control the framing that is /// emitted. See [`sop armor`]. /// /// [`sop armor`]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-02#section-3.8 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum ArmorLabel { Auto, Sig, Key, Cert, Message, } impl Default for ArmorLabel { fn default() -> Self { ArmorLabel::Auto } } impl std::str::FromStr for ArmorLabel { type Err = ParseError; fn from_str(s: &str) -> std::result::Result { match s { "auto" => Ok(ArmorLabel::Auto), "sig" => Ok(ArmorLabel::Sig), "key" => Ok(ArmorLabel::Key), "cert" => Ok(ArmorLabel::Cert), "message" => Ok(ArmorLabel::Message), _ => Err(ParseError(format!( "{:?}, expected one of \ {{auto|sig|key|cert|message}}", s))), } } } impl fmt::Display for ArmorLabel { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { ArmorLabel::Auto => f.write_str("auto"), ArmorLabel::Sig => f.write_str("sig"), ArmorLabel::Key => f.write_str("key"), ArmorLabel::Cert => f.write_str("cert"), ArmorLabel::Message => f.write_str("message"), } } } /// Indicates the cryptographic digest used when making a signature. /// /// It is useful specifically when generating signed PGP/MIME objects, /// which want a `micalg=` parameter for the `multipart/signed` /// content type as described in section 5 of [RFC3156]. /// /// [RFC3156]: https://datatracker.ietf.org/doc/html/rfc3156 #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Micalg { /// Rivest et.al. message digest 5 (deprecated). MD5, /// NIST Secure Hash Algorithm (deprecated). SHA1, /// RIPEMD-160 (deprecated). RipeMD, /// 256-bit version of SHA2. SHA256, /// 384-bit version of SHA2. SHA384, /// 512-bit version of SHA2. SHA512, /// 224-bit version of SHA2. SHA224, /// Unknown hash algorithm. Unknown(String), } impl From for Micalg { fn from(o: u8) -> Self { match o { 1 => Micalg::MD5, 2 => Micalg::SHA1, 3 => Micalg::RipeMD, 8 => Micalg::SHA256, 9 => Micalg::SHA384, 10 => Micalg::SHA512, 11 => Micalg::SHA224, u => Micalg::Unknown(format!("unknown-algo-{}", u)), } } } impl fmt::Display for Micalg { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("pgp-")?; match self { Micalg::MD5 => f.write_str("md5"), Micalg::SHA1 => f.write_str("sha1"), Micalg::RipeMD => f.write_str("ripemd160"), Micalg::SHA256 => f.write_str("sha256"), Micalg::SHA384 => f.write_str("sha384"), Micalg::SHA512 => f.write_str("sha512"), Micalg::SHA224 => f.write_str("sha224"), Micalg::Unknown(a) => f.write_str(&a.to_lowercase()), } } } /// Result specialization. pub type Result = std::result::Result; /// SOP errors. /// /// These are the errors [defined] by the Stateless OpenPGP Protocol. /// /// [defined]: https://datatracker.ietf.org/doc/html/draft-dkg-openpgp-stateless-cli-02#section-6 #[derive(thiserror::Error, Debug)] pub enum Error { /// No acceptable signatures found ("sop verify"). #[error("No acceptable signatures found")] NoSignature, /// Asymmetric algorithm unsupported ("sop encrypt"). #[error("Asymmetric algorithm unsupported")] UnsupportedAsymmetricAlgo, /// Certificate not encryption-capable (e.g., expired, revoked, /// unacceptable usage flags) ("sop encrypt"). #[error("Certificate not encryption-capable")] CertCannotEncrypt, /// Key not signature-capable (e.g., expired, revoked, /// unacceptable usage flags) (`sop sign` and `sop encrypt` with /// `--sign-with`). #[error("Key not signing-capable")] KeyCannotSign, /// Missing required argument. #[error("Missing required argument")] MissingArg, /// Incomplete verification instructions ("sop decrypt"). #[error("Incomplete verification instructions")] IncompleteVerification, /// Unable to decrypt ("sop decrypt"). #[error("Unable to decrypt")] CannotDecrypt, /// Non-"UTF-8" or otherwise unreliable password ("sop encrypt"). #[error("Non-UTF-8 or otherwise unreliable password")] PasswordNotHumanReadable, /// Unsupported option. #[error("Unsupported option")] UnsupportedOption, /// Invalid data type (no secret key where "KEY" expected, etc). #[error("Invalid data type")] BadData, /// Non-text input where text expected. #[error("Non-text input where text expected")] ExpectedText, /// Output file already exists. #[error("Output file already exists")] OutputExists, /// Input file does not exist. #[error("Input file does not exist")] MissingInput, /// A "KEY" input is protected (locked) with a password, and "sop" cannot /// unlock it. #[error("A KEY input is protected with a password")] KeyIsProtected, /// A indirect input parameter is a special designator (it starts with /// "@"), and a filename matching the designator is actually present. #[error("A indirect input parameter is a special designator matches file")] AmbiguousInput, /// Operation not implemented. #[error("Operation not implemented")] NotImplemented, /// An IO error occurred. #[error("IO error")] IoError(#[from] std::io::Error), } /// Errors during parsing of SOP string representations. /// /// For types with a defined string representation, we implement /// [`std::str::FromStr`] for parsing. This type is used to report /// errors during parsing. #[derive(thiserror::Error, Debug)] #[error("Invalid argument: {}", _0)] pub struct ParseError(String); /// Convenience alias. type ParseResult = std::result::Result; #[cfg(test)] mod tests { use super::*; #[test] fn session_key_roundtrip() -> Result<()> { for algo in &[9, 13] { let sk = SessionKey::new( *algo, &[0xE1, 0x48, 0x97, 0x81, 0xAA, 0x22, 0xE1, 0xBF, 0x6E, 0x3E, 0x61, 0x74, 0x8C, 0x8D, 0x3F, 0x35, 0x50, 0x7C, 0x80, 0x9E, 0x95, 0x64, 0x86, 0x87, 0xC7, 0xE4, 0xB9, 0xAF, 0x86, 0x17, 0xD3, 0xAE])?; let sk_s = sk.to_string(); let sk_p: SessionKey = sk_s.parse().unwrap(); assert_eq!(sk.algorithm(), sk_p.algorithm()); assert_eq!(sk.key(), sk_p.key()); } Ok(()) } #[test] fn sign_as_roundtrip() -> Result<()> { use SignAs::*; for a in &[Text, Binary] { let s = a.to_string(); let b: SignAs = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } #[test] fn encrypt_as_roundtrip() -> Result<()> { use EncryptAs::*; for a in &[Text, Binary, MIME] { let s = a.to_string(); let b: EncryptAs = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } #[test] fn armor_label_roundtrip() -> Result<()> { use ArmorLabel::*; for a in &[Auto, Sig, Key, Cert, Message] { let s = a.to_string(); let b: ArmorLabel = s.parse().unwrap(); assert_eq!(a, &b); } Ok(()) } }