Skip to content

Commit

Permalink
Merge pull request #95 from str4d/94-cli-usability
Browse files Browse the repository at this point in the history
Improve CLI usability
  • Loading branch information
str4d authored Mar 24, 2020
2 parents bf4a038 + e79dc5a commit e923b41
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 65 deletions.
3 changes: 3 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ to 1.0.0 are beta releases.
- `age::Encryptor::with_user_passphrase(SecretString)`
- Support for encrypted OpenSSH keys created with `ssh-keygen` prior to OpenSSH
7.6.
- `age::cli_common::file_io::OutputWriter::is_terminal`

### Changed
- `age::Decryptor` has been refactored to auto-detect the decryption type. As a
Expand All @@ -31,6 +32,8 @@ to 1.0.0 are beta releases.
constructors.
- `age::Encryptor::wrap_output` now consumes `self`, making it harder to
accidentally reuse a passphrase for multiple encrypted files.
- `age::cli_common::read_identities` now takes an additional `file_not_found`
parameter for customising the error when an identity filename is not found.

### Removed
- `age::Decryptor::trial_decrypt` (replaced by context-specific decryptors).
Expand Down
14 changes: 12 additions & 2 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ pub fn get_config_dir() -> Option<PathBuf> {

/// Reads identities from the provided files if given, or the default system
/// locations if no files are given.
pub fn read_identities<E, F>(filenames: Vec<String>, no_default: F) -> Result<Vec<Identity>, E>
pub fn read_identities<E, F, G>(
filenames: Vec<String>,
no_default: F,
file_not_found: G,
) -> Result<Vec<Identity>, E>
where
E: From<io::Error>,
F: FnOnce(&str) -> E,
G: Fn(String) -> E,
{
let mut identities = vec![];

Expand All @@ -61,7 +66,12 @@ where
identities.extend(Identity::from_buffer(buf)?);
} else {
for filename in filenames {
identities.extend(Identity::from_file(filename)?);
identities.extend(Identity::from_file(filename.clone()).map_err(
|e| match e.kind() {
io::ErrorKind::NotFound => file_not_found(filename),
_ => e.into(),
},
)?);
}
}

Expand Down
8 changes: 8 additions & 0 deletions age/src/cli_common/file_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,14 @@ impl OutputWriter {

Ok(OutputWriter::Stdout(StdoutWriter::new(format, is_tty)))
}

/// Returns true if this output is to a terminal, and a user will likely see it.
pub fn is_terminal(&self) -> bool {
match self {
OutputWriter::File(..) => false,
OutputWriter::Stdout(w) => w.is_tty,
}
}
}

