openzeppelin_relayer/utils/serde/
repository_encryption.rs

1//! Helper functions to serialize and deserialize secrets as encrypted base64 for storage.
2//!
3//! This module provides serde serializers/deserializers that automatically encrypt
4//! sensitive data when storing to Redis and decrypt when retrieving.
5//!
6//! ## AAD (Additional Authenticated Data)
7//!
8//! **Serialization**: Always uses AAD (via `EncryptionContext`) to bind the ciphertext
9//! to its storage location. The `EncryptionContext`
10//! must be set before serialization.
11//!
12//! **Deserialization**: Supports both legacy (v1, no AAD) and new (v2, with AAD)
13//! encrypted data for backwards compatibility. If `EncryptionContext` is set, it will
14//! be used for v2 decryption; v1 data can be decrypted without context.
15
16use secrets::SecretVec;
17use serde::{Deserialize, Deserializer, Serializer};
18
19use crate::{
20    models::SecretString,
21    utils::{
22        base64_decode, base64_encode, decrypt_sensitive_field_auto,
23        encrypt_sensitive_field_with_aad,
24    },
25};
26
27/// Helper function to serialize secrets as encrypted base64 for storage.
28///
29/// Uses AAD from `EncryptionContext` to bind the ciphertext to its storage location.
30/// The `EncryptionContext` must be set before calling this function.
31pub fn serialize_secret_vec<S>(secret: &SecretVec<u8>, serializer: S) -> Result<S::Ok, S::Error>
32where
33    S: Serializer,
34{
35    // First encode the raw secret as base64
36    let base64 = base64_encode(secret.borrow().as_ref());
37
38    // Then encrypt the base64 string for secure storage with AAD
39    let encrypted = encrypt_sensitive_field_with_aad(&base64)
40        .map_err(|e| serde::ser::Error::custom(format!("Encryption failed: {e}")))?;
41
42    serializer.serialize_str(&encrypted)
43}
44
45/// Helper function to deserialize secrets from encrypted base64 storage.
46///
47/// Supports both legacy (v1, no AAD) and new (v2, with AAD) encrypted data
48/// for backwards compatibility. Uses `EncryptionContext` for v2 decryption if set.
49pub fn deserialize_secret_vec<'de, D>(deserializer: D) -> Result<SecretVec<u8>, D::Error>
50where
51    D: Deserializer<'de>,
52{
53    let encrypted_str = String::deserialize(deserializer)?;
54
55    // Decrypt using auto-detection to handle both v1 (legacy) and v2 (with AAD)
56    let base64_str = decrypt_sensitive_field_auto(&encrypted_str)
57        .map_err(|e| serde::de::Error::custom(format!("Decryption failed: {e}")))?;
58
59    // Then decode the base64 to get the raw secret bytes
60    let decoded = base64_decode(&base64_str)
61        .map_err(|e| serde::de::Error::custom(format!("Invalid base64: {e}")))?;
62
63    Ok(SecretVec::new(decoded.len(), |v| {
64        v.copy_from_slice(&decoded)
65    }))
66}
67
68/// Helper function to serialize secrets as encrypted base64 for storage.
69///
70/// Uses AAD from `EncryptionContext` to bind the ciphertext to its storage location.
71/// The `EncryptionContext` must be set before calling this function.
72pub fn serialize_secret_string<S>(secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
73where
74    S: Serializer,
75{
76    let secret_content = secret.to_str();
77
78    // Encrypt with AAD (already returns base64-encoded string)
79    let encrypted = encrypt_sensitive_field_with_aad(&secret_content)
80        .map_err(|e| serde::ser::Error::custom(format!("Encryption failed: {e}")))?;
81
82    serializer.serialize_str(&encrypted)
83}
84
85/// Helper function to deserialize secrets from encrypted base64 storage.
86///
87/// Supports three formats for backwards compatibility:
88/// 1. Encrypted format (v2): base64(encrypted data with AAD)
89/// 2. Legacy encrypted format (v1): base64(encrypted data without AAD)
90/// 3. Plain text format: unencrypted plain string (for data stored before encryption was added)
91///
92/// Uses `EncryptionContext` for v2 decryption if set.
93pub fn deserialize_secret_string<'de, D>(deserializer: D) -> Result<SecretString, D::Error>
94where
95    D: Deserializer<'de>,
96{
97    let value_str = String::deserialize(deserializer)?;
98
99    // First try the direct decrypt path (single-layer base64 payloads).
100    if let Ok(decrypted) = decrypt_sensitive_field_auto(&value_str) {
101        return Ok(SecretString::new(&decrypted));
102    }
103
104    // Fall back to treating the original string as plain text.
105    Ok(SecretString::new(&value_str))
106}
107
108/// Helper function to serialize optional secrets as encrypted base64 for storage.
109///
110/// Uses AAD from `EncryptionContext` to bind the ciphertext to its storage location.
111/// The `EncryptionContext` must be set before calling this function.
112pub fn serialize_option_secret_string<S>(
113    secret: &Option<SecretString>,
114    serializer: S,
115) -> Result<S::Ok, S::Error>
116where
117    S: Serializer,
118{
119    match secret {
120        Some(secret_string) => {
121            let secret_content = secret_string.to_str();
122
123            // Encrypt with AAD (already returns base64-encoded string)
124            let encrypted = encrypt_sensitive_field_with_aad(&secret_content)
125                .map_err(|e| serde::ser::Error::custom(format!("Encryption failed: {e}")))?;
126
127            serializer.serialize_some(&encrypted)
128        }
129        None => serializer.serialize_none(),
130    }
131}
132
133/// Helper function to deserialize optional secrets from encrypted base64 storage.
134///
135/// Supports three formats for backwards compatibility:
136/// 1. Encrypted format (v2): base64(encrypted data with AAD)
137/// 2. Legacy encrypted format (v1): base64(encrypted data without AAD)
138/// 3. Plain text format: unencrypted plain string (for data stored before encryption was added)
139///
140/// Uses `EncryptionContext` for v2 decryption if set.
141pub fn deserialize_option_secret_string<'de, D>(
142    deserializer: D,
143) -> Result<Option<SecretString>, D::Error>
144where
145    D: Deserializer<'de>,
146{
147    let opt_value_str: Option<String> = Option::deserialize(deserializer)?;
148
149    match opt_value_str {
150        Some(value_str) => {
151            // First try the direct decrypt path (single-layer base64 payloads).
152            if let Ok(decrypted) = decrypt_sensitive_field_auto(&value_str) {
153                return Ok(Some(SecretString::new(&decrypted)));
154            }
155
156            // Fall back to treating the original string as plain text.
157            Ok(Some(SecretString::new(&value_str)))
158        }
159        None => Ok(None),
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::utils::EncryptionContext;
167    use secrets::SecretVec;
168    use serde_json;
169
170    fn test_aad() -> String {
171        "test-context".to_string()
172    }
173
174    #[test]
175    fn test_serialize_deserialize_secret_string() {
176        let secret = SecretString::new("test-secret-content");
177
178        // Create a test struct that uses the secret string serialization
179        #[derive(serde::Serialize, serde::Deserialize)]
180        struct TestStruct {
181            #[serde(
182                serialize_with = "serialize_secret_string",
183                deserialize_with = "deserialize_secret_string"
184            )]
185            secret: SecretString,
186        }
187
188        let test_struct = TestStruct {
189            secret: secret.clone(),
190        };
191
192        // Test serialization with AAD context
193        let serialized = EncryptionContext::with_aad_sync(test_aad(), || {
194            serde_json::to_string(&test_struct).unwrap()
195        });
196
197        // Test deserialization with AAD context
198        let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
199            serde_json::from_str(&serialized).unwrap()
200        });
201
202        // Verify content matches
203        assert_eq!(secret.to_str(), deserialized.secret.to_str());
204    }
205
206    #[test]
207    fn test_serialize_deserialize_secret_vec() {
208        let original_data = vec![1, 2, 3, 4, 5];
209        let secret = SecretVec::new(original_data.len(), |v| v.copy_from_slice(&original_data));
210
211        // Create a test struct that uses the secret vec serialization
212        #[derive(serde::Serialize, serde::Deserialize)]
213        struct TestStruct {
214            #[serde(
215                serialize_with = "serialize_secret_vec",
216                deserialize_with = "deserialize_secret_vec"
217            )]
218            secret_data: SecretVec<u8>,
219        }
220
221        let test_struct = TestStruct {
222            secret_data: secret,
223        };
224
225        // Test serialization with AAD context
226        let serialized = EncryptionContext::with_aad_sync(test_aad(), || {
227            serde_json::to_string(&test_struct).unwrap()
228        });
229
230        // Test deserialization with AAD context
231        let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
232            serde_json::from_str(&serialized).unwrap()
233        });
234
235        // Verify content matches
236        let original_borrowed = original_data;
237        let deserialized_borrowed = deserialized.secret_data.borrow();
238        assert_eq!(original_borrowed, *deserialized_borrowed);
239    }
240
241    #[test]
242    fn test_serialize_deserialize_option_secret_string_some() {
243        let secret = SecretString::new("test-optional-secret");
244
245        // Create a test struct that uses the option secret string serialization
246        #[derive(serde::Serialize, serde::Deserialize)]
247        struct TestStruct {
248            #[serde(
249                serialize_with = "serialize_option_secret_string",
250                deserialize_with = "deserialize_option_secret_string"
251            )]
252            optional_secret: Option<SecretString>,
253        }
254
255        let test_struct = TestStruct {
256            optional_secret: Some(secret.clone()),
257        };
258
259        // Test serialization with AAD context
260        let serialized = EncryptionContext::with_aad_sync(test_aad(), || {
261            serde_json::to_string(&test_struct).unwrap()
262        });
263
264        // Test deserialization with AAD context
265        let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
266            serde_json::from_str(&serialized).unwrap()
267        });
268
269        // Verify content matches
270        assert!(deserialized.optional_secret.is_some());
271        assert_eq!(
272            secret.to_str(),
273            deserialized.optional_secret.unwrap().to_str()
274        );
275    }
276
277    #[test]
278    fn test_serialize_deserialize_option_secret_string_none() {
279        let secret: Option<SecretString> = None;
280
281        // Create a test struct that uses the option secret string serialization
282        #[derive(serde::Serialize, serde::Deserialize)]
283        struct TestStruct {
284            #[serde(
285                serialize_with = "serialize_option_secret_string",
286                deserialize_with = "deserialize_option_secret_string"
287            )]
288            optional_secret: Option<SecretString>,
289        }
290
291        let test_struct = TestStruct {
292            optional_secret: secret,
293        };
294
295        // Test serialization with AAD context
296        let serialized = EncryptionContext::with_aad_sync(test_aad(), || {
297            serde_json::to_string(&test_struct).unwrap()
298        });
299
300        // Test deserialization with AAD context
301        let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
302            serde_json::from_str(&serialized).unwrap()
303        });
304
305        // Verify it's None
306        assert!(deserialized.optional_secret.is_none());
307    }
308
309    #[test]
310    fn test_round_trip_secret_string() {
311        let original = SecretString::new("complex-secret-with-special-chars-!@#$%^&*()");
312
313        #[derive(serde::Serialize, serde::Deserialize)]
314        struct TestStruct {
315            #[serde(
316                serialize_with = "serialize_secret_string",
317                deserialize_with = "deserialize_secret_string"
318            )]
319            secret: SecretString,
320        }
321
322        let test_struct = TestStruct {
323            secret: original.clone(),
324        };
325
326        // Serialize to JSON with AAD context
327        let json = EncryptionContext::with_aad_sync(test_aad(), || {
328            serde_json::to_string(&test_struct).unwrap()
329        });
330
331        // Deserialize back with AAD context
332        let deserialized: TestStruct =
333            EncryptionContext::with_aad_sync(test_aad(), || serde_json::from_str(&json).unwrap());
334
335        // Verify the content is identical
336        assert_eq!(original.to_str(), deserialized.secret.to_str());
337    }
338
339    #[test]
340    fn test_round_trip_option_secret_string_with_multiple_values() {
341        let test_cases = vec![
342            Some(SecretString::new("test1")),
343            None,
344            Some(SecretString::new("")),
345            Some(SecretString::new("test-with-unicode-🔐")),
346            Some(SecretString::new(&"very-long-secret-".repeat(100))),
347        ];
348
349        #[derive(serde::Serialize, serde::Deserialize)]
350        struct TestStruct {
351            #[serde(
352                serialize_with = "serialize_option_secret_string",
353                deserialize_with = "deserialize_option_secret_string"
354            )]
355            optional_secret: Option<SecretString>,
356        }
357
358        for test_case in test_cases {
359            let test_struct = TestStruct {
360                optional_secret: test_case.clone(),
361            };
362
363            // Serialize to JSON with AAD context
364            let json = EncryptionContext::with_aad_sync(test_aad(), || {
365                serde_json::to_string(&test_struct).unwrap()
366            });
367
368            // Deserialize back with AAD context
369            let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
370                serde_json::from_str(&json).unwrap()
371            });
372
373            // Verify the content matches
374            match (test_case, deserialized.optional_secret) {
375                (Some(original), Some(deserialized_secret)) => {
376                    assert_eq!(original.to_str(), deserialized_secret.to_str());
377                }
378                (None, None) => {
379                    // Both are None, this is correct
380                }
381                _ => panic!("Mismatch between original and deserialized optional secret"),
382            }
383        }
384    }
385
386    #[test]
387    fn test_serialized_content_is_encrypted() {
388        let secret = SecretString::new("plaintext-secret");
389
390        #[derive(serde::Serialize)]
391        struct TestStruct {
392            #[serde(serialize_with = "serialize_secret_string")]
393            secret: SecretString,
394        }
395
396        let test_struct = TestStruct { secret };
397
398        // Serialize with AAD context
399        let json = EncryptionContext::with_aad_sync(test_aad(), || {
400            serde_json::to_string(&test_struct).unwrap()
401        });
402
403        // The serialized JSON should not contain the plaintext secret
404        assert!(!json.contains("plaintext-secret"));
405
406        // It should be base64 encoded (contains only valid base64 characters)
407        let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
408        let serialized_secret = json_value["secret"].as_str().unwrap();
409
410        // Verify it's valid base64 by attempting to decode it
411        assert!(base64_decode(serialized_secret).is_ok());
412    }
413
414    #[test]
415    fn test_serialized_option_content_when_some() {
416        let secret = Some(SecretString::new("plaintext-secret"));
417
418        #[derive(serde::Serialize)]
419        struct TestStruct {
420            #[serde(serialize_with = "serialize_option_secret_string")]
421            optional_secret: Option<SecretString>,
422        }
423
424        let test_struct = TestStruct {
425            optional_secret: secret,
426        };
427
428        // Serialize with AAD context
429        let json = EncryptionContext::with_aad_sync(test_aad(), || {
430            serde_json::to_string(&test_struct).unwrap()
431        });
432
433        // The serialized JSON should not contain the plaintext secret
434        assert!(!json.contains("plaintext-secret"));
435
436        // Parse the JSON to verify structure
437        let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
438        assert!(json_value["optional_secret"].is_string());
439
440        let serialized_secret = json_value["optional_secret"].as_str().unwrap();
441        // Verify it's valid base64
442        assert!(base64_decode(serialized_secret).is_ok());
443    }
444
445    #[test]
446    fn test_serialized_option_content_when_none() {
447        let secret: Option<SecretString> = None;
448
449        #[derive(serde::Serialize)]
450        struct TestStruct {
451            #[serde(serialize_with = "serialize_option_secret_string")]
452            optional_secret: Option<SecretString>,
453        }
454
455        let test_struct = TestStruct {
456            optional_secret: secret,
457        };
458
459        // Serialize with AAD context
460        let json = EncryptionContext::with_aad_sync(test_aad(), || {
461            serde_json::to_string(&test_struct).unwrap()
462        });
463
464        // Parse the JSON to verify structure
465        let json_value: serde_json::Value = serde_json::from_str(&json).unwrap();
466        assert!(json_value["optional_secret"].is_null());
467    }
468
469    #[test]
470    fn test_serialize_fails_without_context() {
471        let secret = SecretString::new("test-secret");
472
473        #[derive(serde::Serialize)]
474        struct TestStruct {
475            #[serde(serialize_with = "serialize_secret_string")]
476            secret: SecretString,
477        }
478
479        let test_struct = TestStruct { secret };
480
481        // Should fail without AAD context
482        let result = serde_json::to_string(&test_struct);
483        assert!(result.is_err());
484        assert!(result
485            .unwrap_err()
486            .to_string()
487            .contains("EncryptionContext not set"));
488    }
489
490    #[test]
491    fn test_deserialize_plain_text_backward_compatibility() {
492        // Test deserializing plain text that was stored before encryption was added
493        #[derive(serde::Deserialize)]
494        struct TestStruct {
495            #[serde(deserialize_with = "deserialize_secret_string")]
496            project_id: SecretString,
497            #[serde(deserialize_with = "deserialize_secret_string")]
498            location: SecretString,
499        }
500
501        // Simulate old JSON format with plain strings (including hyphens)
502        let old_json = r#"{"project_id":"my-project-123","location":"us-central1"}"#;
503
504        // Should successfully deserialize plain text as SecretString
505        let deserialized: TestStruct = serde_json::from_str(old_json).unwrap();
506
507        assert_eq!(*deserialized.project_id.to_str(), "my-project-123");
508        assert_eq!(*deserialized.location.to_str(), "us-central1");
509    }
510
511    #[test]
512    fn test_deserialize_option_plain_text_backward_compatibility() {
513        // Test deserializing optional plain text
514        #[derive(serde::Deserialize)]
515        struct TestStruct {
516            #[serde(deserialize_with = "deserialize_option_secret_string")]
517            field: Option<SecretString>,
518        }
519
520        // Plain text
521        let json = r#"{"field":"plain-value-with-hyphen"}"#;
522        let deserialized: TestStruct = serde_json::from_str(json).unwrap();
523        assert_eq!(
524            *deserialized.field.unwrap().to_str(),
525            "plain-value-with-hyphen"
526        );
527
528        // None
529        let json = r#"{"field":null}"#;
530        let deserialized: TestStruct = serde_json::from_str(json).unwrap();
531        assert!(deserialized.field.is_none());
532    }
533
534    #[test]
535    fn test_mixed_format_deserialization() {
536        // Test deserializing a mix of encrypted and plain text fields
537        #[derive(serde::Deserialize)]
538        struct TestStruct {
539            #[serde(deserialize_with = "deserialize_secret_string")]
540            plain_field: SecretString,
541            #[serde(deserialize_with = "deserialize_secret_string")]
542            encrypted_field: SecretString,
543        }
544
545        // First, create an encrypted value
546        let encrypted_value = EncryptionContext::with_aad_sync(test_aad(), || {
547            #[derive(serde::Serialize)]
548            struct TempStruct {
549                #[serde(serialize_with = "serialize_secret_string")]
550                field: SecretString,
551            }
552            let temp = TempStruct {
553                field: SecretString::new("encrypted-content"),
554            };
555            serde_json::to_string(&temp).unwrap()
556        });
557
558        let encrypted_value_parsed: serde_json::Value =
559            serde_json::from_str(&encrypted_value).unwrap();
560        let encrypted_field_value = encrypted_value_parsed["field"].as_str().unwrap();
561
562        // Create JSON with both plain and encrypted fields
563        let mixed_json = format!(
564            r#"{{"plain_field":"plain-text-value","encrypted_field":"{encrypted_field_value}"}}"#
565        );
566
567        // Should successfully deserialize both
568        let deserialized: TestStruct = EncryptionContext::with_aad_sync(test_aad(), || {
569            serde_json::from_str(&mixed_json).unwrap()
570        });
571
572        assert_eq!(*deserialized.plain_field.to_str(), "plain-text-value");
573        assert_eq!(*deserialized.encrypted_field.to_str(), "encrypted-content");
574    }
575}