orca_core/secrets/
store.rs1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt::Write;
5use std::path::{Path, PathBuf};
6
7fn default_master_key_path() -> PathBuf {
9 dirs_or_home().join("master.key")
10}
11
12fn dirs_or_home() -> PathBuf {
14 std::env::var("HOME")
15 .map(|h| PathBuf::from(h).join(".orca"))
16 .unwrap_or_else(|_| PathBuf::from(".orca"))
17}
18
19fn xor_bytes(data: &[u8], key: &[u8]) -> Vec<u8> {
21 if key.is_empty() {
22 return data.to_vec();
23 }
24 data.iter()
25 .enumerate()
26 .map(|(i, b)| b ^ key[i % key.len()])
27 .collect()
28}
29
30fn base64_encode(data: &[u8]) -> String {
32 let mut s = String::with_capacity(data.len() * 2);
33 for b in data {
34 let _ = write!(s, "{b:02x}");
35 }
36 s
37}
38
39fn base64_decode(hex: &str) -> Vec<u8> {
41 (0..hex.len())
42 .step_by(2)
43 .filter_map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
44 .collect()
45}
46
47fn load_or_create_key(path: &Path) -> Result<Vec<u8>> {
49 if path.exists() {
50 std::fs::read(path).context("failed to read master key")
51 } else {
52 if let Some(parent) = path.parent() {
53 std::fs::create_dir_all(parent).context("failed to create key directory")?;
54 }
55 let mut key = vec![0u8; 32];
56 use std::io::Read;
57 std::fs::File::open("/dev/urandom")
58 .context("failed to open /dev/urandom")?
59 .read_exact(&mut key)
60 .context("failed to read random bytes")?;
61 std::fs::write(path, &key).context("failed to write master key")?;
62 #[cfg(unix)]
63 {
64 use std::os::unix::fs::PermissionsExt;
65 let perms = std::fs::Permissions::from_mode(0o600);
66 std::fs::set_permissions(path, perms).context("failed to set key permissions")?;
67 }
68 Ok(key)
69 }
70}
71
72#[derive(Debug, Serialize, Deserialize)]
77pub struct SecretStore {
78 #[serde(skip)]
79 path: PathBuf,
80 #[serde(skip)]
81 master_key: Vec<u8>,
82 secrets: HashMap<String, String>,
83}
84
85impl SecretStore {
86 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
88 Self::open_with_key(path, &default_master_key_path())
89 }
90
91 pub fn open_with_key(path: impl AsRef<Path>, key_path: &Path) -> Result<Self> {
93 let path = path.as_ref().to_path_buf();
94 let master_key = load_or_create_key(key_path)?;
95
96 if path.exists() {
97 let data = std::fs::read_to_string(&path).context("failed to read secrets file")?;
98 let mut store: SecretStore =
99 serde_json::from_str(&data).context("failed to parse secrets file")?;
100 store.path = path;
101 store.master_key = master_key.clone();
102 store.secrets = store
104 .secrets
105 .into_iter()
106 .map(|(k, v)| {
107 let encrypted = base64_decode(&v);
108 let decrypted = xor_bytes(&encrypted, &master_key);
109 (k, String::from_utf8_lossy(&decrypted).to_string())
110 })
111 .collect();
112 Ok(store)
113 } else {
114 let store = SecretStore {
115 path,
116 master_key,
117 secrets: HashMap::new(),
118 };
119 store.save()?;
120 Ok(store)
121 }
122 }
123
124 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) -> Result<()> {
126 self.secrets.insert(key.into(), value.into());
127 self.save()
128 }
129
130 pub fn get(&self, key: &str) -> Option<&str> {
132 self.secrets.get(key).map(|s| s.as_str())
133 }
134
135 pub fn remove(&mut self, key: &str) -> Result<bool> {
137 let existed = self.secrets.remove(key).is_some();
138 if existed {
139 self.save()?;
140 }
141 Ok(existed)
142 }
143
144 pub fn list(&self) -> Vec<String> {
146 let mut keys: Vec<String> = self.secrets.keys().cloned().collect();
147 keys.sort();
148 keys
149 }
150
151 pub fn resolve_env(&self, env: &HashMap<String, String>) -> HashMap<String, String> {
155 env.iter()
156 .map(|(k, v)| (k.clone(), self.resolve_value(v)))
157 .collect()
158 }
159
160 fn save(&self) -> Result<()> {
163 if let Some(parent) = self.path.parent() {
164 std::fs::create_dir_all(parent).context("failed to create secrets directory")?;
165 }
166
167 let encrypted_secrets: HashMap<String, String> = self
169 .secrets
170 .iter()
171 .map(|(k, v)| {
172 let encrypted = xor_bytes(v.as_bytes(), &self.master_key);
173 (k.clone(), base64_encode(&encrypted))
174 })
175 .collect();
176
177 let on_disk = serde_json::json!({ "secrets": encrypted_secrets });
178 let data = serde_json::to_string_pretty(&on_disk).context("failed to serialize secrets")?;
179 std::fs::write(&self.path, &data).context("failed to write secrets file")?;
180
181 #[cfg(unix)]
183 {
184 use std::os::unix::fs::PermissionsExt;
185 let perms = std::fs::Permissions::from_mode(0o600);
186 std::fs::set_permissions(&self.path, perms)
187 .context("failed to set secrets file permissions")?;
188 }
189
190 Ok(())
191 }
192
193 fn resolve_value(&self, value: &str) -> String {
195 let mut result = value.to_string();
196 let mut search_from = 0;
197 loop {
198 let Some(start) = result[search_from..].find("${secrets.") else {
199 break;
200 };
201 let abs_start = search_from + start;
202 let after_prefix = abs_start + "${secrets.".len();
203 let Some(end) = result[after_prefix..].find('}') else {
204 break;
205 };
206 let key = result[after_prefix..after_prefix + end].to_string();
207 if let Some(secret_value) = self.secrets.get(&key) {
208 result = format!(
209 "{}{}{}",
210 &result[..abs_start],
211 secret_value,
212 &result[after_prefix + end + 1..]
213 );
214 } else {
216 search_from = after_prefix + end + 1;
218 }
219 }
220 result
221 }
222}
223
224#[cfg(test)]
225#[path = "store_tests.rs"]
226mod tests;