Skip to content

Commit c028790

Browse files
authored
Merge pull request #248 from str4d/late-night-twitch-stream
Several new features and UX changes
2 parents b189384 + 33956aa commit c028790

File tree

20 files changed

+667
-100
lines changed

20 files changed

+667
-100
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ PATH is a path to a file containing age recipients, one per line
4848
4949
IDENTITY is a path to a file with age identities, one per line
5050
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
51+
Passphrase-encrypted age identity files can be used as identity files.
5152
Multiple identities may be provided, and any unused ones will be ignored.
5253
```
5354

@@ -89,6 +90,26 @@ $ rage -d example.png.age >example.png
8990
Type passphrase: [hidden]
9091
```
9192

93+
### Passphrase-protected identity files
94+
95+
If an identity file passed to `-i/--identity` is a passphrase-encrypted age
96+
file, it will be automatically decrypted.
97+
98+
```
99+
$ rage -p -o key.age <(rage-keygen)
100+
Public key: age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz
101+
Type passphrase (leave empty to autogenerate a secure one): [hidden]
102+
Using an autogenerated passphrase:
103+
flash-bean-celery-network-curious-flower-salt-amateur-fence-giant
104+
$ rage -r age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz secrets.txt > secrets.txt.age
105+
$ rage -d -i key.age secrets.txt.age > secrets.txt
106+
Type passphrase: [hidden]
107+
```
108+
109+
Passphrase-protected identity files are not necessary for most use cases, where
110+
access to the encrypted identity file implies access to the whole system.
111+
However, they can be useful if the identity file is stored remotely.
112+
92113
### SSH keys
93114

94115
As a convenience feature, rage also supports encrypting to `ssh-rsa` and

age/CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,34 @@ and this project adheres to Rust's notion of
99
to 1.0.0 are beta releases.
1010

1111
## [Unreleased]
12+
### Added
13+
- `age::encrypted::Identity`, for decrypting files with passphrase-encrypted
14+
age identity files.
15+
- `age::IdentityFileEntry` enum, representing the possible kinds of entries
16+
within an age identity file.
17+
- `age::{DecryptError, EncryptError, PluginError}: Clone` bounds.
18+
- `age::cli_common::UiCallbacks: Clone + Copy` bounds.
19+
1220
### Changed
1321
- MSRV is now 1.51.0.
22+
- `age::IdentityFile::into_identities` now returns `Vec<IdentityFileEntry>`.
23+
- `age::cli_common::read_identities`:
24+
- Encrypted age files will now be parsed and assumed to be encrypted age
25+
identities. This assumption is checked at file-decryption time.
26+
- New `max_work_factor` parameter for controlling the work factor when
27+
decrypting encrypted identities.
28+
- New `identity_encrypted_without_passphrase` parameter for customising the
29+
error when an invalid encrypted identity is found.
30+
- Identities are now returned in the same order as `filenames` (and
31+
top-to-bottom from within each file). Plugin identities are no longer
32+
coalesced; there is one `Box<dyn Identity>` per plugin identity.
33+
- `age::Callbacks::prompt` has been renamed to `Callbacks::display_message`.
34+
- `age::cli_common::UiCallbacks::display_message` no longer uses `pinentry`
35+
(which displays a temporary prompt that can be dismissed), so the message is
36+
now part of the visible CLI output.
37+
38+
### Removed
39+
- `IdentityFile::split_into` (replaced by `IdentityFileEntry::Plugin`).
1440

1541
## [0.6.0] - 2021-05-02
1642
### Security

age/i18n/en-US/age.ftl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ err-plugin-identity = '{$plugin_name}' couldn't use an identity: {$message}
5757
err-plugin-recipient = '{$plugin_name}' couldn't use recipient {$recipient}: {$message}
5858
err-plugin-multiple = Plugin returned multiple errors:
5959
60+
## Encrypted identities
61+
62+
encrypted-passphrase-prompt = Type passphrase for encrypted identity '{$filename}'
63+
64+
encrypted-warn-no-match = Warning: encrypted identity file '{$filename}' didn't match file's recipients
65+
6066
## SSH identities
6167

6268
ssh-passphrase-prompt = Type passphrase for OpenSSH key '{$filename}'

