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}