Skip to content

Commit

Permalink
Merge pull request #445 from str4d/356-keygen-convert
Browse files Browse the repository at this point in the history
Add `rage-keygen -y` mode to convert identity file to recipients
  • Loading branch information
str4d authored Jan 15, 2024
2 parents 405304d + 708b5e1 commit 92efeef
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 38 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ jobs:
true
fi
- name: Keygen supports conversion from stdin
run: ./${{ matrix.alice }}-keygen | ./${{ matrix.bob }}-keygen -y

- name: Keygen supports conversion from file
if: matrix.recipient == 'x25519'
run: ./${{ matrix.alice }}-keygen -y key.txt

- name: Update FiloSottile/age status with result
if: always() && github.event.action == 'age-interop-request'
run: |
Expand Down
14 changes: 10 additions & 4 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ to 1.0.0 are beta releases.
## [Unreleased]
### Added
- `age::cli_common::file_io`:
- `FileReader`
- `impl Debug for {LazyFile, OutputFormat, OutputWriter, StdoutWriter}`
- `age::identity::IdentityFile::from_input_reader` (behind `cli-common` feature
flag).
- `impl Eq for age::ssh::{ParseRecipientKeyError, UnsupportedKey}`
- `impl {Debug, PartialEq, Eq, Hash} for age::x25519::Recipient`

### Changed
- MSRV is now 1.65.0.
- Migrated to `base64 0.21`, `rsa 0.9`.
- `age::cli_common::file_io::OutputWriter::new` now takes an `allow_overwrite`
boolean argument. If `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).
- `age::cli_common::file_io`:
- `InputReader::File` enum variant now contains `FileReader` instead of
`std::fs::File`.
- `OutputWriter::new` now takes an `allow_overwrite` boolean argument. If
`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).
- `age::ssh`:
- `ParseRecipientKeyError` has a new variant `RsaModulusTooLarge`.
- The following trait implementations now return
Expand Down
23 changes: 20 additions & 3 deletions age/src/cli_common/file_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,16 @@ impl fmt::Display for DenyOverwriteFileError {

impl std::error::Error for DenyOverwriteFileError {}

/// Wrapper around a [`File`].
pub struct FileReader {
inner: File,
filename: String,
}

/// Wrapper around either a file or standard input.
pub enum InputReader {
/// Wrapper around a file.
File(File),
File(FileReader),
/// Wrapper around standard input.
Stdin(io::Stdin),
}
Expand All @@ -65,7 +71,10 @@ impl InputReader {
// Respect the Unix convention that "-" as an input filename
// parameter is an explicit request to use standard input.
if filename != "-" {
return Ok(InputReader::File(File::open(filename)?));
return Ok(InputReader::File(FileReader {
inner: File::open(&filename)?,
filename,
}));
}
}

Expand All @@ -76,12 +85,20 @@ impl InputReader {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Stdin(_)) && io::stdin().is_terminal()
}

pub(crate) fn filename(&self) -> Option<&str> {
if let Self::File(f) = self {
Some(&f.filename)
} else {
None
}
}
}

impl Read for InputReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
InputReader::File(f) => f.read(buf),
InputReader::File(f) => f.inner.read(buf),
InputReader::Stdin(handle) => handle.read(buf),
}
}
Expand Down
10 changes: 10 additions & 0 deletions age/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::io;

use crate::{x25519, Callbacks, DecryptError, EncryptError};

#[cfg(feature = "cli-common")]
use crate::cli_common::file_io::InputReader;

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

Expand Down Expand Up @@ -70,6 +73,13 @@ impl IdentityFile {
Self::parse_identities(None, data)
}

/// Parses one or more identities from an [`InputReader`];
#[cfg(feature = "cli-common")]
pub fn from_input_reader(reader: InputReader) -> io::Result<Self> {
let filename = reader.filename().map(String::from);
Self::parse_identities(filename, io::BufReader::new(reader))
}

