Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ to 1.0.0 are beta releases.
altered to store a `Box<dyn Callbacks>` internally.
- `age::Decryptor::trial_decrypt` and `age::Decryptor::trial_decrypt_seekable`
both no longer take a `request_passphrase` argument.
- `age::cli_common::read_secret`:
- Takes an additional `prompt` parameter.
- Uses the system `pinentry` binary for requesting secrets if available.
- Returns `pinentry::Error` instead of `io::Error`.
- `age::cli_common::read_or_generate_passphrase` now returns `pinentry::Error`
instead of `io::Error`.

### Fixed
- Fixed several crashes in the armored format reader, found by fuzzing. The
Expand Down
3 changes: 2 additions & 1 deletion age/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ zeroize = "1"
console = { version = "0.9", optional = true }
dialoguer = { version = "0.4", optional = true }
dirs = { version = "2", optional = true }
pinentry = { version = "0.1", optional = true }

[dev-dependencies]
criterion = "0.3"
Expand All @@ -75,7 +76,7 @@ quickcheck_macros = "0.8"

[features]
default = []
cli-common = ["console", "dialoguer", "dirs"]
cli-common = ["console", "dialoguer", "dirs", "pinentry"]
unstable = ["num-traits", "rsa"]

[[bench]]
Expand Down
57 changes: 47 additions & 10 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Common helpers for CLI binaries.

use dialoguer::PasswordInput;
use pinentry::PassphraseInput;
use rand::{
distributions::{Distribution, Uniform},
rngs::OsRng,
Expand Down Expand Up @@ -66,24 +67,59 @@ where
Ok(identities)
}

