diff --git a/Cargo.lock b/Cargo.lock index b2968667..59e4b3f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -160,6 +160,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bit-set" version = "0.5.3" @@ -263,6 +269,7 @@ dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", + "serde", "windows-targets 0.52.6", ] @@ -529,6 +536,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deranged" version = "0.3.11" @@ -536,6 +578,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", + "serde", ] [[package]] @@ -1117,6 +1160,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.13.2" @@ -1147,6 +1196,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "home" version = "0.5.9" @@ -1194,6 +1249,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1238,6 +1299,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.6.0" @@ -1246,6 +1318,7 @@ checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown 0.15.0", + "serde", ] [[package]] @@ -2090,7 +2163,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" dependencies = [ - "base64", + "base64 0.21.7", "bitflags 2.6.0", "serde", "serde_derive", @@ -2206,7 +2279,7 @@ version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ - "indexmap", + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -2234,6 +2307,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.6.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1_smol" version = "1.0.1" @@ -2538,6 +2641,7 @@ dependencies = [ "serde", "serde_json", "serde_json_merge", + "serde_with", "shellexpand", "speculoos", "tera", @@ -2563,7 +2667,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.6.0", "serde", "serde_spanned", "toml_datetime", diff --git a/Cargo.toml b/Cargo.toml index 7f611170..a6032db8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ ignore-files = "1.3.0" tokio = { version = "1.41.0", features = ["macros", "rt"] } thiserror = "1.0.65" shellexpand = "3.1.0" +serde_with = "3.11.0" [features] default = ["cli"] diff --git a/bats-tests/tom_home/dotfiles/bombadil.toml b/bats-tests/tom_home/dotfiles/bombadil.toml index 53fc9659..d6726d80 100644 --- a/bats-tests/tom_home/dotfiles/bombadil.toml +++ b/bats-tests/tom_home/dotfiles/bombadil.toml @@ -1,6 +1,6 @@ # Path to your dotfiles relative to your $HOME directory dotfiles_dir = "dotfiles" -gpg_user_id = "test@toml.bombadil.org" +gpg_user_ids = ["test@toml.bombadil.org"] [settings] prehooks = ["echo Hello from bombadil"] diff --git a/src/gpg.rs b/src/gpg.rs index 579e5648..49b296f1 100644 --- a/src/gpg.rs +++ b/src/gpg.rs @@ -9,14 +9,12 @@ const PGP_FOOTER: &str = "\n-----END PGP MESSAGE-----"; #[derive(Clone, Debug)] pub struct Gpg { - pub user_id: String, + pub user_ids: Vec, } impl Gpg { - pub(crate) fn new(user_id: &str) -> Self { - Gpg { - user_id: user_id.to_string(), - } + pub(crate) fn new(user_ids: Vec) -> Self { + Gpg { user_ids } } pub(crate) fn push_secret + ?Sized>( @@ -43,11 +41,12 @@ impl Gpg { } fn encrypt(&self, content: &str) -> Result { - let mut child = Command::new("gpg") - .arg("--encrypt") - .arg("--armor") - .arg("-r") - .arg(&self.user_id) + let mut cmd_binding = Command::new("gpg"); + let cmd = cmd_binding.arg("--encrypt").arg("--armor"); + for user_id in &self.user_ids { + cmd.arg("-r").arg(user_id); + } + let mut child = cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() @@ -73,12 +72,12 @@ impl Gpg { } fn decrypt(&self, content: &str) -> Result { - let mut child = Command::new("gpg") - .arg("--decrypt") - .arg("--armor") - .arg("-r") - .arg(&self.user_id) - .arg("-q") + let mut cmd_binding = Command::new("gpg"); + let cmd = cmd_binding.arg("--decrypt").arg("--armor").arg("-q"); + for user_id in &self.user_ids { + cmd.arg("-r").arg(user_id); + } + let mut child = cmd .stdin(Stdio::piped()) .stdout(Stdio::piped()) .spawn() @@ -116,7 +115,7 @@ mod test { use std::env; use toml::Value; - const GPG_ID: &str = "test@toml.bombadil.org"; + const GPG_IDS: [&'static str; 2] = ["test@toml.bombadil.org", "me@ibotty.net"]; fn gpg_setup() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); @@ -131,7 +130,7 @@ mod test { #[sealed_test(before = gpg_setup())] fn should_encrypt() { - let gpg = Gpg::new(GPG_ID); + let gpg = Gpg::new(GPG_IDS.map(String::from).to_vec()); let result = gpg.encrypt("test"); @@ -140,7 +139,7 @@ mod test { #[sealed_test(before = gpg_setup())] fn should_not_encrypt_unkown_gpg_user() { - let gpg = Gpg::new("unknown.user"); + let gpg = Gpg::new(vec!("unknown.user".to_string())); let result = gpg.encrypt("test"); @@ -149,7 +148,7 @@ mod test { #[sealed_test(before = gpg_setup())] fn should_decrypt() -> Result<()> { - let gpg = Gpg::new(GPG_ID); + let gpg = Gpg::new(GPG_IDS.map(String::from).to_vec()); let encrypted = gpg.encrypt("value")?; let decrypted = gpg.decrypt(&encrypted); @@ -163,7 +162,7 @@ mod test { #[sealed_test(before = gpg_setup())] fn should_push_to_var() -> Result<()> { - let gpg = Gpg::new(GPG_ID); + let gpg = Gpg::new(GPG_IDS.map(String::from).to_vec()); std::fs::write("vars.toml", "")?; gpg.push_secret("key", "value", "vars.toml")?; @@ -177,7 +176,7 @@ mod test { #[sealed_test(before = gpg_setup())] fn should_decrypt_from_file() -> Result<()> { - let gpg = Gpg::new(GPG_ID); + let gpg = Gpg::new(GPG_IDS.map(String::from).to_vec()); std::fs::write("vars.toml", "")?; gpg.push_secret("key", "value", "vars.toml")?; diff --git a/src/lib.rs b/src/lib.rs index 083df172..5c23b21a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -371,7 +371,7 @@ impl Bombadil { if let Some(gpg) = &self.gpg { gpg.push_secret(key, value, var_file) } else { - Err(anyhow!("No gpg_user_id in bombadil settings")) + Err(anyhow!("No gpg_user_ids in bombadil settings")) } } @@ -521,7 +521,7 @@ impl Bombadil { let path = config.get_dotfiles_path()?; let gpg = match mode { - Mode::Gpg => config.gpg_user_id.map(|user_id| Gpg::new(&user_id)), + Mode::Gpg => config.gpg_user_ids.map(|user_ids| Gpg::new(user_ids)), Mode::NoGpg => None, }; diff --git a/src/settings/mod.rs b/src/settings/mod.rs index badb30f6..97968d99 100644 --- a/src/settings/mod.rs +++ b/src/settings/mod.rs @@ -8,6 +8,7 @@ use config::{ConfigError, File}; use dirs::home_dir; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; +use serde_with::{serde_as, OneOrMany}; use std::collections::HashMap; use std::path::PathBuf; @@ -19,9 +20,9 @@ lazy_static! { pub static ref SETTINGS: Settings = Settings::get().unwrap_or_default(); pub static ref GPG: Option = { SETTINGS - .gpg_user_id + .gpg_user_ids .as_ref() - .map(|gpg| Gpg::new(gpg.as_str())) + .map(|user_ids| Gpg::new(user_ids.to_vec())) }; } @@ -40,13 +41,17 @@ pub fn dotfile_dir() -> PathBuf { } /// The Global bombadil configuration +#[serde_as] #[derive(Debug, Deserialize, Serialize, Default)] #[serde(deny_unknown_fields)] pub struct Settings { /// User define dotfiles directory, usually your versioned dotfiles pub dotfiles_dir: PathBuf, - pub gpg_user_id: Option, + #[serde(alias = "gpg_user_id")] + #[serde_as(as = "Option>")] + #[serde(default)] + pub gpg_user_ids: Option>, #[serde(default)] pub settings: ActiveProfile, diff --git a/website/docs/guide/imports.md b/website/docs/guide/imports.md index 8debd1b0..2dbe325d 100644 --- a/website/docs/guide/imports.md +++ b/website/docs/guide/imports.md @@ -13,7 +13,7 @@ Instead of having all your configs defined in a single toml file, you can split ```toml dotfiles_dir = "dotfiles" -gpg_user_id = "me@example.org" +gpg_user_ids = ["me@example.org"] import = [ { path = "wm/sway/sway.toml" }, diff --git a/website/docs/guide/secrets.md b/website/docs/guide/secrets.md index 1c40e8b5..3702ab96 100644 --- a/website/docs/guide/secrets.md +++ b/website/docs/guide/secrets.md @@ -21,8 +21,8 @@ Add your gpg user id to `bombadil.toml`: ```toml dotfile_dir = "bombadil-example" -# The gpg user associated with the key pair you want to use -gpg_user_id = "me@example.org" +# The gpg user ids associated with the key pairs you want to use +gpg_user_ids = ["me@example.org"] vars = [ "vars.toml" ] ``` diff --git a/website/docs/quickstart.md b/website/docs/quickstart.md index 289a32f8..40faed7f 100644 --- a/website/docs/quickstart.md +++ b/website/docs/quickstart.md @@ -44,8 +44,8 @@ here is a sample configuration: # Path to your dotfile directory containing this config file (relative to $HOME). dotfiles_dir = "dotfiles" -# (Optional) GPG user id for secret encryption/decryption. -gpg_user_id = "paul.delafosse@protonmail.com" +# (Optional) GPG user ids for secret encryption/decryption. +gpg_user_ids = ["paul.delafosse@protonmail.com"] # (Optional) list of bombadil config files to include in the configuration. import = [