fn parse_identities<R: io::BufRead>(filename: Option<String>, data: R) -> io::Result<Self> {
let mut identities = vec![];

Expand Down
3 changes: 3 additions & 0 deletions rage/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to Rust's notion of
to 1.0.0 are beta releases.

## [Unreleased]
### Added
- `rage-keygen -y IDENTITY_FILE` to convert identity files to recipients.

### Changed
- MSRV is now 1.65.0.
- Migrated from `gumdrop` to `clap` for argument parsing.
Expand Down
50 changes: 36 additions & 14 deletions rage/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ macro_rules! fl {
struct Example {
text: String,
cmd: &'static str,
output: Option<String>,
output: Vec<String>,
}

impl Example {
const fn new(text: String, cmd: &'static str, output: Option<String>) -> Self {
const fn new(text: String, cmd: &'static str, output: Vec<String>) -> Self {
Self { text, cmd, output }
}
}
Expand Down Expand Up @@ -139,38 +139,38 @@ impl Cli {
Example::new(
fl!("man-rage-example-enc-single"),
"echo \"_o/\" | rage -o hello.age -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u",
None,
vec![],
),
Example::new(
fl!("man-rage-example-enc-multiple"),
"echo \"_o/\" | rage -r age1uvscypafkkxt6u2gkguxet62cenfmnpc0smzzlyun0lzszfatawq4kvf2u \
-r age1ex4ty8ppg02555at009uwu5vlk5686k3f23e7mac9z093uvzfp8sxr5jum > hello.age",
None,
vec![],
),
Example::new(
fl!("man-rage-example-enc-password"),
"rage -p -o hello.txt.age hello.txt",
Some(format!("{}:", fl!("type-passphrase"))),
vec![format!("{}:", fl!("type-passphrase"))],
),
Example::new(
fl!("man-rage-example-enc-list"),
"tar cv ~/xxx | rage -R recipients.txt > xxx.tar.age",
None,
vec![],
),
Example::new(
fl!("man-rage-example-enc-identities"),
"tar cv ~/xxx | rage -e -i keyA.txt -i keyB.txt > xxx.tar.age",
None,
vec![],
),
Example::new(
fl!("man-rage-example-enc-url"),
"echo \"_o/\" | rage -o hello.age -R <(curl https://github.com/str4d.keys)",
None,
vec![],
),
Example::new(
fl!("man-rage-example-dec-identities"),
"rage -d -o hello -i keyA.txt -i keyB.txt hello.age",
None,
vec![],
),
])
.render(w)
Expand All @@ -182,14 +182,36 @@ impl Cli {
self.rage_keygen.about(fl!("man-keygen-about")),
|_, w| {
Examples([
Example::new(fl!("man-keygen-example-stdout"), "rage-keygen", None),
Example::new(
fl!("man-keygen-example-stdout"),
"rage-keygen",
vec![
format!(
"# {}: 2021-01-02T15:30:45+01:00",
fl!("identity-file-created"),
),
format!(
"# {}: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z",
fl!("identity-file-pubkey"),
),
"AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9".to_owned(),
],
),
Example::new(
fl!("man-keygen-example-file"),
"rage-keygen -o key.txt",
Some(format!(
vec![format!(
"{}: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p",
fl!("tty-pubkey")
)),
)],
),
Example::new(
fl!("man-keygen-example-convert"),
"rage-keygen -y key.txt",
vec![
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
.to_owned(),
],
),
])
.render(w)
Expand All @@ -204,12 +226,12 @@ impl Cli {
Example::new(
fl!("man-mount-example-identity"),
"rage-mount -t tar -i key.txt encrypted.tar.age ./tmp",
None,
vec![],
),
Example::new(
fl!("man-mount-example-passphrase"),
"rage-mount -t zip encrypted.zip.age ./tmp",
Some(format!("{}:", fl!("type-passphrase"))),
vec![format!("{}:", fl!("type-passphrase"))],
),
])
.render(w)
Expand Down
12 changes: 12 additions & 0 deletions rage/i18n/en-US/rage.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ rage-after-help-example =
{" "}{$example_c}
keygen-help-flag-output = {help-flag-output} Defaults to standard output.
keygen-help-flag-convert = Convert an identity file to a recipients file.
## Formatting

Expand All @@ -100,7 +101,9 @@ warn-double-encrypting = Encrypting an already-encrypted file
## General errors

err-failed-to-open-input = Failed to open input: {$err}
err-failed-to-open-output = Failed to open output: {$err}
err-failed-to-read-input = Failed to read from input: {$err}
err-failed-to-write-output = Failed to write to output: {$err}
err-identity-ambiguous = {-flag-identity} requires either {-flag-encrypt} or {-flag-decrypt}.
err-mixed-encrypt-decrypt = {-flag-encrypt} can't be used with {-flag-decrypt}.
Expand All @@ -112,6 +115,14 @@ err-ux-B = Tell us
# Put (len(A) - len(B) - 32) spaces here.
err-ux-C = {" "}
## Keygen errors

err-identity-file-contains-plugin = Identity file '{$filename}' contains identities for '{-age-plugin-}{$plugin_name}'.
rec-identity-file-contains-plugin = Try using '{-age-plugin-}{$plugin_name}' to convert this identity to a recipient.
err-no-identities-in-file = No identities found in file '{$filename}'.
err-no-identities-in-stdin = No identities found in standard input.
## Encryption errors

err-enc-broken-stdout = Could not write to stdout: {$err}
Expand Down Expand Up @@ -215,6 +226,7 @@ man-keygen-about = Generate age-compatible encryption key pairs
man-keygen-example-stdout = Generate a new key pair
man-keygen-example-file = Generate a new key pair and save it to a file
man-keygen-example-convert = Convert an identity file to a recipient
man-mount-about = Mount an age-encrypted filesystem
Expand Down
9 changes: 9 additions & 0 deletions rage/src/bin/rage-keygen/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ use crate::fl;
#[command(disable_help_flag(true))]
#[command(disable_version_flag(true))]
pub(crate) struct AgeOptions {
#[arg(help_heading = fl!("args-header"))]
#[arg(value_name = fl!("input"))]
#[arg(help = fl!("help-arg-input"))]
pub(crate) input: Option<String>,

#[arg(action = ArgAction::Help, short, long)]
#[arg(help = fl!("help-flag-help"))]
pub(crate) help: Option<bool>,
Expand All @@ -31,4 +36,8 @@ pub(crate) struct AgeOptions {
#[arg(value_name = fl!("output"))]
#[arg(help = fl!("keygen-help-flag-output"))]
pub(crate) output: Option<String>,

#[arg(short = 'y')]
#[arg(help = fl!("keygen-help-flag-convert"))]
pub(crate) convert: bool,
}
37 changes: 37 additions & 0 deletions rage/src/bin/rage-keygen/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,58 @@ macro_rules! wlnfl {
}

pub(crate) enum Error {
FailedToOpenInput(io::Error),
FailedToOpenOutput(io::Error),
FailedToReadInput(io::Error),
FailedToWriteOutput(io::Error),
IdentityFileContainsPlugin {
filename: Option<String>,
plugin_name: String,
},
NoIdentities {
filename: Option<String>,
},
}

// Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug`
// manually to provide the error output we want.
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::FailedToOpenInput(e) => {
wlnfl!(f, "err-failed-to-open-input", err = e.to_string())?
}
Error::FailedToOpenOutput(e) => {
wlnfl!(f, "err-failed-to-open-output", err = e.to_string())?
}
Error::FailedToReadInput(e) => {
wlnfl!(f, "err-failed-to-read-input", err = e.to_string())?
}
Error::FailedToWriteOutput(e) => {
wlnfl!(f, "err-failed-to-write-output", err = e.to_string())?
}
Error::IdentityFileContainsPlugin {
filename,
plugin_name,
} => {
wlnfl!(
f,
"err-identity-file-contains-plugin",
filename = filename.as_deref().unwrap_or_default(),
plugin_name = plugin_name.as_str(),
)?;
wlnfl!(
f,
"rec-identity-file-contains-plugin",
plugin_name = plugin_name.as_str(),
)?
}
Error::NoIdentities { filename } => match filename {
Some(filename) => {
wlnfl!(f, "err-no-identities-in-file", filename = filename.as_str())?
}
None => wlnfl!(f, "err-no-identities-in-stdin")?,
},
}
writeln!(f)?;
writeln!(f, "[ {} ]", crate::fl!("err-ux-A"))?;
Expand Down
Loading

0 comments on commit 92efeef

Please sign in to comment.