1use cbc::Decryptor;
2use cipher::block_padding::Pkcs7;
3use cipher::{BlockDecryptMut, KeyIvInit};
4use hex::FromHex;
5use std::env;
6use std::fs::File;
7use std::io::{BufRead, BufReader};
8use thiserror::Error;
9
10#[derive(Error, Debug)]
11pub enum EnvError {
12 #[error("Environment variable error: {0}")]
13 VarError(#[from] env::VarError),
14
15 #[error("Failed to decrypt: {0}")]
16 DecryptionFailed(String),
17
18 #[error("Missing key file for decryption")]
19 MissingKeyFile,
20
21 #[error("IO error: {0}")]
22 IoError(#[from] std::io::Error),
23
24 #[error("Key file format error: {0}")]
25 KeyFileFormatError(String),
26}
27
28pub fn get_var(name: &str) -> Result<String, EnvError> {
38 Ok(env::var(name)?)
39}
40
41pub fn get_var_or(name: &str, default: &str) -> String {
48 env::var(name).unwrap_or_else(|_| default.to_string())
49}
50
51pub fn is_encrypted(value: &str) -> bool {
61 value.starts_with("+encs+")
62}
63
64fn parse_key_file(path: &str) -> Result<(String, String, String), EnvError> {
82 let file = File::open(path).map_err(|_| EnvError::MissingKeyFile)?;
83 let reader = BufReader::new(file);
84
85 let mut salt = None;
86 let mut key = None;
87 let mut iv = None;
88
89 for (line_num, line_result) in reader.lines().enumerate() {
90 let line = line_result?;
91 let line_num = line_num + 1; if line.trim().is_empty() {
94 continue;
95 }
96
97 match line.trim().split_once('=') {
98 Some(("salt", value)) => salt = Some(value.to_string()),
99 Some(("key", value)) => key = Some(value.to_string()),
100 Some(("iv", value)) => iv = Some(value.to_string()),
101 Some((other, _)) => {
102 return Err(EnvError::KeyFileFormatError(format!(
103 "Unexpected content at line {}: '{}'",
104 line_num, other
105 )));
106 }
107 None => {}
108 }
109 }
110
111 let salt =
112 salt.ok_or_else(|| EnvError::KeyFileFormatError("Missing salt in key file".to_string()))?;
113 let key =
114 key.ok_or_else(|| EnvError::KeyFileFormatError("Missing key in key file".to_string()))?;
115 let iv =
116 iv.ok_or_else(|| EnvError::KeyFileFormatError("Missing iv in key file".to_string()))?;
117
118 Ok((salt, key, iv))
119}
120
121pub fn decrypt(value: &str, key_file: &str) -> Result<String, EnvError> {
147 if value.len() < 6 || !is_encrypted(value) {
148 return Ok(value.to_string());
149 }
150
151 let hex = &value[6..];
152 let mut encrypted_bytes = Vec::from_hex(hex)
153 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid hex encoding: {}", e)))?;
154
155 let (_, key_hex, iv_hex) = parse_key_file(key_file)?;
156
157 let key_bytes = Vec::from_hex(key_hex)
158 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid key hex: {}", e)))?;
159 let iv_bytes = Vec::from_hex(iv_hex)
160 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid iv hex: {}", e)))?;
161
162 type Aes256Cbc = Decryptor<aes::Aes256>;
163
164 let decrypted_bytes = Aes256Cbc::new_from_slices(&key_bytes, &iv_bytes)
165 .map_err(|_| EnvError::DecryptionFailed("Invalid key or IV length".to_string()))?
166 .decrypt_padded_mut::<Pkcs7>(&mut encrypted_bytes)
167 .map_err(|e| EnvError::DecryptionFailed(format!("Decryption failed: {}", e)))?;
168
169 String::from_utf8(decrypted_bytes.into())
170 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid UTF-8 in decrypted data: {}", e)))
171}
172
173pub fn get_secure_var(name: &str, key_file: &str) -> Result<String, EnvError> {
187 let value = get_var(name)?;
188 if is_encrypted(&value) {
189 decrypt(&value, key_file)
190 } else {
191 Ok(value)
192 }
193}
194
195pub fn get_secure_var_or(name: &str, key_file: &str, default: &str) -> Result<String, EnvError> {
220 match get_var(name) {
221 Ok(val) => {
222 if is_encrypted(&val) {
223 decrypt(&val, key_file)
224 } else {
225 Ok(val)
226 }
227 }
228 Err(EnvError::VarError(_)) => Ok(default.to_string()),
229 Err(e) => Err(e),
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use std::fs::File;
237 use std::io::Write;
238 use temp_env::with_var;
239 use tempfile::tempdir;
240
241 const VALID_KEY_FILE_CONTENTS: &str = r#"salt=89A6A795C9CCECB5
242key=26D6EDD53A0AFA8FA1AA3FBCD2FFF2A0BF4809A4E04511F629FC732C2A42A8FC
243iv=472A3557ADDD2525AD4E555738636A67
244"#;
245
246 const ENCRYPTED_VAR_1: &str = "+encs+BCC9E963342C9CFEFB45093F3437A680";
247 const DECRYPTED_VAR_1: &str = "12345";
248
249 const ENCRYPTED_VAR_2: &str = "+encs+3510EEEF4163EB21C671FB5C57ADFCE2";
250 const DECRYPTED_VAR_2: &str = "/";
251
252 #[test]
253 fn test_get_env() {
254 with_var("TEST_VAR", Some("test_value"), || {
256 assert_eq!(get_var("TEST_VAR").unwrap(), "test_value");
257 assert_eq!(get_var_or("TEST_VAR", "default"), "test_value");
258 });
259
260 with_var::<_, &str, _, _>("NON_EXISTENT_VAR", None, || {
262 assert!(get_var("NON_EXISTENT_VAR").is_err());
263 assert_eq!(get_var_or("NON_EXISTENT_VAR", "default"), "default");
264 });
265 }
266
267 #[test]
268 fn test_is_encrypted() {
269 assert!(is_encrypted("+encs+1234567890ABCDEF"));
270 assert!(!is_encrypted("plain_text"));
271 assert!(!is_encrypted(""));
272 }
273
274 #[test]
275 fn test_parse_key_file() {
276 let dir = tempdir().unwrap();
277 let key_file_path = dir.path().join("key-file");
278
279 {
281 let mut file = File::create(&key_file_path).unwrap();
282 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
283 }
284
285 let result = parse_key_file(key_file_path.to_str().unwrap());
287 assert!(result.is_ok());
288 let (salt, key, iv) = result.unwrap();
289 assert_eq!(salt, "89A6A795C9CCECB5");
290 assert_eq!(
291 key,
292 "26D6EDD53A0AFA8FA1AA3FBCD2FFF2A0BF4809A4E04511F629FC732C2A42A8FC"
293 );
294 assert_eq!(iv, "472A3557ADDD2525AD4E555738636A67");
295
296 let invalid_key_file_path = dir.path().join("invalid-key-file");
298 let mut file = File::create(&invalid_key_file_path).unwrap();
299 writeln!(file, "salt=1234567890ABCDEF").unwrap();
300 writeln!(file, "invalid_line=something").unwrap();
301 writeln!(file, "iv=1234567890ABCDEF").unwrap();
302
303 let result = parse_key_file(invalid_key_file_path.to_str().unwrap());
304 assert!(result.is_err());
305
306 if let Err(EnvError::KeyFileFormatError(msg)) = result {
307 assert!(msg.contains("Unexpected content at line 2"));
308 } else {
309 panic!("Expected KeyFileFormatError");
310 }
311
312 let empty_key_file_path = dir.path().join("empty-key-file");
314 let _file = File::create(&empty_key_file_path).unwrap();
315
316 let result = parse_key_file(empty_key_file_path.to_str().unwrap());
317 assert!(result.is_err());
318
319 if let Err(EnvError::KeyFileFormatError(msg)) = result {
320 assert!(msg.contains("Missing salt in key file"));
322 } else {
323 panic!("Expected KeyFileFormatError");
324 }
325
326 let spaced_key_file_path = dir.path().join("spaced-key-file");
328 let mut file = File::create(&spaced_key_file_path).unwrap();
329 writeln!(file, "salt=1234567890ABCDEF").unwrap();
330 writeln!(file).unwrap();
331 writeln!(file, "key=1234567890ABCDEF1234567890ABCDEF").unwrap();
332 writeln!(file, " ").unwrap();
333 writeln!(file, "iv=1234567890ABCDEF").unwrap();
334
335 let result = parse_key_file(spaced_key_file_path.to_str().unwrap());
336 assert!(result.is_ok());
337 }
338
339 #[test]
340 fn test_decrypt_unencrypted() {
341 let dir = tempdir().unwrap();
342 let key_file_path = dir.path().join("key-file");
343 {
344 let mut file = File::create(&key_file_path).unwrap();
345 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
346 }
347 let result = decrypt("not-encrypted", key_file_path.to_str().unwrap());
349 assert!(result.is_ok());
350 assert_eq!(result.unwrap(), "not-encrypted");
351 }
352
353 #[test]
354 fn test_get_secure() {
355 let dir = tempdir().unwrap();
356 let key_file_path = dir.path().join("key-file");
357 {
358 let mut file = File::create(&key_file_path).unwrap();
359 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
360 }
361
362 with_var("PLAIN_VAR", Some("plain_text"), || {
364 let result = get_secure_var("PLAIN_VAR", key_file_path.to_str().unwrap());
365 assert!(result.is_ok());
366 assert_eq!(result.unwrap(), "plain_text");
367 });
368
369 with_var::<_, &str, _, _>("MISSING_VAR", None, || {
371 let result = get_secure_var("MISSING_VAR", key_file_path.to_str().unwrap());
372 assert!(result.is_err());
373 if let Err(e) = result {
374 assert!(matches!(e, EnvError::VarError(_)));
375 }
376 });
377
378 with_var("ENCRYPTED_VAR", Some(ENCRYPTED_VAR_1), || {
380 let result = get_secure_var("ENCRYPTED_VAR", key_file_path.to_str().unwrap());
381 assert!(result.is_ok());
382 assert_eq!(result.unwrap(), DECRYPTED_VAR_1.to_string());
383 });
384
385 with_var("ENCRYPTED_VAR", Some(ENCRYPTED_VAR_2), || {
387 let result = get_secure_var("ENCRYPTED_VAR", key_file_path.to_str().unwrap());
388 assert!(result.is_ok());
389 assert_eq!(result.unwrap(), DECRYPTED_VAR_2.to_string());
390 });
391 }
392
393 #[test]
394 fn test_decrypt_missing_keyfile() {
395 let result = decrypt(ENCRYPTED_VAR_1, "/non/existent/keyfile");
396 assert!(result.is_err());
397 assert!(matches!(result.unwrap_err(), EnvError::MissingKeyFile));
398 }
399
400 #[test]
401 fn test_decrypt_invalid_hex() {
402 let dir = tempdir().unwrap();
403 let key_file_path = dir.path().join("key-file");
404 {
405 let mut file = File::create(&key_file_path).unwrap();
406 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
407 }
408 let result = decrypt("+encs+ZZ", key_file_path.to_str().unwrap());
409 assert!(result.is_err());
410 assert!(matches!(result.unwrap_err(), EnvError::DecryptionFailed(_)));
411 }
412}