Skip to main content

mk_lib/
secrets.rs

1use std::env;
2use std::fs::{
3  self,
4  File,
5};
6use std::path::{
7  Path,
8  PathBuf,
9};
10
11use anyhow::Context as _;
12use hashbrown::HashMap;
13use pgp::composed::{
14  Deserializable as _,
15  Message,
16  SignedSecretKey,
17};
18
19use crate::file::ToUtf8 as _;
20use crate::utils::{
21  parse_env_contents,
22  resolve_path,
23};
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct SecretConfig {
27  pub vault_location: PathBuf,
28  pub keys_location: PathBuf,
29  pub key_name: String,
30}
31
32impl SecretConfig {
33  pub fn resolve(
34    base_dir: &Path,
35    vault_location: Option<&str>,
36    keys_location: Option<&str>,
37    key_name: Option<&str>,
38  ) -> Self {
39    let vault_location = vault_location
40      .map(|path| resolve_path(base_dir, path))
41      .unwrap_or_else(|| default_vault_location(base_dir));
42    let keys_location = keys_location
43      .map(|path| resolve_path(base_dir, path))
44      .unwrap_or_else(default_keys_location);
45    let key_name = key_name.unwrap_or("default").to_string();
46
47    Self {
48      vault_location,
49      keys_location,
50      key_name,
51    }
52  }
53}
54
55pub fn load_secret_values(
56  path: &str,
57  base_dir: &Path,
58  vault_location: Option<&str>,
59  keys_location: Option<&str>,
60  key_name: Option<&str>,
61) -> anyhow::Result<Vec<String>> {
62  let config = SecretConfig::resolve(base_dir, vault_location, keys_location, key_name);
63  verify_vault(&config.vault_location)?;
64  let signed_secret_key = load_secret_key(&config)?;
65
66  let secret_path = config.vault_location.join(path);
67  if !secret_path.exists() || !secret_path.is_dir() {
68    anyhow::bail!(
69      "Secret path does not exist: {}",
70      secret_path.to_utf8().unwrap_or("<non-utf8-path>")
71    );
72  }
73
74  let mut data_paths = fs::read_dir(&secret_path)?
75    .filter_map(Result::ok)
76    .map(|entry| {
77      if entry.path().is_dir() {
78        entry.path().join("data.asc")
79      } else {
80        entry.path()
81      }
82    })
83    .filter(|path| path.exists() && path.is_file())
84    .collect::<Vec<_>>();
85  data_paths.sort();
86
87  let mut values = Vec::with_capacity(data_paths.len());
88  for data_path in data_paths {
89    let mut data_file = std::io::BufReader::new(File::open(data_path)?);
90    let (message, _) = Message::from_armor(&mut data_file)?;
91    let mut decrypted_message = message.decrypt(&pgp::types::Password::empty(), &signed_secret_key)?;
92    let value = decrypted_message
93      .as_data_string()
94      .context("Failed to read secret value")?;
95    values.push(value);
96  }
97
98  if values.is_empty() {
99    anyhow::bail!("No secrets found for path: {path}");
100  }
101
102  Ok(values)
103}
104
105pub fn load_secret_value(
106  path: &str,
107  base_dir: &Path,
108  vault_location: Option<&str>,
109  keys_location: Option<&str>,
110  key_name: Option<&str>,
111) -> anyhow::Result<String> {
112  let values = load_secret_values(path, base_dir, vault_location, keys_location, key_name)?;
113  match values.as_slice() {
114    [value] => Ok(value.clone()),
115    [] => anyhow::bail!("No secrets found for path: {path}"),
116    _ => anyhow::bail!("Secret path resolved to multiple values: {path}"),
117  }
118}
119
120pub fn list_secret_paths(
121  path_prefix: Option<&str>,
122  base_dir: &Path,
123  vault_location: Option<&str>,
124) -> anyhow::Result<Vec<String>> {
125  let config = SecretConfig::resolve(base_dir, vault_location, None, None);
126  verify_vault(&config.vault_location)?;
127
128  let root = match path_prefix {
129    Some(path_prefix) if !path_prefix.is_empty() => config.vault_location.join(path_prefix),
130    _ => config.vault_location.clone(),
131  };
132
133  if !root.exists() || !root.is_dir() {
134    anyhow::bail!(
135      "Secret path does not exist: {}",
136      root.to_utf8().unwrap_or("<non-utf8-path>")
137    );
138  }
139
140  let mut secret_paths = Vec::new();
141  collect_secret_paths(&config.vault_location, &root, &mut secret_paths)?;
142  secret_paths.sort();
143  secret_paths.dedup();
144  Ok(secret_paths)
145}
146
147pub fn load_secret_env(
148  paths: &[String],
149  base_dir: &Path,
150  vault_location: Option<&str>,
151  keys_location: Option<&str>,
152  key_name: Option<&str>,
153) -> anyhow::Result<HashMap<String, String>> {
154  let mut env_vars = HashMap::new();
155
156  for path in paths {
157    for value in load_secret_values(path, base_dir, vault_location, keys_location, key_name)? {
158      env_vars.extend(parse_env_contents(&value));
159    }
160  }
161
162  Ok(env_vars)
163}
164
165fn default_vault_location(base_dir: &Path) -> PathBuf {
166  resolve_path(base_dir, "./.mk/vault")
167}
168
169fn default_keys_location() -> PathBuf {
170  let home_dir = if cfg!(target_os = "windows") {
171    env::var("USERPROFILE").unwrap_or_else(|_| "./.mk/priv".to_string())
172  } else {
173    env::var("HOME").unwrap_or_else(|_| "./.mk/priv".to_string())
174  };
175
176  let mut path = PathBuf::from(home_dir);
177  path.push(".config");
178  path.push("mk");
179  path.push("priv");
180  path
181}
182
183fn verify_vault(vault_location: &Path) -> anyhow::Result<()> {
184  if !vault_location.exists() || !vault_location.is_dir() {
185    anyhow::bail!("The store does not exist");
186  }
187
188  Ok(())
189}
190
191fn load_secret_key(config: &SecretConfig) -> anyhow::Result<SignedSecretKey> {
192  if !config.keys_location.exists() || !config.keys_location.is_dir() {
193    anyhow::bail!("The keys location does not exist");
194  }
195
196  let key_path = config.keys_location.join(format!("{}.key", config.key_name));
197  if !key_path.exists() || !key_path.is_file() {
198    anyhow::bail!("The key does not exist");
199  }
200
201  let mut secret_key_string = File::open(key_path)?;
202  let (signed_secret_key, _) = SignedSecretKey::from_armor_single(&mut secret_key_string)?;
203  signed_secret_key.verify()?;
204  Ok(signed_secret_key)
205}
206
207fn collect_secret_paths(vault_root: &Path, dir: &Path, secret_paths: &mut Vec<String>) -> anyhow::Result<()> {
208  let data_path = dir.join("data.asc");
209  if data_path.exists() && data_path.is_file() {
210    let relative = dir
211      .strip_prefix(vault_root)
212      .map_err(|_| anyhow::anyhow!("Failed to resolve secret path relative to vault"))?;
213    secret_paths.push(relative.to_utf8().unwrap_or("<non-utf8-path>").to_string());
214  }
215
216  for entry in fs::read_dir(dir)?.filter_map(Result::ok) {
217    let path = entry.path();
218    if path.is_dir() {
219      collect_secret_paths(vault_root, &path, secret_paths)?;
220    }
221  }
222
223  Ok(())
224}