Skip to content

Commit

Permalink
Merge pull request #248 from str4d/late-night-twitch-stream
Browse files Browse the repository at this point in the history
Several new features and UX changes
  • Loading branch information
str4d authored Aug 22, 2021
2 parents b189384 + 33956aa commit c028790
Show file tree
Hide file tree
Showing 20 changed files with 667 additions and 100 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ PATH is a path to a file containing age recipients, one per line
IDENTITY is a path to a file with age identities, one per line
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
Passphrase-encrypted age identity files can be used as identity files.
Multiple identities may be provided, and any unused ones will be ignored.
```

Expand Down Expand Up @@ -89,6 +90,26 @@ $ rage -d example.png.age >example.png
Type passphrase: [hidden]
```

### Passphrase-protected identity files

If an identity file passed to `-i/--identity` is a passphrase-encrypted age
file, it will be automatically decrypted.

```
$ rage -p -o key.age <(rage-keygen)
Public key: age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz
Type passphrase (leave empty to autogenerate a secure one): [hidden]
Using an autogenerated passphrase:
flash-bean-celery-network-curious-flower-salt-amateur-fence-giant
$ rage -r age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz secrets.txt > secrets.txt.age
$ rage -d -i key.age secrets.txt.age > secrets.txt
Type passphrase: [hidden]
```

Passphrase-protected identity files are not necessary for most use cases, where
access to the encrypted identity file implies access to the whole system.
However, they can be useful if the identity file is stored remotely.

### SSH keys

As a convenience feature, rage also supports encrypting to `ssh-rsa` and
Expand Down
26 changes: 26 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,34 @@ and this project adheres to Rust's notion of
to 1.0.0 are beta releases.

## [Unreleased]
### Added
- `age::encrypted::Identity`, for decrypting files with passphrase-encrypted
age identity files.
- `age::IdentityFileEntry` enum, representing the possible kinds of entries
within an age identity file.
- `age::{DecryptError, EncryptError, PluginError}: Clone` bounds.
- `age::cli_common::UiCallbacks: Clone + Copy` bounds.

### Changed
- MSRV is now 1.51.0.
- `age::IdentityFile::into_identities` now returns `Vec<IdentityFileEntry>`.
- `age::cli_common::read_identities`:
- Encrypted age files will now be parsed and assumed to be encrypted age
identities. This assumption is checked at file-decryption time.
- New `max_work_factor` parameter for controlling the work factor when
decrypting encrypted identities.
- New `identity_encrypted_without_passphrase` parameter for customising the
error when an invalid encrypted identity is found.
- Identities are now returned in the same order as `filenames` (and
top-to-bottom from within each file). Plugin identities are no longer
coalesced; there is one `Box<dyn Identity>` per plugin identity.
- `age::Callbacks::prompt` has been renamed to `Callbacks::display_message`.
- `age::cli_common::UiCallbacks::display_message` no longer uses `pinentry`
(which displays a temporary prompt that can be dismissed), so the message is
now part of the visible CLI output.

### Removed
- `IdentityFile::split_into` (replaced by `IdentityFileEntry::Plugin`).

## [0.6.0] - 2021-05-02
### Security
Expand Down
6 changes: 6 additions & 0 deletions age/i18n/en-US/age.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ err-plugin-identity = '{$plugin_name}' couldn't use an identity: {$message}
err-plugin-recipient = '{$plugin_name}' couldn't use recipient {$recipient}: {$message}
err-plugin-multiple = Plugin returned multiple errors:
## Encrypted identities

encrypted-passphrase-prompt = Type passphrase for encrypted identity '{$filename}'
encrypted-warn-no-match = Warning: encrypted identity file '{$filename}' didn't match file's recipients
## SSH identities

ssh-passphrase-prompt = Type passphrase for OpenSSH key '{$filename}'
Expand Down
76 changes: 25 additions & 51 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Common helpers for CLI binaries.

use pinentry::{MessageDialog, PassphraseInput};
use pinentry::PassphraseInput;
use rand::{
distributions::{Distribution, Uniform},
rngs::OsRng,
Expand All @@ -11,33 +11,45 @@ use std::fs::File;
use std::io::{self, BufReader};
use subtle::ConstantTimeEq;

use crate::{fl, identity::IdentityFile, Callbacks, Identity};

#[cfg(feature = "plugin")]
use crate::plugin;
use crate::{armor::ArmoredReader, fl, identity::IdentityFile, Callbacks, Identity};

pub mod file_io;

const BIP39_WORDLIST: &str = include_str!("../assets/bip39-english.txt");

/// Reads identities from the provided files if given, or the default system
/// locations if no files are given.
pub fn read_identities<E, G>(
pub fn read_identities<E, G, H>(
filenames: Vec<String>,
max_work_factor: Option<u8>,
file_not_found: G,
identity_encrypted_without_passphrase: H,
#[cfg(feature = "ssh")] unsupported_ssh: impl Fn(String, crate::ssh::UnsupportedKey) -> E,
) -> Result<Vec<Box<dyn Identity>>, E>
where
E: From<crate::DecryptError>,
E: From<io::Error>,
G: Fn(String) -> E,
H: Fn(String) -> E,
{
let mut identities: Vec<Box<dyn Identity>> = vec![];

#[cfg(feature = "plugin")]
let mut plugin_identities: Vec<plugin::Identity> = vec![];

for filename in filenames {
// Try parsing as an encrypted age identity.
if let Ok(identity) = crate::encrypted::Identity::from_buffer(
ArmoredReader::new(BufReader::new(File::open(&filename)?)),
Some(filename.clone()),
UiCallbacks,
max_work_factor,
) {
if let Some(identity) = identity {
identities.push(Box::new(identity));
continue;
} else {
return Err(identity_encrypted_without_passphrase(filename));
}
}

// Try parsing as a single multi-line SSH identity.
#[cfg(feature = "ssh")]
match crate::ssh::Identity::from_buffer(
Expand All @@ -59,39 +71,8 @@ where
_ => e.into(),
})?;

#[cfg(feature = "plugin")]
let (new_ids, mut new_plugin_ids) = identity_file.split_into();

#[cfg(not(feature = "plugin"))]
let new_ids = identity_file.into_identities();

identities.extend(
new_ids
.into_iter()
.map(|i| Box::new(i) as Box<dyn Identity>),
);

#[cfg(feature = "plugin")]
plugin_identities.append(&mut new_plugin_ids);
}

#[cfg(feature = "plugin")]
{
// Collect the names of the required plugins.
let mut plugin_names = plugin_identities
.iter()
.map(|r| r.plugin())
.collect::<Vec<_>>();
plugin_names.sort_unstable();
plugin_names.dedup();

// Find the required plugins.
for plugin_name in plugin_names {
identities.push(Box::new(crate::plugin::IdentityPluginV1::new(
plugin_name,
&plugin_identities,
UiCallbacks,
)?))
for entry in identity_file.into_identities() {
identities.push(entry.into_identity(UiCallbacks)?);
}
}

Expand Down Expand Up @@ -162,18 +143,11 @@ pub fn read_secret(
}

/// Implementation of age callbacks that makes requests to the user via the UI.
#[derive(Clone, Copy)]
pub struct UiCallbacks;

impl Callbacks for UiCallbacks {
fn prompt(&self, message: &str) {
if let Some(dialog) = MessageDialog::with_default_binary() {
// pinentry binary is available!
if dialog.show_message(message).is_ok() {
return;
}
}

// Fall back to CLI interface.
fn display_message(&self, message: &str) {
eprintln!("{}", message);
}

Expand Down
Loading

0 comments on commit c028790

Please sign in to comment.