Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specific files to be re-encrypted with --rekey #149

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 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 @@ -36,6 +36,13 @@ Decrypt all secrets given in the rules configuration file and encrypt them with
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
When rekeying, \fBragenix\fR does not write any plaintext data to disk; all processing happens in\-memory\.
.TP
\fB\-R\fR, \fB\-\-rekey\-one\fR \fIPATH\fR
Decrypt specified secrets given in the rules configuration file and encrypt them with the defined public keys\. This option is useful to grant a new recipient access to specific 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
When rekeying, \fBragenix\fR does not write any plaintext data to disk; all processing happens in\-memory\.
.SH "COMMON OPTIONS"
.TP
\fB\-\-rules\fR \fIPATH\fR
Expand Down
15 changes: 14 additions & 1 deletion docs/ragenix.1.html

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

11 changes: 11 additions & 0 deletions docs/ragenix.1.ronn
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ store.
When rekeying, `ragenix` does not write any plaintext data to disk; all
processing happens in-memory.

* `-R`, `--rekey-one` <PATH>:
Decrypt specified secrets given in the rules configuration file and encrypt
them with the defined public keys. This option is useful to grant a new
recipient access to specific secrets.

If the `--identity` option is not given, `ragenix` tries to decrypt <PATH>
with the default SSH private keys. See `--identity` for details.

When rekeying, `ragenix` does not write any plaintext data to disk; all
processing happens in-memory.

## COMMON OPTIONS

* `--rules` <PATH>:
Expand Down
15 changes: 14 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub(crate) struct Opts {
pub editor: Option<String>,
pub identities: Option<Vec<String>>,
pub rekey: bool,
pub rekey_chosen: Option<Vec<String>>,
pub rules: String,
pub schema: bool,
pub verbose: bool,
Expand Down Expand Up @@ -40,6 +41,15 @@ fn build() -> Command {
.short('r')
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("rekey-one")
.help("re-encrypts specified secrets with specified recipients")
.long("rekey-one")
.short('R')
.value_name("FILE")
.action(ArgAction::Append)
.value_hint(ValueHint::FilePath),
)
.arg(
Arg::new("identity")
.help("private key to use when decrypting")
Expand All @@ -66,7 +76,7 @@ fn build() -> Command {
)
.group(
ArgGroup::new("action")
.args(["edit", "rekey", "schema"])
.args(["edit", "rekey", "rekey-one", "schema"])
.required(true),
)
.arg(
Expand Down Expand Up @@ -108,6 +118,9 @@ where
.get_many::<String>("identity")
.map(|vals| vals.cloned().collect::<Vec<_>>()),
rekey: matches.get_flag("rekey"),
rekey_chosen: matches
.get_many::<String>("rekey-one")
.map(|vals| vals.cloned().collect::<Vec<_>>()),
rules: matches
.get_one::<String>("rules")
.cloned()
Expand Down
20 changes: 14 additions & 6 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 @@ -42,7 +39,18 @@ fn main() -> Result<()> {
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())?;
ragenix::rekey(&rules, &identities, true, &mut std::io::stdout())?;
} else if let Some(paths) = opts.rekey_chosen {
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 a path relative to the current working 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
83 changes: 83 additions & 0 deletions tests/ragenix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,89 @@ 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-one")
.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-one")
.arg(files[0])
.arg("--rekey-one")
.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-one")
.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