Skip to content

Commit

Permalink
Add support for -R - and -i -
Browse files Browse the repository at this point in the history
Closes #177.
  • Loading branch information
str4d committed Jan 21, 2024
1 parent d3b2195 commit 8bd346d
Show file tree
Hide file tree
Showing 31 changed files with 289 additions and 24 deletions.
18 changes: 16 additions & 2 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ jobs:
- name: Write (very not private) age X25519 key
if: matrix.recipient == 'x25519'
run: echo "AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX" >key.txt
- name: Save the corresponding age x25519 recipient
if: matrix.recipient == 'x25519'
run: echo "age1y8m84r6pwd4da5d45zzk03rlgv2xr7fn9px80suw3psrahul44ashl0usm" >key.txt.pub
- name: Set the corresponding age x25519 recipient
if: matrix.recipient == 'x25519'
run: echo "AGE_PUBKEY=-r age1y8m84r6pwd4da5d45zzk03rlgv2xr7fn9px80suw3psrahul44ashl0usm" >> $GITHUB_ENV
Expand Down Expand Up @@ -202,16 +205,27 @@ jobs:
- name: Generate a file to encrypt
run: echo "Test 5" > test5.txt
- name: Encrypt to identity in a named pipe
run: ./${{ matrix.alice }} -e -i <(cat key.txt) -o test.age test5.txt
run: ./${{ matrix.alice }} -e -i <(cat key.txt) -o test5.age test5.txt
- name: Decrypt with identity in a named pipe
run: ./${{ matrix.bob }} -d -i <(cat key.txt) test.age | grep -q "^Test 5$"
run: ./${{ matrix.bob }} -d -i <(cat key.txt) test5.age | grep -q "^Test 5$"
- name: Store test5.age
uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ matrix.alice }}_${{ matrix.bob }}_${{ matrix.recipient }}_test5.age
path: test5.age

- name: Encrypt to recipient in standard input
run: cat key.txt.pub | ./${{ matrix.alice }} -e -R - -o test6.age test5.txt
- name: Decrypt with identity in standard input
run: cat key.txt | ./${{ matrix.bob }} -d -i - test6.age | grep -q "^Test 5$"
- name: Store test6.age
uses: actions/upload-artifact@v4
if: failure()
with:
name: ${{ matrix.alice }}_${{ matrix.bob }}_${{ matrix.recipient }}_test6.age
path: test6.age

- name: Keygen prevents overwriting an existing file
run: |
touch do_not_overwrite_key.txt
Expand Down
5 changes: 5 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ to 1.0.0 are beta releases.
- `file_io`:
- `FileReader`
- `impl Debug for {LazyFile, OutputFormat, OutputWriter, StdoutWriter}`
- `StdinGuard`
- `read_recipients`
- `age::identity::IdentityFile::from_input_reader` (behind `cli-common` feature
flag).
Expand All @@ -31,11 +32,15 @@ to 1.0.0 are beta releases.
`OutputWriter` will write to a file, this boolean enables the caller to
control whether the file will be overwritten if it exists (instead of the
implicit behaviour that was previously changed in 0.6.0).
- `read_identities` now takes an `&mut StdinGuard` argument, and `filenames`
may now contain at most one entry of `"-"`, which will be interpreted as
reading from standard input.
- `ReadError` has new variants:
- `EncryptedIdentities`
- `InvalidRecipient`
- `InvalidRecipientsFile`
- `MissingRecipientsFile`
- `MultipleStdin`
- `RsaModulusTooLarge`
- `age::ssh`:
- `ParseRecipientKeyError` has a new variant `RsaModulusTooLarge`.
Expand Down
2 changes: 2 additions & 0 deletions age/i18n/en-US/age.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ err-read-invalid-recipients-file =
err-read-missing-recipients-file = Recipients file not found: {$filename}
err-read-multiple-stdin = Standard input can't be used for multiple purposes.
err-read-rsa-modulus-too-large =
RSA Modulus Too Large
---------------------
Expand Down
28 changes: 28 additions & 0 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,34 @@ pub use recipients::read_recipients;

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

/// A guard that helps to ensure that standard input is only used once.
pub struct StdinGuard {
stdin_used: bool,
}

