fix(reencrypt): also re-encrypt inline encrypted vars in the config

reencrypt only rewrote .age files; the inline 'encrypted = { KEY = "base64" }'
vars in doot.doot were left encrypted to the old recipients, so after adding a
recipient they stayed undecryptable by the new key.

Now each inline var is decrypted, re-encrypted to the current recipients, and
its ciphertext literal swapped in place in the source (textual replace, not an
AST reprint - so the file's formatting and comments are untouched, and an
indirected or non-literal ciphertext is skipped with a warning).

Verified on the real config: all 8 inline vars re-encrypted to the new
recipients and decrypt correctly.
This commit is contained in:
Ray Andrew 2026-06-17 21:56:28 -07:00
parent 59eae012de
commit b31953b134
Signed by: rayandrew
SSH key fingerprint: SHA256:iGurnBY6QgoHsQWxP3NgvMEA4F3GjTcszIJnLk2jinw

View file

@ -55,11 +55,35 @@ pub fn run(config_path: Option<PathBuf>, recipients: Vec<String>) -> anyhow::Res
anyhow::bail!("no recipient keys found"); anyhow::bail!("no recipient keys found");
} }
// Also re-encrypt encrypted vars from the doot config
let (result, _vars) = super::load(&path)?; let (result, _vars) = super::load(&path)?;
let mut count = 0; let mut count = 0;
// Re-encrypt inline encrypted vars (stored as base64 ciphertext in the config
// source) by rewriting their literals in place.
if !result.encrypted_vars.is_empty() {
let mut source = std::fs::read_to_string(&path)?;
let mut changed = 0;
for (name, b64) in &result.encrypted_vars {
let from = format!("\"{b64}\"");
if !source.contains(&from) {
eprintln!(
"skip var {name}: ciphertext not found verbatim in {}",
path.display()
);
continue;
}
let new_b64 = reencrypt_var(b64, &identity_key, &keys)?;
source = source.replace(&from, &format!("\"{new_b64}\""));
eprintln!("re-encrypted var {name}");
changed += 1;
}
if changed > 0 {
std::fs::write(&path, source)?;
count += changed;
}
}
// Re-encrypt .age files referenced in encrypted: block // Re-encrypt .age files referenced in encrypted: block
for (name, rel_path) in &result.encrypted_files { for (name, rel_path) in &result.encrypted_files {
let full_path = if rel_path.is_relative() { let full_path = if rel_path.is_relative() {
@ -144,6 +168,22 @@ pub fn run(config_path: Option<PathBuf>, recipients: Vec<String>) -> anyhow::Res
Ok(()) Ok(())
} }
/// Decrypt a base64 age ciphertext and re-encrypt it to `recipients`, returning
/// the new base64 ciphertext.
fn reencrypt_var(b64: &str, identity_key: &str, recipients: &[String]) -> anyhow::Result<String> {
let data = doot_core::builtins::crypto::base64_decode(b64)
.map_err(|e| anyhow::anyhow!("invalid base64: {e}"))?;
let plaintext = AgeEncryption::new()
.with_identity(identity_key)?
.decrypt(&data)?;
let mut encryption = AgeEncryption::new();
for key in recipients {
encryption.add_recipient(key)?;
}
let ciphertext = encryption.encrypt(&plaintext)?;
Ok(doot_core::builtins::crypto::base64_encode(&ciphertext))
}
fn reencrypt_file(path: &PathBuf, identity_key: &str, recipients: &[String]) -> anyhow::Result<()> { fn reencrypt_file(path: &PathBuf, identity_key: &str, recipients: &[String]) -> anyhow::Result<()> {
// Decrypt // Decrypt
let decryption = AgeEncryption::new().with_identity(identity_key)?; let decryption = AgeEncryption::new().with_identity(identity_key)?;