age/src/cli_common.rs

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Common helpers for CLI binaries.
22
3-
use pinentry::{MessageDialog, PassphraseInput};
3+
use pinentry::PassphraseInput;
44
use rand::{
55
distributions::{Distribution, Uniform},
66
rngs::OsRng,
@@ -11,33 +11,45 @@ use std::fs::File;
1111
use std::io::{self, BufReader};
1212
use subtle::ConstantTimeEq;
1313

14-
use crate::{fl, identity::IdentityFile, Callbacks, Identity};
15-
16-
#[cfg(feature = "plugin")]
17-
use crate::plugin;
14+
use crate::{armor::ArmoredReader, fl, identity::IdentityFile, Callbacks, Identity};
1815

1916
pub mod file_io;
2017

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

2320
/// Reads identities from the provided files if given, or the default system
2421
/// locations if no files are given.
25-
pub fn read_identities<E, G>(
22+
pub fn read_identities<E, G, H>(
2623
filenames: Vec<String>,
24+
max_work_factor: Option<u8>,
2725
file_not_found: G,
26+
identity_encrypted_without_passphrase: H,
2827
#[cfg(feature = "ssh")] unsupported_ssh: impl Fn(String, crate::ssh::UnsupportedKey) -> E,
2928
) -> Result<Vec<Box<dyn Identity>>, E>
3029
where
3130
E: From<crate::DecryptError>,
3231
E: From<io::Error>,
3332
G: Fn(String) -> E,
33+
H: Fn(String) -> E,
3434
{
3535
let mut identities: Vec<Box<dyn Identity>> = vec![];
3636

37-
#[cfg(feature = "plugin")]
38-
let mut plugin_identities: Vec<plugin::Identity> = vec![];
39-
4037
for filename in filenames {
38+
// Try parsing as an encrypted age identity.
39+
if let Ok(identity) = crate::encrypted::Identity::from_buffer(
40+
ArmoredReader::new(BufReader::new(File::open(&filename)?)),
41+
Some(filename.clone()),
42+
UiCallbacks,
43+
max_work_factor,
44+
) {
45+
if let Some(identity) = identity {
46+
identities.push(Box::new(identity));
47+
continue;
48+
} else {
49+
return Err(identity_encrypted_without_passphrase(filename));
50+
}
51+
}
52+
4153
// Try parsing as a single multi-line SSH identity.
4254
#[cfg(feature = "ssh")]
4355
match crate::ssh::Identity::from_buffer(
@@ -59,39 +71,8 @@ where
5971
_ => e.into(),
6072
})?;
6173

62-
#[cfg(feature = "plugin")]
63-
let (new_ids, mut new_plugin_ids) = identity_file.split_into();
64-
65-
#[cfg(not(feature = "plugin"))]
66-
let new_ids = identity_file.into_identities();
67-
68-
identities.extend(
69-
new_ids
70-
.into_iter()
71-
.map(|i| Box::new(i) as Box<dyn Identity>),
72-
);
73-
74-
#[cfg(feature = "plugin")]
75-
plugin_identities.append(&mut new_plugin_ids);
76-
}
77-
78-
#[cfg(feature = "plugin")]
79-
{
80-
// Collect the names of the required plugins.
81-
let mut plugin_names = plugin_identities
82-
.iter()
83-
.map(|r| r.plugin())
84-
.collect::<Vec<_>>();
85-
plugin_names.sort_unstable();
86-
plugin_names.dedup();
87-
88-
// Find the required plugins.
89-
for plugin_name in plugin_names {
90-
identities.push(Box::new(crate::plugin::IdentityPluginV1::new(
91-
plugin_name,
92-
&plugin_identities,
93-
UiCallbacks,
94-
)?))
74+
for entry in identity_file.into_identities() {
75+
identities.push(entry.into_identity(UiCallbacks)?);
9576
}
9677
}
9778

@@ -162,18 +143,11 @@ pub fn read_secret(
162143
}
163144

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

167149
impl Callbacks for UiCallbacks {
168-
fn prompt(&self, message: &str) {
169-
if let Some(dialog) = MessageDialog::with_default_binary() {
170-
// pinentry binary is available!
171-
if dialog.show_message(message).is_ok() {
172-
return;
173-
}
174-
}
175-
176-
// Fall back to CLI interface.
150+
fn display_message(&self, message: &str) {
177151
eprintln!("{}", message);
178152
}
179153

0 commit comments

Comments
 (0)