1use crate::crypto;
2use crate::error::ConfigSecretsError;
3use aes_gcm::{Aes256Gcm, KeyInit};
4use bs58::{decode, encode};
5use rand::{RngCore, thread_rng};
6use std::fs;
7use std::path::Path;
8
9fn get_cipher(key: &str) -> Result<Aes256Gcm, ConfigSecretsError> {
10 let key_bytes = decode(key)
11 .into_vec()
12 .map_err(|e| ConfigSecretsError::InvalidEncoding(e.to_string()))?;
13
14 if key_bytes.len() != 32 {
15 return Err(ConfigSecretsError::InvalidKeyLength(key_bytes.len()));
16 }
17
18 Aes256Gcm::new_from_slice(&key_bytes)
19 .map_err(|_| ConfigSecretsError::InvalidKeyLength(key_bytes.len()))
20}
21
22pub fn generate_key() -> String {
24 let mut key = [0u8; 32];
25 thread_rng().fill_bytes(&mut key);
26 encode(&key).into_string()
27}
28
29pub fn decrypt_secrets(config: &str, key: &str) -> Result<String, ConfigSecretsError> {
31 let cipher = get_cipher(key)?;
32 let marker = "SECRET(";
33 let mut output = String::new();
34 let mut cursor = 0;
35
36 while let Some(start_offset) = config[cursor..].find(marker) {
37 let absolute_start = cursor + start_offset;
38 output.push_str(&config[cursor..absolute_start]);
39
40 match config[absolute_start..].find(')') {
41 Some(end_offset) => {
42 let absolute_end = absolute_start + end_offset;
43 let content_str = &config[absolute_start + marker.len()..absolute_end];
44
45 let ciphertext = decode(content_str)
46 .into_vec()
47 .map_err(|e| ConfigSecretsError::InvalidEncoding(e.to_string()))?;
48
49 let decrypted = crypto::decrypt(&ciphertext, &cipher)?;
50 output.push_str(&decrypted);
51
52 cursor = absolute_end + 1;
53 }
54 None => return Err(ConfigSecretsError::UnclosedBlock("SECRET".to_string())),
55 }
56 }
57 output.push_str(&config[cursor..]);
58 Ok(output)
59}
60
61pub fn decrypt_file<P: AsRef<Path>>(path: P, key: &str) -> Result<String, ConfigSecretsError> {
63 let content =
64 fs::read_to_string(path).map_err(|e| ConfigSecretsError::IoError(e.to_string()))?;
65 decrypt_secrets(&content, key)
66}
67
68pub fn encrypt_secrets(config: &str, key: &str) -> Result<String, ConfigSecretsError> {
70 let cipher = get_cipher(key)?;
71 let marker = "ENCRYPT(";
72 let mut output = String::new();
73 let mut cursor = 0;
74
75 while let Some(start_offset) = config[cursor..].find(marker) {
76 let absolute_start = cursor + start_offset;
77 output.push_str(&config[cursor..absolute_start]);
78
79 match config[absolute_start..].find(')') {
80 Some(end_offset) => {
81 let absolute_end = absolute_start + end_offset;
82 let content = &config[absolute_start + marker.len()..absolute_end];
83
84 let encrypted_bytes = crypto::encrypt(content, &cipher)?;
85 let encoded_str = encode(&encrypted_bytes).into_string();
86
87 output.push_str("SECRET(");
88 output.push_str(&encoded_str);
89 output.push(')');
90
91 cursor = absolute_end + 1;
92 }
93 None => return Err(ConfigSecretsError::UnclosedBlock("ENCRYPT".to_string())),
94 }
95 }
96 output.push_str(&config[cursor..]);
97 Ok(output)
98}
99
100pub fn encrypt_secrets_to_file<P: AsRef<Path>>(
102 config: &str,
103 output_path: P,
104 key: &str,
105) -> Result<(), ConfigSecretsError> {
106 let encrypted_content = encrypt_secrets(config, key)?;
107 fs::write(output_path, encrypted_content)
108 .map_err(|e| ConfigSecretsError::IoError(e.to_string()))
109}
110
111pub fn encrypt_file<P: AsRef<Path>, Q: AsRef<Path>>(
113 input_path: P,
114 output_path: Q,
115 key: &str,
116) -> Result<(), ConfigSecretsError> {
117 let content =
118 fs::read_to_string(input_path).map_err(|e| ConfigSecretsError::IoError(e.to_string()))?;
119 encrypt_secrets_to_file(&content, output_path, key)
120}
121
122pub fn encrypt_file_in_place<P: AsRef<Path>>(path: P, key: &str) -> Result<(), ConfigSecretsError> {
124 let content =
125 fs::read_to_string(&path).map_err(|e| ConfigSecretsError::IoError(e.to_string()))?;
126 let encrypted_content = encrypt_secrets(&content, key)?;
128 fs::write(path, encrypted_content).map_err(|e| ConfigSecretsError::IoError(e.to_string()))
129}
130
131pub fn encrypt_value(value: &str, key: &str) -> Result<String, ConfigSecretsError> {
133 let cipher = get_cipher(key)?;
134 let encrypted_bytes = crypto::encrypt(value, &cipher)?;
135 Ok(encode(&encrypted_bytes).into_string())
136}
137
138pub fn decrypt_value(input: &str, key: &str) -> Result<String, ConfigSecretsError> {
140 let cipher = get_cipher(key)?;
141
142 let inner_content = if input.trim().starts_with("SECRET(") && input.trim().ends_with(')') {
144 let trimmed = input.trim();
145 &trimmed["SECRET(".len()..trimmed.len() - 1]
146 } else {
147 input.trim()
148 };
149
150 let ciphertext = decode(inner_content)
151 .into_vec()
152 .map_err(|e| ConfigSecretsError::InvalidEncoding(e.to_string()))?;
153
154 crypto::decrypt(&ciphertext, &cipher)
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_encrypt_decrypt_value() {
163 let key = generate_key();
164 let secret = "my-password";
165
166 let encrypted = encrypt_value(secret, &key).unwrap();
168 assert!(!encrypted.starts_with("SECRET("));
170 assert!(encrypted.chars().all(|c| c.is_alphanumeric()));
171
172 let decrypted = decrypt_value(&encrypted, &key).unwrap();
174 assert_eq!(decrypted, secret);
175
176 let wrapped = format!("SECRET({})", encrypted);
178 let decrypted_wrapped = decrypt_value(&wrapped, &key).unwrap();
179 assert_eq!(decrypted_wrapped, secret);
180 }
181
182 #[test]
183 fn test_decrypt_value_invalid_encoding() {
184 let key = generate_key();
185 let err = decrypt_value("invalid-encoding!", &key).unwrap_err();
186 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
187 }
188
189 #[test]
190 fn test_generate_key() {
191 let key = generate_key();
192 assert!(!key.is_empty());
193 let decoded = decode(&key).into_vec();
195 assert!(decoded.is_ok());
196 assert_eq!(decoded.unwrap().len(), 32);
198 }
199
200 #[test]
201 fn test_encrypt_secrets() {
202 let key = &generate_key();
203 let input = r#"{"pass": "ENCRYPT(my_secret)"}"#;
204 let output = encrypt_secrets(input, key).unwrap();
205
206 assert!(output.contains("SECRET("));
207 assert!(!output.contains("ENCRYPT("));
208 assert!(!output.contains("my_secret")); }
210
211 #[test]
212 fn test_decrypt_secrets() {
213 let key = &generate_key();
214 let input = r#"{"pass": "ENCRYPT(my_secret)"}"#;
216 let encrypted = encrypt_secrets(input, key).unwrap();
217
218 let decrypted = decrypt_secrets(&encrypted, key).unwrap();
220 assert!(decrypted.contains(r#""pass": "my_secret""#));
221 }
222
223 #[test]
224 fn test_encrypt_secrets_to_file() {
225 let key = &generate_key();
226 let dir = std::env::temp_dir();
227 let path = dir.join("secrets_out.json");
228 let input = r#"key = "ENCRYPT(value)""#;
229
230 encrypt_secrets_to_file(input, &path, key).unwrap();
231
232 let content = fs::read_to_string(&path).unwrap();
233 assert!(content.contains("SECRET("));
234 assert!(!content.contains("value"));
235
236 let _ = fs::remove_file(path);
237 }
238
239 #[test]
240 fn test_encrypt_file() {
241 let key = &generate_key();
242 let dir = std::env::temp_dir();
243 let in_path = dir.join("in.json");
244 let out_path = dir.join("out.json");
245
246 fs::write(&in_path, r#"data: ENCRYPT(sensitive)"#).unwrap();
247
248 encrypt_file(&in_path, &out_path, key).unwrap();
249
250 let out_content = fs::read_to_string(&out_path).unwrap();
251 assert!(out_content.contains("SECRET("));
252
253 let _ = fs::remove_file(in_path);
254 let _ = fs::remove_file(out_path);
255 }
256
257 #[test]
258 fn test_encrypt_file_in_place() {
259 let key = &generate_key();
260 let dir = std::env::temp_dir();
261 let path = dir.join("inplace_test.yaml");
262
263 fs::write(&path, "pass: ENCRYPT(word)").unwrap();
264
265 encrypt_file_in_place(&path, key).unwrap();
266
267 let content = fs::read_to_string(&path).unwrap();
268 assert!(content.contains("SECRET("));
269
270 let decrypted = decrypt_secrets(&content, key).unwrap();
272 assert!(decrypted.contains("pass: word"));
273
274 let _ = fs::remove_file(path);
275 }
276
277 #[test]
278 fn test_decrypt_file() {
279 let key = &generate_key();
280 let dir = std::env::temp_dir();
281 let path = dir.join("decrypt_me.ini");
282
283 let content = r#"secret=ENCRYPT(hidden)"#;
284 let encrypted = encrypt_secrets(content, key).unwrap();
285 fs::write(&path, encrypted).unwrap();
286
287 let decrypted = decrypt_file(&path, key).unwrap();
288 assert!(decrypted.contains("secret=hidden"));
289
290 let _ = fs::remove_file(path);
291 }
292
293 #[test]
294 fn test_mixed_content() {
295 let key = &generate_key();
296 let input = r#"
297 visible = "true"
298 secret1 = "ENCRYPT(one)"
299 secret2 = "ENCRYPT(two)"
300 also_visible = 123
301 "#;
302
303 let encrypted = encrypt_secrets(input, key).unwrap();
304 let decrypted = decrypt_secrets(&encrypted, key).unwrap();
305
306 assert!(decrypted.contains(r#"visible = "true""#));
307 assert!(decrypted.contains(r#"secret1 = "one""#));
308 assert!(decrypted.contains(r#"secret2 = "two""#));
309 assert!(decrypted.contains(r#"also_visible = 123"#));
310 }
311
312 #[test]
313 fn test_invalid_key_encoding() {
314 assert!(matches!(
315 get_cipher("not-alphanumeric!"),
316 Err(ConfigSecretsError::InvalidEncoding(_))
317 ));
318 }
319
320 #[test]
321 fn test_invalid_key_length() {
322 let key = encode(&vec![0u8]).into_string();
324 assert!(matches!(
325 get_cipher(&key),
326 Err(ConfigSecretsError::InvalidKeyLength(1))
327 ));
328 }
329
330 #[test]
331 fn test_unclosed_encrypt_block() {
332 let key = &generate_key();
333 let input = "val = ENCRYPT(oops";
334 let err = encrypt_secrets(input, key).unwrap_err();
335 assert_eq!(
336 err,
337 ConfigSecretsError::UnclosedBlock("ENCRYPT".to_string())
338 );
339 }
340
341 #[test]
342 fn test_unclosed_secret_block() {
343 let key = &generate_key();
344 let input = "val = SECRET(oops";
345 let err = decrypt_secrets(input, key).unwrap_err();
346 assert_eq!(err, ConfigSecretsError::UnclosedBlock("SECRET".to_string()));
347 }
348
349 #[test]
350 fn test_invalid_encoding_in_secret() {
351 let key = &generate_key();
352 let input = "val = SECRET(!!!)";
353 let err = decrypt_secrets(input, key).unwrap_err();
354 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
355 }
356
357 #[test]
358 fn test_decrypt_secrets_invalid_key() {
359 let err = decrypt_secrets("SECRET(val)", "invalid-key").unwrap_err();
360 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
361 }
362
363 #[test]
364 fn test_decrypt_secrets_decryption_failed() {
365 let key = generate_key();
366 let input = "pass = SECRET(2222)";
369 let err = decrypt_secrets(input, &key).unwrap_err();
370 assert!(matches!(
371 err,
372 ConfigSecretsError::CiphertextTooShort
373 | ConfigSecretsError::DecryptionFailed
374 | ConfigSecretsError::InvalidEncoding(_)
375 ));
376 }
377
378 #[test]
379 fn test_encrypt_secrets_invalid_key() {
380 let err = encrypt_secrets("ENCRYPT(val)", "invalid-key").unwrap_err();
381 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
382 }
383
384 #[test]
385 fn test_file_funcs_invalid_key() {
386 let bad_key = "invalid-key";
387 let dir = std::env::temp_dir();
388 let path = dir.join("dummy_config.txt");
389 fs::write(&path, "content").unwrap();
390
391 let err = decrypt_file(&path, bad_key).unwrap_err();
393 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
394
395 let err = encrypt_secrets_to_file("content", &path, bad_key).unwrap_err();
397 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
398
399 let err = encrypt_file(&path, &path, bad_key).unwrap_err();
401 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
402
403 let err = encrypt_file_in_place(&path, bad_key).unwrap_err();
405 assert!(matches!(err, ConfigSecretsError::InvalidEncoding(_)));
406
407 let _ = fs::remove_file(path);
408 }
409}