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 get_var_or_empty(name: &str) -> String {
53 get_var_or(name, "")
54}
55
56pub fn is_encrypted(value: &str) -> bool {
66 value.starts_with("+encs+")
67}
68
69fn parse_key_file(path: &str) -> Result<(String, String, String), EnvError> {
87 let file = File::open(path).map_err(|_| EnvError::MissingKeyFile)?;
88 let reader = BufReader::new(file);
89
90 let mut salt = None;
91 let mut key = None;
92 let mut iv = None;
93
94 for (line_num, line_result) in reader.lines().enumerate() {
95 let line = line_result?;
96 let line_num = line_num + 1; if line.trim().is_empty() {
99 continue;
100 }
101
102 match line.trim().split_once('=') {
103 Some(("salt", value)) => salt = Some(value.to_string()),
104 Some(("key", value)) => key = Some(value.to_string()),
105 Some(("iv", value)) => iv = Some(value.to_string()),
106 Some((other, _)) => {
107 return Err(EnvError::KeyFileFormatError(format!(
108 "Unexpected content at line {}: '{}'",
109 line_num, other
110 )));
111 }
112 None => {}
113 }
114 }
115
116 let salt =
117 salt.ok_or_else(|| EnvError::KeyFileFormatError("Missing salt in key file".to_string()))?;
118 let key =
119 key.ok_or_else(|| EnvError::KeyFileFormatError("Missing key in key file".to_string()))?;
120 let iv =
121 iv.ok_or_else(|| EnvError::KeyFileFormatError("Missing iv in key file".to_string()))?;
122
123 Ok((salt, key, iv))
124}
125
126pub fn decrypt(value: &str, key_file: &str) -> Result<String, EnvError> {
152 if value.len() < 6 || !is_encrypted(value) {
153 return Ok(value.to_string());
154 }
155
156 let hex = &value[6..];
157 let mut encrypted_bytes = Vec::from_hex(hex)
158 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid hex encoding: {}", e)))?;
159
160 let (_, key_hex, iv_hex) = parse_key_file(key_file)?;
161
162 let key_bytes = Vec::from_hex(key_hex)
163 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid key hex: {}", e)))?;
164 let iv_bytes = Vec::from_hex(iv_hex)
165 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid iv hex: {}", e)))?;
166
167 type Aes256Cbc = Decryptor<aes::Aes256>;
168
169 let decrypted_bytes = Aes256Cbc::new_from_slices(&key_bytes, &iv_bytes)
170 .map_err(|_| EnvError::DecryptionFailed("Invalid key or IV length".to_string()))?
171 .decrypt_padded_mut::<Pkcs7>(&mut encrypted_bytes)
172 .map_err(|e| EnvError::DecryptionFailed(format!("Decryption failed: {}", e)))?;
173
174 String::from_utf8(decrypted_bytes.into())
175 .map_err(|e| EnvError::DecryptionFailed(format!("Invalid UTF-8 in decrypted data: {}", e)))
176}
177
178pub fn get_secure_var(name: &str, key_file: &str) -> Result<String, EnvError> {
192 let value = get_var(name)?;
193 if is_encrypted(&value) {
194 decrypt(&value, key_file)
195 } else {
196 Ok(value)
197 }
198}
199
200pub fn get_secure_var_or(name: &str, key_file: &str, default: &str) -> Result<String, EnvError> {
225 match get_var(name) {
226 Ok(val) => {
227 if is_encrypted(&val) {
228 decrypt(&val, key_file)
229 } else {
230 Ok(val)
231 }
232 }
233 Err(EnvError::VarError(_)) => Ok(default.to_string()),
234 Err(e) => Err(e),
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use std::fs::File;
242 use std::io::Write;
243 use temp_env::with_var;
244 use tempfile::tempdir;
245
246 const VALID_KEY_FILE_CONTENTS: &str = r#"salt=89A6A795C9CCECB5
247key=26D6EDD53A0AFA8FA1AA3FBCD2FFF2A0BF4809A4E04511F629FC732C2A42A8FC
248iv=472A3557ADDD2525AD4E555738636A67
249"#;
250
251 const ENCRYPTED_VAR_1: &str = "+encs+BCC9E963342C9CFEFB45093F3437A680";
252 const DECRYPTED_VAR_1: &str = "12345";
253
254 const ENCRYPTED_VAR_2: &str = "+encs+3510EEEF4163EB21C671FB5C57ADFCE2";
255 const DECRYPTED_VAR_2: &str = "/";
256
257 #[test]
258 fn test_get_env() {
259 with_var("TEST_VAR", Some("test_value"), || {
261 assert_eq!(get_var("TEST_VAR").unwrap(), "test_value");
262 assert_eq!(get_var_or("TEST_VAR", "default"), "test_value");
263 });
264
265 with_var::<_, &str, _, _>("NON_EXISTENT_VAR", None, || {
267 assert!(get_var("NON_EXISTENT_VAR").is_err());
268 assert_eq!(get_var_or("NON_EXISTENT_VAR", "default"), "default");
269 assert_eq!(get_var_or_empty("NON_EXISTENT_VAR"), "");
270 });
271 }
272
273 #[test]
274 fn test_is_encrypted() {
275 assert!(is_encrypted("+encs+1234567890ABCDEF"));
276 assert!(!is_encrypted("plain_text"));
277 assert!(!is_encrypted(""));
278 }
279
280 #[test]
281 fn test_parse_key_file() {
282 let dir = tempdir().unwrap();
283 let key_file_path = dir.path().join("key-file");
284
285 {
287 let mut file = File::create(&key_file_path).unwrap();
288 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
289 }
290
291 let result = parse_key_file(key_file_path.to_str().unwrap());
293 assert!(result.is_ok());
294 let (salt, key, iv) = result.unwrap();
295 assert_eq!(salt, "89A6A795C9CCECB5");
296 assert_eq!(
297 key,
298 "26D6EDD53A0AFA8FA1AA3FBCD2FFF2A0BF4809A4E04511F629FC732C2A42A8FC"
299 );
300 assert_eq!(iv, "472A3557ADDD2525AD4E555738636A67");
301
302 let invalid_key_file_path = dir.path().join("invalid-key-file");
304 let mut file = File::create(&invalid_key_file_path).unwrap();
305 writeln!(file, "salt=1234567890ABCDEF").unwrap();
306 writeln!(file, "invalid_line=something").unwrap();
307 writeln!(file, "iv=1234567890ABCDEF").unwrap();
308
309 let result = parse_key_file(invalid_key_file_path.to_str().unwrap());
310 assert!(result.is_err());
311
312 if let Err(EnvError::KeyFileFormatError(msg)) = result {
313 assert!(msg.contains("Unexpected content at line 2"));
314 } else {
315 panic!("Expected KeyFileFormatError");
316 }
317
318 let empty_key_file_path = dir.path().join("empty-key-file");
320 let _file = File::create(&empty_key_file_path).unwrap();
321
322 let result = parse_key_file(empty_key_file_path.to_str().unwrap());
323 assert!(result.is_err());
324
325 if let Err(EnvError::KeyFileFormatError(msg)) = result {
326 assert!(msg.contains("Missing salt in key file"));
328 } else {
329 panic!("Expected KeyFileFormatError");
330 }
331
332 let spaced_key_file_path = dir.path().join("spaced-key-file");
334 let mut file = File::create(&spaced_key_file_path).unwrap();
335 writeln!(file, "salt=1234567890ABCDEF").unwrap();
336 writeln!(file).unwrap();
337 writeln!(file, "key=1234567890ABCDEF1234567890ABCDEF").unwrap();
338 writeln!(file, " ").unwrap();
339 writeln!(file, "iv=1234567890ABCDEF").unwrap();
340
341 let result = parse_key_file(spaced_key_file_path.to_str().unwrap());
342 assert!(result.is_ok());
343 }
344
345 #[test]
346 fn test_decrypt_unencrypted() {
347 let dir = tempdir().unwrap();
348 let key_file_path = dir.path().join("key-file");
349 {
350 let mut file = File::create(&key_file_path).unwrap();
351 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
352 }
353 let result = decrypt("not-encrypted", key_file_path.to_str().unwrap());
355 assert!(result.is_ok());
356 assert_eq!(result.unwrap(), "not-encrypted");
357 }
358
359 #[test]
360 fn test_get_secure() {
361 let dir = tempdir().unwrap();
362 let key_file_path = dir.path().join("key-file");
363 {
364 let mut file = File::create(&key_file_path).unwrap();
365 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
366 }
367
368 with_var("PLAIN_VAR", Some("plain_text"), || {
370 let result = get_secure_var("PLAIN_VAR", key_file_path.to_str().unwrap());
371 assert!(result.is_ok());
372 assert_eq!(result.unwrap(), "plain_text");
373 });
374
375 with_var::<_, &str, _, _>("MISSING_VAR", None, || {
377 let result = get_secure_var("MISSING_VAR", key_file_path.to_str().unwrap());
378 assert!(result.is_err());
379 if let Err(e) = result {
380 assert!(matches!(e, EnvError::VarError(_)));
381 }
382 });
383
384 with_var("ENCRYPTED_VAR", Some(ENCRYPTED_VAR_1), || {
386 let result = get_secure_var("ENCRYPTED_VAR", key_file_path.to_str().unwrap());
387 assert!(result.is_ok());
388 assert_eq!(result.unwrap(), DECRYPTED_VAR_1.to_string());
389 });
390
391 with_var("ENCRYPTED_VAR", Some(ENCRYPTED_VAR_2), || {
393 let result = get_secure_var("ENCRYPTED_VAR", key_file_path.to_str().unwrap());
394 assert!(result.is_ok());
395 assert_eq!(result.unwrap(), DECRYPTED_VAR_2.to_string());
396 });
397 }
398
399 #[test]
400 fn test_decrypt_missing_keyfile() {
401 let result = decrypt(ENCRYPTED_VAR_1, "/non/existent/keyfile");
402 assert!(result.is_err());
403 assert!(matches!(result.unwrap_err(), EnvError::MissingKeyFile));
404 }
405
406 #[test]
407 fn test_decrypt_invalid_hex() {
408 let dir = tempdir().unwrap();
409 let key_file_path = dir.path().join("key-file");
410 {
411 let mut file = File::create(&key_file_path).unwrap();
412 writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
413 }
414 let result = decrypt("+encs+ZZ", key_file_path.to_str().unwrap());
415 assert!(result.is_err());
416 assert!(matches!(result.unwrap_err(), EnvError::DecryptionFailed(_)));
417 }
418}