impl StdinGuard {
/// Constructs a new `StdinGuard`.
///
/// `input_is_stdin` should be set to `true` if standard input is being used for
/// plaintext input during encryption, or ciphertext input during decryption.
pub fn new(input_is_stdin: bool) -> Self {
Self {
stdin_used: input_is_stdin,
}
}

fn open(&mut self, filename: String) -> Result<file_io::InputReader, ReadError> {

Check warning on line 46 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L46

Added line #L46 was not covered by tests
let input = file_io::InputReader::new(Some(filename))?;
if matches!(input, file_io::InputReader::Stdin(_)) {
if self.stdin_used {
return Err(ReadError::MultipleStdin);

Check warning on line 50 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L49-L50

Added lines #L49 - L50 were not covered by tests
}
self.stdin_used = true;

Check warning on line 52 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L52

Added line #L52 was not covered by tests
}
Ok(input)
}
}

fn confirm(query: &str, ok: &str, cancel: Option<&str>) -> pinentry::Result<bool> {
if let Some(mut input) = ConfirmationDialog::with_default_binary() {
// pinentry binary is available!
Expand Down
3 changes: 3 additions & 0 deletions age/src/cli_common/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum ReadError {
},
/// The given recipients file could not be found.
MissingRecipientsFile(String),
/// Standard input was used by multiple files.
MultipleStdin,
/// A recipient is an `ssh-rsa`` public key with a modulus larger than we support.
#[cfg(feature = "ssh")]
#[cfg_attr(docsrs, doc(cfg(feature = "ssh")))]
Expand Down Expand Up @@ -90,6 +92,7 @@ impl fmt::Display for ReadError {
"err-read-missing-recipients-file",
filename = filename.as_str(),
),
ReadError::MultipleStdin => wfl!(f, "err-read-multiple-stdin"),

Check warning on line 95 in age/src/cli_common/error.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/error.rs#L95

Added line #L95 was not covered by tests
#[cfg(feature = "ssh")]
ReadError::RsaModulusTooLarge => {
wfl!(f, "err-read-rsa-modulus-too-large", max_size = 4096)
Expand Down
35 changes: 25 additions & 10 deletions age/src/cli_common/identities.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
use std::fs::File;
use std::io::{self, BufReader};

use super::{ReadError, UiCallbacks};
use super::{file_io::InputReader, ReadError, StdinGuard, UiCallbacks};
use crate::{identity::IdentityFile, Identity};

#[cfg(feature = "armor")]
use crate::armor::ArmoredReader;

/// Reads identities from the provided files.
///
/// `filenames` may contain at most one entry of `"-"`, which will be interpreted as
/// reading from standard input. An error will be returned if `stdin_guard` is guarding an
/// existing usage of standard input.
pub fn read_identities(
filenames: Vec<String>,
max_work_factor: Option<u8>,
stdin_guard: &mut StdinGuard,
) -> Result<Vec<Box<dyn Identity>>, ReadError> {
let mut identities: Vec<Box<dyn Identity>> = Vec::with_capacity(filenames.len());

parse_identity_files::<_, ReadError>(
filenames,
max_work_factor,
stdin_guard,
&mut identities,
|identities, identity| {
identities.push(Box::new(identity));
Expand Down Expand Up @@ -58,21 +63,24 @@ pub fn read_identities(
pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
filenames: Vec<String>,
max_work_factor: Option<u8>,
stdin_guard: &mut StdinGuard,
ctx: &mut Ctx,
#[cfg(feature = "armor")] encrypted_identity: impl Fn(
&mut Ctx,
crate::encrypted::Identity<ArmoredReader<BufReader<File>>, UiCallbacks>,
crate::encrypted::Identity<ArmoredReader<BufReader<InputReader>>, UiCallbacks>,
) -> Result<(), E>,
#[cfg(feature = "ssh")] ssh_identity: impl Fn(&mut Ctx, &str, crate::ssh::Identity) -> Result<(), E>,
identity_file_entry: impl Fn(&mut Ctx, crate::IdentityFileEntry) -> Result<(), E>,
) -> Result<(), E> {
for filename in filenames {
let mut reader = PeekableReader::new(BufReader::new(File::open(&filename).map_err(
|e| match e.kind() {
io::ErrorKind::NotFound => ReadError::IdentityNotFound(filename.clone()),
_ => e.into(),
},
)?));
let mut reader = PeekableReader::new(BufReader::new(

Check warning on line 76 in age/src/cli_common/identities.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/identities.rs#L76

Added line #L76 was not covered by tests
stdin_guard.open(filename.clone()).map_err(|e| match e {
ReadError::Io(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {
ReadError::IdentityNotFound(filename.clone())

Check warning on line 79 in age/src/cli_common/identities.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/identities.rs#L78-L79

Added lines #L78 - L79 were not covered by tests
}
_ => e,

Check warning on line 81 in age/src/cli_common/identities.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/identities.rs#L81

Added line #L81 was not covered by tests
})?,
));

#[cfg(feature = "armor")]
// Try parsing as an encrypted age identity.
Expand Down Expand Up @@ -188,7 +196,14 @@ impl<R: io::BufRead> io::BufRead for PeekableReader<R> {
fn fill_buf(&mut self) -> io::Result<&[u8]> {
match self.state {
PeekState::Peeking { consumed } => {
if consumed < self.inner.fill_buf()?.len() {
let inner_len = self.inner.fill_buf()?.len();
if inner_len == 0 {
// This state only occurs when the underlying data source is empty.
// Don't fall through to change the state to `Reading`, because we can
// always reset an empty stream.
assert_eq!(consumed, 0);
Ok(&[])

Check warning on line 205 in age/src/cli_common/identities.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/identities.rs#L204-L205

Added lines #L204 - L205 were not covered by tests
} else if consumed < inner_len {
// Re-call so we aren't extending the lifetime of the mutable borrow
// on `self.inner` to outside the conditional, which would prevent us
// from performing other mutable operations on the other side.
Expand Down
16 changes: 12 additions & 4 deletions age/src/cli_common/recipients.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::fs::File;
use std::io::{self, BufReader};

use super::StdinGuard;
use super::{identities::parse_identity_files, ReadError, UiCallbacks};
use crate::{x25519, EncryptError, IdentityFileEntry, Recipient};

Expand Down Expand Up @@ -101,11 +101,16 @@ fn read_recipients_list<R: io::BufRead>(
}

/// Reads recipients from the provided arguments.
///
/// `recipients_file_strings` and `identity_strings` may collectively contain at most one
/// entry of `"-"`, which will be interpreted as reading from standard input. An error
/// will be returned if `stdin_guard` is guarding an existing usage of standard input.
pub fn read_recipients(
recipient_strings: Vec<String>,
recipients_file_strings: Vec<String>,
identity_strings: Vec<String>,
max_work_factor: Option<u8>,
stdin_guard: &mut StdinGuard,
) -> Result<Vec<Box<dyn Recipient + Send>>, ReadError> {
let mut recipients: Vec<Box<dyn Recipient + Send>> = vec![];
let mut plugin_recipients: Vec<plugin::Recipient> = vec![];
Expand All @@ -116,9 +121,11 @@ pub fn read_recipients(
}

for arg in recipients_file_strings {
let f = File::open(&arg).map_err(|e| match e.kind() {
io::ErrorKind::NotFound => ReadError::MissingRecipientsFile(arg.clone()),
_ => e.into(),
let f = stdin_guard.open(arg.clone()).map_err(|e| match e {
ReadError::Io(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {
ReadError::MissingRecipientsFile(arg.clone())

Check warning on line 126 in age/src/cli_common/recipients.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/recipients.rs#L124-L126

Added lines #L124 - L126 were not covered by tests
}
_ => e,

Check warning on line 128 in age/src/cli_common/recipients.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common/recipients.rs#L128

Added line #L128 was not covered by tests
})?;
let buf = BufReader::new(f);
read_recipients_list(&arg, buf, &mut recipients, &mut plugin_recipients)?;
Expand All @@ -127,6 +134,7 @@ pub fn read_recipients(
parse_identity_files::<_, ReadError>(
identity_strings,
max_work_factor,
stdin_guard,
&mut (&mut recipients, &mut plugin_identities),
|(recipients, _), identity| {
recipients.extend(identity.recipients().map_err(|e| {
Expand Down
3 changes: 3 additions & 0 deletions age/tests/test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use std::io::Read;
#[test]
#[cfg(feature = "cli-common")]
fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
use age::cli_common::StdinGuard;

for test_vector in fs::read_dir("./tests/testdata")?.filter(|res| {
res.as_ref()
.map(|e| {
Expand All @@ -29,6 +31,7 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
name
)],
None,
&mut StdinGuard::new(false),
)?;
d.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
}
Expand Down
7 changes: 7 additions & 0 deletions rage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ to 1.0.0 are beta releases.
### Changed
- MSRV is now 1.65.0.
- Migrated from `gumdrop` to `clap` for argument parsing.
- `-R/--recipients-file` and `-i/--identity` now support "read-once" files, like
those used by process substitution (`-i <(other_binary get-age-identity)`) and
named pipes.
- The filename `-` (hyphen) is now treated as an explicit request to read from
standard input when used with `-R/--recipients-file` or `-i/--identity`. It
must only occur once across the `-R/--recipients-file` and `-i/--identity`
flags, and the input file. It cannot be used if the input file is omitted.

### Fixed
- OpenSSH private keys passed to `-i/--identity` that contain invalid public
Expand Down
1 change: 1 addition & 0 deletions rage/i18n/en-US/rage.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ rec-dec-armor-flag = Note that armored files are detected automatically.
err-dec-missing-identities = Missing identities.
rec-dec-missing-identities = Did you forget to specify {-flag-identity}?
rec-dec-missing-identities-stdin = Did you forget to provide the identity over standard input?
err-dec-mixed-identity-passphrase = {-flag-identity} can't be used with passphrase-encrypted files.
Expand Down
7 changes: 5 additions & 2 deletions rage/src/bin/rage-mount/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use age::{
armor::ArmoredReader,
cli_common::{read_identities, read_secret},
cli_common::{read_identities, read_secret, StdinGuard},
stream::StreamReader,
};
use clap::{CommandFactory, Parser};
Expand Down Expand Up @@ -207,6 +207,8 @@ fn main() -> Result<(), Error> {
let types = opts.types;
let mountpoint = opts.mountpoint;

let mut stdin_guard = StdinGuard::new(false);

match age::Decryptor::new_buffered(ArmoredReader::new(file))? {
age::Decryptor::Passphrase(decryptor) => {
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
Expand All @@ -218,7 +220,8 @@ fn main() -> Result<(), Error> {
}
}
age::Decryptor::Recipients(decryptor) => {
let identities = read_identities(opts.identity, opts.max_work_factor)?;
let identities =
read_identities(opts.identity, opts.max_work_factor, &mut stdin_guard)?;

if identities.is_empty() {
return Err(Error::MissingIdentities);
Expand Down
12 changes: 9 additions & 3 deletions rage/src/bin/rage/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@ pub(crate) enum DecryptError {
ArmorFlag,
IdentityRead(age::cli_common::ReadError),
Io(io::Error),
MissingIdentities,
MissingIdentities {
stdin_identity: bool,
},
MixedIdentityAndPassphrase,
MixedIdentityAndPluginName,
PassphraseFlag,
Expand Down Expand Up @@ -156,9 +158,13 @@ impl fmt::Display for DecryptError {
}
DecryptError::IdentityRead(e) => write!(f, "{}", e),
DecryptError::Io(e) => write!(f, "{}", e),
DecryptError::MissingIdentities => {
DecryptError::MissingIdentities { stdin_identity } => {
wlnfl!(f, "err-dec-missing-identities")?;
wfl!(f, "rec-dec-missing-identities")
if *stdin_identity {
wfl!(f, "rec-dec-missing-identities-stdin")
} else {
wfl!(f, "rec-dec-missing-identities")
}
}
DecryptError::MixedIdentityAndPassphrase => {
wfl!(f, "err-dec-mixed-identity-passphrase")
Expand Down
Loading

0 comments on commit 8bd346d

Please sign in to comment.