/// Reads a secret from stdin. If `confirm.is_some()` then an empty secret is allowed.
pub fn read_secret(prompt: &str, confirm: Option<&str>) -> io::Result<SecretString> {
let mut input = PasswordInput::new();
input.with_prompt(prompt);
if let Some(confirm_prompt) = confirm {
/// Requests a secret from the user.
///
/// If a `pinentry` binary is available on the system, it is used to request the secret.
/// If not, we fall back to requesting directly in the CLI via stdin.
///
/// # Parameters
///
/// - `description` is the primary information provided to the user about the secret
/// being requested. It is printed in all cases.
/// - `prompt` is a short phrase such as "Passphrase" or "PIN". It is printed in front of
/// the input field when `pinentry` is used.
/// - `confirm` is an optional short phrase such as "Confirm passphrase". Setting it
/// enables input confirmation.
/// - If `confirm.is_some()` then an empty secret is allowed.
pub fn read_secret(
description: &str,
prompt: &str,
confirm: Option<&str>,
) -> pinentry::Result<SecretString> {
if let Some(mut input) = PassphraseInput::with_default_binary() {
// pinentry binary is available!
input
.with_description(description)
.with_prompt(prompt)
.with_timeout(30);
if let Some(confirm_prompt) = confirm {
input.with_confirmation(confirm_prompt, "Inputs do not match");
} else {
input.required("Input is required");
}
input.interact()
} else {
// Fall back to CLI interface.
let mut input = PasswordInput::new();
input.with_prompt(description);
if let Some(confirm_prompt) = confirm {
input
.with_confirmation(confirm_prompt, "Inputs do not match")
.allow_empty_password(true);
}
input
.with_confirmation(confirm_prompt, "Inputs do not match")
.allow_empty_password(true);
.interact()
.map(SecretString::new)
.map_err(|e| e.into())
}
input.interact().map(SecretString::new)
}

/// Implementation of age callbacks that makes requests to the user via the UI.
pub struct UiCallbacks;

impl Callbacks for UiCallbacks {
fn request_passphrase(&self, description: &str) -> Option<SecretString> {
read_secret(description, None).ok()
read_secret(description, "Passphrase", None).ok()
}
}

Expand All @@ -96,9 +132,10 @@ pub enum Passphrase {
}

/// Reads a passphrase from stdin, or generates a secure one if none is provided.
pub fn read_or_generate_passphrase() -> io::Result<Passphrase> {
pub fn read_or_generate_passphrase() -> pinentry::Result<Passphrase> {
let res = read_secret(
"Type passphrase (leave empty to autogenerate a secure one)",
"Passphrase",
Some("Confirm passphrase"),
)?;

Expand Down
5 changes: 5 additions & 0 deletions rage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ to 1.0.0 are beta releases.

## [Unreleased]
(relative to the CLI tools in `age 0.2.0`)

### Changed
- If a `pinentry` binary is available, it will be used preferentially to request
secrets such as passphrases. The previous CLI input will be used if `pinentry`
is not available.
1 change: 1 addition & 0 deletions rage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ env_logger = "0.7"
gumdrop = "0.6"
log = "0.4"
minreq = { version = "1.4", features = ["https"] }
pinentry = "0.1"
secrecy = "0.6"

# rage-mount dependencies
Expand Down
2 changes: 1 addition & 1 deletion rage/src/bin/rage-mount/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn main() -> Result<(), Error> {
return Err(Error::MixedIdentityAndPassphrase);
}

match read_secret("Type passphrase", None) {
match read_secret("Type passphrase", "Passphrase", None) {
Ok(passphrase) => age::Decryptor::Passphrase {
passphrase,
max_work_factor: opts.max_work_factor,
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 @@ -8,6 +8,7 @@ pub(crate) enum EncryptError {
MissingRecipients,
MixedRecipientAndPassphrase,
PassphraseWithoutFileArgument,
TimedOut(String),
UnknownAlias(String),
}

Expand Down Expand Up @@ -37,6 +38,7 @@ impl fmt::Display for EncryptError {
f,
"File to encrypt must be passed as an argument when using -p/--passphrase"
),
EncryptError::TimedOut(source) => write!(f, "Timed out waiting for {}", source),
EncryptError::UnknownAlias(alias) => write!(f, "Unknown {}", alias),
}
}
Expand All @@ -50,6 +52,7 @@ pub(crate) enum DecryptError {
MixedIdentityAndPassphrase,
PassphraseWithoutFileArgument,
RecipientFlag,
TimedOut(String),
UnsupportedKey(String, age::keys::UnsupportedKey),
}

Expand Down Expand Up @@ -100,6 +103,7 @@ impl fmt::Display for DecryptError {
"Did you mean to use -i/--identity to specify a private key?"
)
}
DecryptError::TimedOut(source) => write!(f, "Timed out waiting for {}", source),
DecryptError::UnsupportedKey(filename, k) => {
writeln!(f, "Unsupported key: {}", filename)?;
writeln!(f)?;
Expand Down
28 changes: 25 additions & 3 deletions rage/src/bin/rage/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,18 @@ fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
eprintln!(" {}", new_passphrase.expose_secret());
age::Encryptor::Passphrase(new_passphrase)
}
Err(_) => return Ok(()),
Err(pinentry::Error::Cancelled) => return Ok(()),
Err(pinentry::Error::Timeout) => {
return Err(error::EncryptError::TimedOut("passphrase input".to_owned()))
}
Err(pinentry::Error::Gpg(e)) => {
// Pretend it is an I/O error
return Err(error::EncryptError::Io(io::Error::new(
io::ErrorKind::Other,
format!("{}", e),
)));
}
Err(pinentry::Error::Io(e)) => return Err(error::EncryptError::Io(e)),
}
} else {
if opts.recipient.is_empty() {
Expand Down Expand Up @@ -271,12 +282,23 @@ fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
return Err(error::DecryptError::PassphraseWithoutFileArgument);
}

match read_secret("Type passphrase", None) {
match read_secret("Type passphrase", "Passphrase", None) {
Ok(passphrase) => age::Decryptor::Passphrase {
passphrase,
max_work_factor: opts.max_work_factor,
},
Err(_) => return Ok(()),
Err(pinentry::Error::Cancelled) => return Ok(()),
Err(pinentry::Error::Timeout) => {
return Err(error::DecryptError::TimedOut("passphrase input".to_owned()))
}
Err(pinentry::Error::Gpg(e)) => {
// Pretend it is an I/O error
return Err(error::DecryptError::Io(io::Error::new(
io::ErrorKind::Other,
format!("{}", e),
)));
}
Err(pinentry::Error::Io(e)) => return Err(error::DecryptError::Io(e)),
}
} else {
let identities = read_identities(opts.identity, |default_filename| {
Expand Down