Skip to content

Commit

Permalink
Allow specific files to be re-encrypted with --rekey
Browse files Browse the repository at this point in the history
Sometimes it's useful to rekey only specific secrets. This change allows
paths to be passed to the `--rekey` option in order to only re-encrypt
them, defaulting to all as before.
  • Loading branch information
devplayer0 committed Sep 5, 2024
1 parent e977cf6 commit 1a7fd94
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 26 deletions.
10 changes: 5 additions & 5 deletions docs/ragenix.1
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions docs/ragenix.1.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions docs/ragenix.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
12 changes: 8 additions & 4 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub(crate) struct Opts {
pub edit: Option<String>,
pub editor: Option<String>,
pub identities: Option<Vec<String>>,
pub rekey: bool,
pub rekey: Option<Vec<String>>,
pub rules: String,
pub schema: bool,
pub verbose: bool,
Expand All @@ -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")
Expand Down Expand Up @@ -107,7 +109,9 @@ where
identities: matches
.get_many::<String>("identity")
.map(|vals| vals.cloned().collect::<Vec<_>>()),
rekey: matches.get_flag("rekey"),
rekey: matches
.get_many::<String>("rekey")
.map(|vals| vals.cloned().collect::<Vec<_>>()),
rules: matches
.get_one::<String>("rules")
.cloned()
Expand Down
25 changes: 18 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -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::<Result<Vec<PathBuf>>>()?;
let chosen_rules = rules
.into_iter()
.filter(|x| paths_normalized.contains(&x.path))
.collect::<Vec<ragenix::RagenixRule>>();

ragenix::rekey(&chosen_rules, &identities, false, &mut std::io::stdout())?;
}
}
}

Expand Down
5 changes: 4 additions & 1 deletion src/ragenix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,18 @@ pub(crate) fn parse_rules<P: AsRef<Path>>(rules_path: P) -> Result<Vec<RagenixRu
pub(crate) fn rekey(
entries: &[RagenixRule],
identities: &[String],
no_exist_ok: bool,
mut writer: impl Write,
) -> Result<()> {
let identities = age::get_identities(identities)?;
for entry in entries {
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(())
Expand Down
10 changes: 9 additions & 1 deletion src/util.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Util functions

use std::{
fs::File,
fs::{self, File},
io,
path::{Component, Path, PathBuf},
};
Expand Down Expand Up @@ -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<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
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<P: AsRef<Path>>(path: P) -> Result<Vec<u8>> {
let mut file = File::open(path)?;
Expand Down
82 changes: 82 additions & 0 deletions tests/ragenix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<String>>()
.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::<Vec<String>>()
.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!())?;
Expand Down

0 comments on commit 1a7fd94

Please sign in to comment.