Skip to content

Commit

Permalink
rage: Add rage-keygen -y mode to convert identity file to recipients
Browse files Browse the repository at this point in the history
Matches `age-keygen -y` semantics.

Closes #356.
  • Loading branch information
str4d committed Jan 15, 2024
1 parent ad5e153 commit 5b78ccd
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 17 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
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
8 changes: 8 additions & 0 deletions rage/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,14 @@ impl Cli {
fl!("tty-pubkey")
)],
),
Example::new(
fl!("man-keygen-example-convert"),
"rage-keygen -y key.txt",
vec![
"age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
.to_owned(),
],
),
])
.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
71 changes: 54 additions & 17 deletions rage/src/bin/rage-keygen/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use age::{cli_common::file_io, secrecy::ExposeSecret};
use clap::Parser;

use std::io::Write;
use std::io::{self, Write};

mod cli;
mod error;
Expand Down Expand Up @@ -35,7 +35,7 @@ fn main() -> Result<(), error::Error> {

let opts = cli::AgeOptions::parse();

let mut output = file_io::OutputWriter::new(
let output = file_io::OutputWriter::new(
opts.output,
false,
file_io::OutputFormat::Text,
Expand All @@ -44,24 +44,61 @@ fn main() -> Result<(), error::Error> {
)
.map_err(error::Error::FailedToOpenOutput)?;

if opts.convert {
convert(opts.input, output)
} else {
generate(output).map_err(error::Error::FailedToWriteOutput)
}
}

fn generate(mut output: file_io::OutputWriter) -> io::Result<()> {
let sk = age::x25519::Identity::generate();
let pk = sk.to_public();

(|| {
writeln!(
output,
"# {}: {}",
fl!("identity-file-created"),
chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
)?;
writeln!(output, "# {}: {}", fl!("identity-file-pubkey"), pk)?;
writeln!(output, "{}", sk.to_string().expose_secret())?;

if !output.is_terminal() {
eprintln!("{}: {}", fl!("tty-pubkey"), pk);
writeln!(
output,
"# {}: {}",
fl!("identity-file-created"),
chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
)?;
writeln!(output, "# {}: {}", fl!("identity-file-pubkey"), pk)?;
writeln!(output, "{}", sk.to_string().expose_secret())?;

if !output.is_terminal() {
eprintln!("{}: {}", fl!("tty-pubkey"), pk);
}

Ok(())
}

fn convert(
filename: Option<String>,
mut output: file_io::OutputWriter,
) -> Result<(), error::Error> {
let file = age::IdentityFile::from_input_reader(
file_io::InputReader::new(filename.clone()).map_err(error::Error::FailedToOpenInput)?,
)
.map_err(error::Error::FailedToReadInput)?;

let identities = file.into_identities();
if identities.is_empty() {
return Err(error::Error::NoIdentities { filename });
}

for identity in identities {
match identity {
age::IdentityFileEntry::Native(sk) => {
writeln!(output, "{}", sk.to_public().to_string())

Check failure on line 91 in rage/src/bin/rage-keygen/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (MSRV)

`to_string` applied to a type that implements `Display` in `writeln!` args

error: `to_string` applied to a type that implements `Display` in `writeln!` args --> rage/src/bin/rage-keygen/main.rs:91:54 | 91 | writeln!(output, "{}", sk.to_public().to_string()) | ^^^^^^^^^^^^ help: remove this | = note: `-D clippy::to-string-in-format-args` implied by `-D warnings` = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args

Check failure on line 91 in rage/src/bin/rage-keygen/main.rs

View workflow job for this annotation

GitHub Actions / Clippy (MSRV)

`to_string` applied to a type that implements `Display` in `writeln!` args

error: `to_string` applied to a type that implements `Display` in `writeln!` args --> rage/src/bin/rage-keygen/main.rs:91:54 | 91 | writeln!(output, "{}", sk.to_public().to_string()) | ^^^^^^^^^^^^ help: remove this | = note: `-D clippy::to-string-in-format-args` implied by `-D warnings` = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#to_string_in_format_args
.map_err(error::Error::FailedToWriteOutput)?
}
age::IdentityFileEntry::Plugin(id) => {
return Err(error::Error::IdentityFileContainsPlugin {
filename,
plugin_name: id.plugin().to_string(),
});
}
}
}

Ok(())
})()
.map_err(error::Error::FailedToWriteOutput)
Ok(())
}

0 comments on commit 5b78ccd

Please sign in to comment.