geneos_toolkit/
env.rs

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
28/// Retrieves an environment variable's value.
29///
30/// # Arguments
31///
32/// * `name` - The name of the environment variable.
33///
34/// # Returns
35///
36/// The value of the environment variable if present, or an error.
37pub fn get_var(name: &str) -> Result<String, EnvError> {
38    Ok(env::var(name)?)
39}
40
41/// Retrieves an environment variable's value or returns a default if not set.
42///
43/// # Arguments
44///
45/// * `name` - The name of the environment variable.
46/// * `default` - The default value to return if the environment variable is not set.
47pub fn get_var_or(name: &str, default: &str) -> String {
48    env::var(name).unwrap_or_else(|_| default.to_string())
49}
50
51/// Retrieves an environment variable's value or returns an empty string if not set.
52pub fn get_var_or_empty(name: &str) -> String {
53    get_var_or(name, "")
54}
55
56/// Checks if a string slice is encrypted. Encrypted values start with "+encs+".
57///
58/// # Arguments
59///
60/// * `value` - The string slice to check.
61///
62/// # Returns
63///
64/// `true` if the value is encrypted, `false` otherwise.
65pub fn is_encrypted(value: &str) -> bool {
66    value.starts_with("+encs+")
67}
68
69/// Parses a key file to extract the salt, key, and initialization vector (IV).
70///
71/// The key file must contain three lines in the format:
72/// ```text
73/// salt=...
74/// key=...
75/// iv=...
76/// ```
77/// Empty or whitespace-only lines are ignored.
78///
79/// # Arguments
80///
81/// * `path` - The path to the key file.
82///
83/// # Returns
84///
85/// A tuple containing the salt, key, and IV as strings.
86fn 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; // 1-based line numbering for human readability
97
98        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
126/// Decrypts an encrypted Geneos environment variable.
127///
128/// This function assumes the encryption was performed using AES-256 in CBC mode with PKCS7 padding.
129/// Encrypted values must start with "+encs+" followed by the hexadecimal representation of the ciphertext.
130/// The provided key file must contain the salt, key, and IV in the expected format.
131/// If the input value is not encrypted (i.e. does not start with "+encs+"), it is returned unchanged.
132///
133/// # Arguments
134///
135/// * `value` - The encrypted string slice.
136/// * `key_file` - The path to the key file containing decryption parameters.
137///
138/// # Returns
139///
140/// The decrypted string on success, or an error if decryption fails.
141///
142/// # Example
143///
144/// ```no_run
145/// use geneos_toolkit::env;
146///
147/// let encrypted = "+encs+69B1E12815FA83702F0016B0E7FBD33B";
148/// let decrypted = env::decrypt(encrypted, "path/to/key-file").unwrap();
149/// println!("Decrypted value: {}", decrypted);
150/// ```
151pub 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
178/// Retrieves an environment variable and automatically decrypts it if needed.
179///
180/// If the environment variable's value starts with "+encs+", it is assumed to be encrypted and will
181/// be decrypted using the provided key file.
182///
183/// # Arguments
184///
185/// * `name` - The name of the environment variable.
186/// * `key_file` - The path to the key file containing decryption parameters.
187///
188/// # Returns
189///
190/// The plain text value of the environment variable on success, or an error.
191pub 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
200/// Retrieves a secure environment variable's value, returning a default if the variable is not set.
201///
202/// This function first attempts to get the environment variable named `name`.
203/// - If the variable is not present, it returns the provided `default` value.
204/// - If the variable is present and its value starts with `+encs+`, it is assumed to be encrypted
205///   and the function will attempt to decrypt it using the specified `key_file`.
206/// - If the variable is present and not encrypted, its value is returned as-is.
207///
208/// # Errors
209///
210/// If the variable is present but decryption fails or if any other error occurs (for example,
211/// an I/O error or a key file format error), the error is propagated.
212///
213/// # Example
214///
215/// ```no_run
216/// use geneos_toolkit::env::get_secure_var_or;
217///
218/// fn main() -> Result<(), Box<dyn std::error::Error>> {
219///     let value = get_secure_var_or("SECURE_ENV_VAR", "path/to/key_file", "default_value")?;
220///     println!("Value: {}", value);
221///     Ok(())
222/// }
223/// ```
224pub 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        // Test retrieving an existing variable.
260        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        // Test non-existent variable.
266        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        // Create a valid key file.
286        {
287            let mut file = File::create(&key_file_path).unwrap();
288            writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
289        }
290
291        // Valid parsing.
292        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        // Test invalid key file: unexpected content.
303        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        // Test empty file.
319        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            // An empty file will first fail on the test for a salt.
327            assert!(msg.contains("Missing salt in key file"));
328        } else {
329            panic!("Expected KeyFileFormatError");
330        }
331
332        // Test key file with empty/whitespace lines.
333        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        // Unencrypted values should pass through unchanged.
354        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        // Plain variable.
369        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        // Missing variable.
376        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        // Encrypted variable 1.
385        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        // Encrypted variable 2.
392        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}