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/// Checks if a string slice is encrypted. Encrypted values start with "+encs+".
52///
53/// # Arguments
54///
55/// * `value` - The string slice to check.
56///
57/// # Returns
58///
59/// `true` if the value is encrypted, `false` otherwise.
60pub fn is_encrypted(value: &str) -> bool {
61    value.starts_with("+encs+")
62}
63
64/// Parses a key file to extract the salt, key, and initialization vector (IV).
65///
66/// The key file must contain three lines in the format:
67/// ```text
68/// salt=...
69/// key=...
70/// iv=...
71/// ```
72/// Empty or whitespace-only lines are ignored.
73///
74/// # Arguments
75///
76/// * `path` - The path to the key file.
77///
78/// # Returns
79///
80/// A tuple containing the salt, key, and IV as strings.
81fn 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; // 1-based line numbering for human readability
92
93        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
121/// Decrypts an encrypted Geneos environment variable.
122///
123/// This function assumes the encryption was performed using AES-256 in CBC mode with PKCS7 padding.
124/// Encrypted values must start with "+encs+" followed by the hexadecimal representation of the ciphertext.
125/// The provided key file must contain the salt, key, and IV in the expected format.
126/// If the input value is not encrypted (i.e. does not start with "+encs+"), it is returned unchanged.
127///
128/// # Arguments
129///
130/// * `value` - The encrypted string slice.
131/// * `key_file` - The path to the key file containing decryption parameters.
132///
133/// # Returns
134///
135/// The decrypted string on success, or an error if decryption fails.
136///
137/// # Example
138///
139/// ```no_run
140/// use geneos_toolkit::env;
141///
142/// let encrypted = "+encs+69B1E12815FA83702F0016B0E7FBD33B";
143/// let decrypted = env::decrypt(encrypted, "path/to/key-file").unwrap();
144/// println!("Decrypted value: {}", decrypted);
145/// ```
146pub 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
173/// Retrieves an environment variable and automatically decrypts it if needed.
174///
175/// If the environment variable's value starts with "+encs+", it is assumed to be encrypted and will
176/// be decrypted using the provided key file.
177///
178/// # Arguments
179///
180/// * `name` - The name of the environment variable.
181/// * `key_file` - The path to the key file containing decryption parameters.
182///
183/// # Returns
184///
185/// The plain text value of the environment variable on success, or an error.
186pub 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
195/// Retrieves a secure environment variable's value, returning a default if the variable is not set.
196///
197/// This function first attempts to get the environment variable named `name`.
198/// - If the variable is not present, it returns the provided `default` value.
199/// - If the variable is present and its value starts with `+encs+`, it is assumed to be encrypted
200///   and the function will attempt to decrypt it using the specified `key_file`.
201/// - If the variable is present and not encrypted, its value is returned as-is.
202///
203/// # Errors
204///
205/// If the variable is present but decryption fails or if any other error occurs (for example,
206/// an I/O error or a key file format error), the error is propagated.
207///
208/// # Example
209///
210/// ```no_run
211/// use geneos_toolkit::env::get_secure_var_or;
212///
213/// fn main() -> Result<(), Box<dyn std::error::Error>> {
214///     let value = get_secure_var_or("SECURE_ENV_VAR", "path/to/key_file", "default_value")?;
215///     println!("Value: {}", value);
216///     Ok(())
217/// }
218/// ```
219pub 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        // Test retrieving an existing variable.
255        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        // Test non-existent variable.
261        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        // Create a valid key file.
280        {
281            let mut file = File::create(&key_file_path).unwrap();
282            writeln!(file, "{}", VALID_KEY_FILE_CONTENTS).unwrap();
283        }
284
285        // Valid parsing.
286        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        // Test invalid key file: unexpected content.
297        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        // Test empty file.
313        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            // An empty file will first fail on the test for a salt.
321            assert!(msg.contains("Missing salt in key file"));
322        } else {
323            panic!("Expected KeyFileFormatError");
324        }
325
326        // Test key file with empty/whitespace lines.
327        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        // Unencrypted values should pass through unchanged.
348        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        // Plain variable.
363        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        // Missing variable.
370        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        // Encrypted variable 1.
379        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        // Encrypted variable 2.
386        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}