diff --git a/docs/ragenix.1 b/docs/ragenix.1
index 78ab95e..58e6f38 100644
--- a/docs/ragenix.1
+++ b/docs/ragenix.1
@@ -1,6 +1,6 @@
-.\" generated with Ronn-NG/v0.9.1
-.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
-.TH "RAGENIX" "1" "January 2022" ""
+.\" generated with Ronn-NG/v0.10.1
+.\" http://github.com/apjanke/ronn-ng/tree/0.10.1
+.TH "RAGENIX" "1" "January 1980" ""
.SH "NAME"
\fBragenix\fR \- age\-encrypted secrets for Nix
.SH "SYNOPSIS"
@@ -30,8 +30,8 @@ Use the given \fIPROGRAM\fR to open the decrypted file for editing\. Defaults to
.IP
Giving the special token \fB\-\fR as a \fIPROGRAM\fR causes \fBragenix\fR to read from standard input\. In this case, \fBragenix\fR stream\-encrypts data from standard input only and does not open the file for editing\.
.TP
-\fB\-r\fR, \fB\-\-rekey\fR
-Decrypt all secrets given in the rules configuration file and encrypt them with the defined public keys\. If a secret file does not exist yet, it is ignored\. This option is useful to grant a new recipient access to one or multiple secrets\.
+\fB\-r\fR, \fB\-\-rekey\fR [PATH]
+Decrypt secrets given in the rules configuration file and encrypt them with the defined public keys\. If no paths are given, rekey all secrets\. If no paths are given and a secret file does not exist yet, it is ignored\. This option is useful to grant a new recipient access to one or multiple secrets\.
.IP
If the \fB\-\-identity\fR option is not given, \fBragenix\fR tries to decrypt \fIPATH\fR with the default SSH private keys\. See \fB\-\-identity\fR for details\.
.IP
diff --git a/docs/ragenix.1.html b/docs/ragenix.1.html
index 5959865..48f7bbf 100644
--- a/docs/ragenix.1.html
+++ b/docs/ragenix.1.html
@@ -127,10 +127,10 @@
OPTIONS
input only and does not open the file for editing.
--r
, --rekey
-
- Decrypt all secrets given in the rules configuration file and encrypt them
- with the defined public keys. If a secret file does not exist yet, it is
+-r
, --rekey
[PATH]
+ Decrypt secrets given in the rules configuration file and encrypt them
+ with the defined public keys. If no paths are given, rekey all secrets.
+ If no paths are given and a secret file does not exist yet, it is
ignored. This option is useful to grant a new recipient access to one or
multiple secrets.
@@ -268,7 +268,7 @@ AUTHORS
diff --git a/docs/ragenix.1.ronn b/docs/ragenix.1.ronn
index 9d77c9d..22b7957 100644
--- a/docs/ragenix.1.ronn
+++ b/docs/ragenix.1.ronn
@@ -45,9 +45,10 @@ store.
standard input. In this case, `ragenix` stream-encrypts data from standard
input only and does not open the file for editing.
-* `-r`, `--rekey`:
- Decrypt all secrets given in the rules configuration file and encrypt them
- with the defined public keys. If a secret file does not exist yet, it is
+* `-r`, `--rekey` [PATH]:
+ Decrypt secrets given in the rules configuration file and encrypt them
+ with the defined public keys. If no paths are given, rekey all secrets.
+ If no paths are given and a secret file does not exist yet, it is
ignored. This option is useful to grant a new recipient access to one or
multiple secrets.
diff --git a/src/cli.rs b/src/cli.rs
index 252c1b7..c50d0e9 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -12,7 +12,7 @@ pub(crate) struct Opts {
pub edit: Option,
pub editor: Option,
pub identities: Option>,
- pub rekey: bool,
+ pub rekey: Option>,
pub rules: String,
pub schema: bool,
pub verbose: bool,
@@ -35,10 +35,12 @@ fn build() -> Command {
)
.arg(
Arg::new("rekey")
- .help("re-encrypts all secrets with specified recipients")
+ .help("re-encrypts secrets with specified recipients")
.long("rekey")
.short('r')
- .action(ArgAction::SetTrue),
+ .num_args(0..)
+ .value_name("FILE")
+ .value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("identity")
@@ -107,7 +109,9 @@ where
identities: matches
.get_many::("identity")
.map(|vals| vals.cloned().collect::>()),
- rekey: matches.get_flag("rekey"),
+ rekey: matches
+ .get_many::("rekey")
+ .map(|vals| vals.cloned().collect::>()),
rules: matches
.get_one::("rules")
.cloned()
diff --git a/src/main.rs b/src/main.rs
index cbad641..1a00d73 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,5 @@
use color_eyre::eyre::{eyre, Result};
-use std::{env, fs, path::Path, process};
+use std::{env, path::PathBuf, process};
mod age;
mod cli;
@@ -29,10 +29,7 @@ fn main() -> Result<()> {
let identities = opts.identities.unwrap_or_default();
if let Some(path) = &opts.edit {
- let path_normalized = util::normalize_path(Path::new(path));
- let edit_path = std::env::current_dir()
- .and_then(fs::canonicalize)
- .map(|p| p.join(path_normalized))?;
+ let edit_path = util::canonicalize_rule_path(path)?;
let rule = rules
.into_iter()
.find(|x| x.path == edit_path)
@@ -41,8 +38,22 @@ fn main() -> Result<()> {
// `EDITOR`/`--editor` is mandatory if action is `--edit`
let editor = &opts.editor.unwrap();
ragenix::edit(&rule, &identities, editor, &mut std::io::stdout())?;
- } else if opts.rekey {
- ragenix::rekey(&rules, &identities, &mut std::io::stdout())?;
+ } else if let Some(paths) = opts.rekey {
+ if paths.is_empty() {
+ // Option passed but no files specified - rekey all
+ ragenix::rekey(&rules, &identities, true, &mut std::io::stdout())?;
+ } else {
+ let paths_normalized = paths
+ .into_iter()
+ .map(util::canonicalize_rule_path)
+ .collect::>>()?;
+ let chosen_rules = rules
+ .into_iter()
+ .filter(|x| paths_normalized.contains(&x.path))
+ .collect::>();
+
+ ragenix::rekey(&chosen_rules, &identities, false, &mut std::io::stdout())?;
+ }
}
}
diff --git a/src/ragenix/mod.rs b/src/ragenix/mod.rs
index 7b14a8a..f066f45 100644
--- a/src/ragenix/mod.rs
+++ b/src/ragenix/mod.rs
@@ -148,6 +148,7 @@ pub(crate) fn parse_rules>(rules_path: P) -> Result Result<()> {
let identities = age::get_identities(identities)?;
@@ -155,8 +156,10 @@ pub(crate) fn rekey(
if entry.path.exists() {
writeln!(writer, "Rekeying {}", entry.path.display())?;
age::rekey(&entry.path, &identities, &entry.public_keys)?;
- } else {
+ } else if no_exist_ok {
writeln!(writer, "Does not exist, ignored: {}", entry.path.display())?;
+ } else {
+ return Err(eyre!("Does not exist: {}", entry.path.display()));
}
}
Ok(())
diff --git a/src/util.rs b/src/util.rs
index e6e96c6..7f6d5d1 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -1,7 +1,7 @@
//! Util functions
use std::{
- fs::File,
+ fs::{self, File},
io,
path::{Component, Path, PathBuf},
};
@@ -49,6 +49,14 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf {
ret
}
+/// Make an input path absolute relative to the current directory (rules format)
+pub(crate) fn canonicalize_rule_path>(path: P) -> Result {
+ let path_normalized = normalize_path(path.as_ref());
+ Ok(std::env::current_dir()
+ .and_then(fs::canonicalize)
+ .map(|p| p.join(path_normalized))?)
+}
+
/// Hash a file using SHA-256
pub(crate) fn sha256>(path: P) -> Result> {
let mut file = File::open(path)?;
diff --git a/tests/ragenix.rs b/tests/ragenix.rs
index bc03b93..abe2059 100644
--- a/tests/ragenix.rs
+++ b/tests/ragenix.rs
@@ -367,6 +367,88 @@ fn rekeying_fails_no_valid_identites() -> Result<()> {
Ok(())
}
+#[test]
+#[cfg_attr(not(feature = "recursive-nix"), ignore)]
+fn rekeying_one_works() -> Result<()> {
+ let (_dir, path) = copy_example_to_tmpdir()?;
+
+ let files = &["root.passwd.age"];
+ let expected = files
+ .iter()
+ .map(|s| path.join(s))
+ .map(|p| format!("Rekeying {}", p.display()))
+ .collect::>()
+ .join("\n")
+ + "\n";
+
+ let mut cmd = Command::cargo_bin(crate_name!())?;
+ let assert = cmd
+ .current_dir(&path)
+ .arg("--rekey")
+ .arg(files[0])
+ .arg("--identity")
+ .arg("keys/id_ed25519")
+ .assert();
+
+ assert.success().stdout(expected);
+
+ Ok(())
+}
+
+#[test]
+#[cfg_attr(not(feature = "recursive-nix"), ignore)]
+fn rekeying_one_multiple_works() -> Result<()> {
+ let (_dir, path) = copy_example_to_tmpdir()?;
+
+ let files = &["github-runner.token.age", "root.passwd.age"];
+ let expected = files
+ .iter()
+ .map(|s| path.join(s))
+ .map(|p| format!("Rekeying {}", p.display()))
+ .collect::>()
+ .join("\n")
+ + "\n";
+
+ let mut cmd = Command::cargo_bin(crate_name!())?;
+ let assert = cmd
+ .current_dir(&path)
+ .arg("--rekey")
+ .arg(files[0])
+ .arg(files[1])
+ .arg("--identity")
+ .arg("keys/id_ed25519")
+ .assert();
+
+ assert.success().stdout(expected);
+
+ Ok(())
+}
+
+#[test]
+#[cfg_attr(not(feature = "recursive-nix"), ignore)]
+fn rekeying_one_fails_not_existing_files() -> Result<()> {
+ let (_dir, path) = copy_example_to_tmpdir()?;
+
+ let missing_file = path.join("root.passwd.age");
+ fs::remove_file(&missing_file)?;
+
+ let mut cmd = Command::cargo_bin(crate_name!())?;
+ let assert = cmd
+ .current_dir(&path)
+ .arg("--rekey")
+ .arg(&missing_file)
+ .arg("--identity")
+ .arg("keys/id_ed25519")
+ .assert();
+
+ assert.failure().stderr(predicate::str::contains(format!(
+ "Does not exist: {}",
+ missing_file.display()
+ )));
+
+ Ok(())
+}
+
#[test]
fn prints_schema() -> Result<()> {
let mut cmd = Command::cargo_bin(crate_name!())?;