impl Write for OutputWriter {
Expand Down
64 changes: 33 additions & 31 deletions rage/examples/generate-docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,70 +22,70 @@ fn rage_page() {
Flag::new()
.short("-h")
.long("--help")
.help("Display help text and exit"),
.help("Display help text and exit."),
)
.flag(
Flag::new()
.short("-V")
.long("--version")
.help("Display version and exit"),
.help("Display version info and exit."),
)
.flag(
Flag::new()
.short("-d")
.long("--decrypt")
.help("Decrypt the input (default is to encrypt)"),
.help("Decrypt the input. By default, the input is encrypted."),
)
.flag(
Flag::new()
.short("-p")
.long("--passphrase")
.help("Use a passphrase instead of public keys"),
.help("Encrypt with a passphrase instead of recipients."),
)
.flag(
Flag::new()
.short("-a")
.long("--armor")
.help("Create ASCII armored output (default is age binary format)"),
.help("Encrypt to a PEM encoded format."),
)
.option(
Opt::new("recipient")
Opt::new("RECIPIENT")
.short("-r")
.long("--recipient")
.help("A recipient to encrypt to (can be repeated)"),
.help("Encrypt to the specified RECIPIENT. May be repeated."),
)
.option(
Opt::new("identity")
Opt::new("IDENTITY")
.short("-i")
.long("--identity")
.help("An identity to decrypt with (can be repeated)"),
.help("Use the private key file at IDENTITY. May be repeated."),
)
.option(
Opt::new("output")
Opt::new("OUTPUT")
.short("-o")
.long("--output")
.help("The file path to write output to (defaults to stdout)"),
.help("Write the result to the file at path OUTPUT. Defaults to standard output."),
)
.option(
Opt::new("WF")
.long("--max-work-factor")
.help("The maximum work factor to allow for passphrase decryption"),
.help("The maximum work factor to allow for passphrase decryption."),
)
.arg(Arg::new("[INPUT_FILE (defaults to stdin)]"))
.example(Example::new().text("Encryption to a public key").command(
.example(Example::new().text("Encryption to a recipient").command(
"echo \"_o/\" | rage -o hello.age -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u",
))
.example(
Example::new()
.text("Encryption to multiple public keys (with default output to stdout)")
.text("Encryption to multiple recipients (with default output to stdout)")
.command(
"echo \"_o/\" | rage -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u \
-r age1ex4ty8ppg02555at009uwu5vlk5686k3f23e7mac9z093uvzfp8sxr5jum > hello.age",
),
)
.example(
Example::new()
.text("Encryption with a password (interactive only, use public keys for batch!)")
.text("Encryption with a password (interactive only, use recipients for batch!)")
.command("rage -p -o hello.txt.age hello.txt")
.output("Type passphrase:"),
)
Expand All @@ -104,21 +104,21 @@ fn rage_page() {
)
.example(
Example::new()
.text("Decryption with keys at ~/.config/age/keys.txt")
.text("Decryption with identities at ~/.config/age/keys.txt")
.command("rage --decrypt hello.age")
.output("_o/"),
)
.example(
Example::new()
.text("Decryption with custom keys")
.text("Decryption with custom identities")
.command("rage -d -o hello -i keyA.txt -i keyB.txt hello.age"),
);
#[cfg(feature = "unstable")]
let builder = builder
.option(
Opt::new("aliases")
Opt::new("ALIASES")
.long("--aliases")
.help("The list of aliases to load (defaults to ~/.config/age/aliases.txt)"),
.help("Load the aliases list from ALIASES. Defaults to ~/.config/age/aliases.txt"),
)
.example(
Example::new()
Expand Down Expand Up @@ -146,19 +146,18 @@ fn rage_keygen_page() {
Flag::new()
.short("-h")
.long("--help")
.help("Display help text and exit"),
.help("Display help text and exit."),
)
.flag(
Flag::new()
.short("-V")
.long("--version")
.help("Display version and exit"),
.help("Display version info and exit."),
)
.option(
Opt::new("output")
.short("-o")
.long("--output")
.help("The file path to write the key pair to (defaults to stdout)"),
Opt::new("OUTPUT").short("-o").long("--output").help(
"Write the key pair to the file at path OUTPUT. Defaults to standard output.",
),
)
.example(
Example::new()
Expand All @@ -168,7 +167,10 @@ fn rage_keygen_page() {
.example(
Example::new()
.text("Generate a new key pair and save it to a file")
.command("rage-keygen -o key.txt"),
.command("rage-keygen -o key.txt")
.output(
"Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
),
)
.render();

Expand All @@ -183,25 +185,25 @@ fn rage_mount_page() {
Flag::new()
.short("-h")
.long("--help")
.help("Display help text and exit"),
.help("Display help text and exit."),
)
.flag(
Flag::new()
.short("-V")
.long("--version")
.help("Display version and exit"),
.help("Display version info and exit."),
)
.flag(
Flag::new()
.short("-t")
.long("--types")
.help("The type of the filesystem (one of \"tar\", \"zip\")"),
.help("The type of the filesystem (one of \"tar\", \"zip\")."),
)
.option(
Opt::new("identity")
Opt::new("IDENTITY")
.short("-i")
.long("--identity")
.help("An identity to decrypt with (can be repeated)"),
.help("Use the private key file at IDENTITY. May be repeated."),
)
.arg(Arg::new("filename"))
.arg(Arg::new("mountpoint"))
Expand Down
13 changes: 9 additions & 4 deletions rage/src/bin/rage-keygen/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ use std::io::Write;

#[derive(Debug, Options)]
struct AgeOptions {
#[options(help = "print help message")]
#[options(help = "Print this help message and exit.")]
help: bool,

#[options(help = "print version info and exit", short = "V")]
#[options(help = "Print version info and exit.", short = "V")]
version: bool,

#[options(help = "output to OUTPUT (default stdout)")]
#[options(help = "Write the result to the file at path OUTPUT. Defaults to standard output.")]
output: Option<String>,
}

Expand All @@ -36,14 +36,19 @@ fn main() {
};

let sk = age::keys::SecretKey::generate();
let pk = sk.to_public();

if let Err(e) = (|| {
if !output.is_terminal() {
eprintln!("Public key: {}", pk);
}

writeln!(
output,
"# created: {}",
chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
)?;
writeln!(output, "# public key: {}", sk.to_public())?;
writeln!(output, "# public key: {}", pk)?;
writeln!(output, "{}", sk.to_string().expose_secret())
})() {
error!("Failed to write to output: {}", e);
Expand Down
14 changes: 7 additions & 7 deletions rage/src/bin/rage-mount/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,29 +79,29 @@ impl fmt::Debug for Error {

#[derive(Debug, Options)]
struct AgeMountOptions {
#[options(free, help = "The encrypted filesystem to mount")]
#[options(free, help = "The encrypted filesystem to mount.")]
filename: String,

#[options(free, help = "The directory to mount the filesystem at")]
#[options(free, help = "The directory to mount the filesystem at.")]
mountpoint: String,

#[options(help = "print help message")]
#[options(help = "Print this help message and exit.")]
help: bool,

#[options(help = "print version info and exit", short = "V")]
#[options(help = "Print version info and exit.", short = "V")]
version: bool,

#[options(help = "indicates the filesystem type (one of \"tar\", \"zip\")")]
#[options(help = "Indicates the filesystem type (one of \"tar\", \"zip\").")]
types: String,

#[options(
help = "maximum work factor to allow for passphrase decryption",
help = "Maximum work factor to allow for passphrase decryption.",
meta = "WF",
no_short
)]
max_work_factor: Option<u8>,

#[options(help = "identity to decrypt with (may be repeated)")]
#[options(help = "Use the private key file at IDENTITY. May be repeated.")]
identity: Vec<String>,
}

Expand Down
4 changes: 4 additions & 0 deletions rage/src/bin/rage/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ impl fmt::Display for EncryptError {
pub(crate) enum DecryptError {
Age(age::Error),
ArmorFlag,
IdentityNotFound(String),
Io(io::Error),
MissingIdentities(String),
PassphraseFlag,
Expand Down Expand Up @@ -103,6 +104,9 @@ impl fmt::Display for DecryptError {
writeln!(f, "-a/--armor can't be used with -d/--decrypt.")?;
write!(f, "Note that armored files are detected automatically.")
}
DecryptError::IdentityNotFound(filename) => {
write!(f, "Identity file not found: {}", filename)
}
DecryptError::Io(e) => write!(f, "{}", e),
DecryptError::MissingIdentities(default_filename) => {
writeln!(f, "Missing identities.")?;
Expand Down
Loading

0 comments on commit e923b41

Please sign in to comment.