pipedash_core/infrastructure/config/
encrypted_config.rs1use aes_gcm::{
2 aead::{
3 Aead,
4 KeyInit,
5 },
6 Aes256Gcm,
7 Nonce,
8};
9use base64::{
10 engine::general_purpose::STANDARD as BASE64,
11 Engine as _,
12};
13use serde::{
14 Deserialize,
15 Deserializer,
16 Serialize,
17 Serializer,
18};
19
20const CONFIG_ENCRYPTION_SALT: &[u8] = b"pipedash-config-encrypt-v1";
21
22#[derive(Debug, Clone, Default)]
23pub struct EncryptedValue {
24 pub plaintext: Option<String>,
25 pub encrypted_repr: Option<String>,
26}
27
28impl EncryptedValue {
29 pub fn from_plaintext(value: String) -> Self {
30 Self {
31 plaintext: if value.is_empty() { None } else { Some(value) },
32 encrypted_repr: None,
33 }
34 }
35
36 pub fn empty() -> Self {
37 Self {
38 plaintext: None,
39 encrypted_repr: None,
40 }
41 }
42
43 pub fn is_set(&self) -> bool {
44 self.plaintext.is_some() || self.encrypted_repr.is_some()
45 }
46
47 pub fn is_encrypted(&self) -> bool {
48 self.encrypted_repr.is_some()
49 }
50
51 pub fn get(&self, password: &str) -> Result<Option<String>, String> {
52 if let Some(ref plain) = self.plaintext {
53 return Ok(Some(plain.clone()));
54 }
55
56 if let Some(ref encrypted) = self.encrypted_repr {
57 return Self::decrypt_value(encrypted, password).map(Some);
58 }
59
60 Ok(None)
61 }
62
63 pub fn get_plaintext(&self) -> Option<&str> {
64 self.plaintext.as_deref()
65 }
66
67 pub fn encrypt(plaintext: &str, password: &str) -> Result<String, String> {
68 let key = derive_config_key(password);
69 let cipher = Aes256Gcm::new_from_slice(&key)
70 .map_err(|e| format!("Failed to create cipher: {}", e))?;
71
72 let nonce_bytes: [u8; 12] = rand::random();
73 let nonce = Nonce::from(nonce_bytes);
74
75 let ciphertext = cipher
76 .encrypt(&nonce, plaintext.as_bytes())
77 .map_err(|e| format!("Encryption failed: {}", e))?;
78
79 Ok(format!(
80 "enc:v1:{}:{}",
81 BASE64.encode(nonce_bytes),
82 BASE64.encode(ciphertext)
83 ))
84 }
85
86 pub fn encrypt_value(plaintext: &str, password: &str) -> Result<Self, String> {
87 let encrypted_repr = Self::encrypt(plaintext, password)?;
88 Ok(Self {
89 plaintext: None,
90 encrypted_repr: Some(encrypted_repr),
91 })
92 }
93
94 fn decrypt_value(encrypted: &str, password: &str) -> Result<String, String> {
95 let parts: Vec<&str> = encrypted.split(':').collect();
96 if parts.len() != 4 {
97 return Err("Invalid encrypted format: expected 4 parts".into());
98 }
99 if parts[0] != "enc" {
100 return Err("Invalid encrypted format: must start with 'enc'".into());
101 }
102 if parts[1] != "v1" {
103 return Err(format!(
104 "Unsupported encryption version: {}. Only v1 is supported.",
105 parts[1]
106 ));
107 }
108
109 let nonce_bytes = BASE64
110 .decode(parts[2])
111 .map_err(|e| format!("Invalid nonce encoding: {}", e))?;
112 let ciphertext = BASE64
113 .decode(parts[3])
114 .map_err(|e| format!("Invalid ciphertext encoding: {}", e))?;
115
116 if nonce_bytes.len() != 12 {
117 return Err(format!(
118 "Invalid nonce length: expected 12 bytes, got {}",
119 nonce_bytes.len()
120 ));
121 }
122
123 let key = derive_config_key(password);
124 let cipher = Aes256Gcm::new_from_slice(&key)
125 .map_err(|e| format!("Failed to create cipher: {}", e))?;
126
127 let nonce_array: [u8; 12] = nonce_bytes
128 .try_into()
129 .map_err(|_| "Invalid nonce length".to_string())?;
130 let nonce = Nonce::from(nonce_array);
131
132 let plaintext = cipher
133 .decrypt(&nonce, ciphertext.as_ref())
134 .map_err(|_| "Decryption failed - wrong password or corrupted data".to_string())?;
135
136 String::from_utf8(plaintext).map_err(|e| format!("Invalid UTF-8 in decrypted value: {}", e))
137 }
138}
139
140fn derive_config_key(password: &str) -> [u8; 32] {
141 use argon2::{
142 Argon2,
143 ParamsBuilder,
144 };
145
146 let mut output = [0u8; 32];
147
148 let params = ParamsBuilder::new()
149 .m_cost(4096) .t_cost(1) .p_cost(1) .output_len(32)
153 .build()
154 .expect("Invalid Argon2 parameters");
155
156 let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
157
158 argon2
159 .hash_password_into(password.as_bytes(), CONFIG_ENCRYPTION_SALT, &mut output)
160 .expect("Failed to derive config encryption key");
161
162 output
163}
164
165impl Serialize for EncryptedValue {
166 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
167 where
168 S: Serializer,
169 {
170 if let Some(ref encrypted) = self.encrypted_repr {
171 return serializer.serialize_str(encrypted);
172 }
173 match &self.plaintext {
174 Some(s) => serializer.serialize_str(s),
175 None => serializer.serialize_str(""),
176 }
177 }
178}
179
180impl<'de> Deserialize<'de> for EncryptedValue {
181 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
182 where
183 D: Deserializer<'de>,
184 {
185 let s = String::deserialize(deserializer)?;
186
187 if s.starts_with("enc:v1:") {
188 Ok(Self {
189 plaintext: None,
190 encrypted_repr: Some(s),
191 })
192 } else {
193 Ok(Self {
194 plaintext: if s.is_empty() { None } else { Some(s) },
195 encrypted_repr: None,
196 })
197 }
198 }
199}
200
201pub fn is_encrypted_format(s: &str) -> bool {
202 s.starts_with("enc:v1:")
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_encrypt_decrypt() {
211 let plaintext = "postgres://user:pass@localhost/db";
212 let password = "my-secure-password";
213
214 let encrypted = EncryptedValue::encrypt(plaintext, password).unwrap();
215 assert!(encrypted.starts_with("enc:v1:"));
216
217 let value = EncryptedValue {
218 plaintext: None,
219 encrypted_repr: Some(encrypted),
220 };
221
222 let decrypted = value.get(password).unwrap().unwrap();
223 assert_eq!(decrypted, plaintext);
224 }
225
226 #[test]
227 fn test_wrong_password() {
228 let plaintext = "secret-value";
229 let encrypted = EncryptedValue::encrypt(plaintext, "correct-password").unwrap();
230
231 let value = EncryptedValue {
232 plaintext: None,
233 encrypted_repr: Some(encrypted),
234 };
235
236 let result = value.get("wrong-password");
237 assert!(result.is_err());
238 }
239
240 #[test]
241 fn test_plaintext_passthrough() {
242 let value = EncryptedValue::from_plaintext("plain-value".to_string());
243 let result = value.get("any-password").unwrap().unwrap();
244 assert_eq!(result, "plain-value");
245 }
246
247 #[test]
248 fn test_empty_value() {
249 let value = EncryptedValue::empty();
250 assert!(!value.is_set());
251 assert!(value.get("password").unwrap().is_none());
252 }
253
254 #[test]
255 fn test_serde_plaintext() {
256 let value = EncryptedValue::from_plaintext("my-value".to_string());
257 let serialized = serde_json::to_string(&value).unwrap();
258 assert_eq!(serialized, "\"my-value\"");
259
260 let deserialized: EncryptedValue = serde_json::from_str(&serialized).unwrap();
261 assert_eq!(deserialized.plaintext.as_deref(), Some("my-value"));
262 }
263
264 #[test]
265 fn test_serde_encrypted() {
266 let encrypted_str = "enc:v1:AAAAAAAAAAAAAAAA:BBBBBBBB";
267 let serialized = format!("\"{}\"", encrypted_str);
268
269 let deserialized: EncryptedValue = serde_json::from_str(&serialized).unwrap();
270 assert!(deserialized.is_encrypted());
271 assert_eq!(deserialized.encrypted_repr.as_deref(), Some(encrypted_str));
272 }
273
274 #[test]
275 fn test_is_encrypted_format() {
276 assert!(is_encrypted_format("enc:v1:nonce:ciphertext"));
277 assert!(!is_encrypted_format("plain-value"));
278 assert!(!is_encrypted_format("enc:v2:something"));
279 }
280
281 #[test]
282 fn test_encrypt_value() {
283 let value = EncryptedValue::encrypt_value("my-secret", "my-password").unwrap();
284 assert!(value.is_encrypted());
285 assert!(value.encrypted_repr.is_some());
286 assert!(value.plaintext.is_none());
287
288 let decrypted = value.get("my-password").unwrap().unwrap();
289 assert_eq!(decrypted, "my-secret");
290 }
291}