openzeppelin_relayer/utils/
encryption_context.rs

1//! Task-local context for AAD (Additional Authenticated Data) in encryption operations.
2//!
3//! This module provides a mechanism to pass AAD context through the async task stack
4//! without explicitly threading it through function signatures. This is particularly
5//! useful for serde serializers/deserializers that don't have access to external context.
6//!
7//! # Why `task_local!` instead of `thread_local!`?
8//!
9//! Async tasks can move between threads during execution. Using `task_local!` ensures
10//! the context follows the task, not the thread, making it correct for async Rust.
11//!
12//! # Usage
13//!
14//! ```ignore
15//! // In repository operations, wrap serialization with AAD context:
16//! let key = self.signer_key(&signer.id);
17//! let serialized = EncryptionContext::with_aad(key.clone(), || async {
18//!     serde_json::to_string(&signer)
19//! }).await?;
20//! ```
21
22tokio::task_local! {
23    static AAD_CONTEXT: String;
24}
25
26/// Provides task-local context for AAD during encryption/decryption operations.
27///
28/// This allows passing the Redis key (or other storage identifier) as AAD
29/// to encryption operations without modifying function signatures throughout
30/// the codebase.
31pub struct EncryptionContext;
32
33impl EncryptionContext {
34    /// Runs a future with AAD context set.
35    ///
36    /// The AAD context will be available to any code running within the future
37    /// via `EncryptionContext::get()`.
38    ///
39    /// # Arguments
40    ///
41    /// * `aad` - The AAD string (typically the Redis key) to use for encryption
42    /// * `f` - A closure that returns a Future to execute with the AAD context
43    ///
44    /// # Example
45    ///
46    /// ```ignore
47    /// let key = "oz-relayer:signer:my-signer-id";
48    /// let result = EncryptionContext::with_aad(key.to_string(), || async {
49    ///     // Within this block, EncryptionContext::get() returns Some(key)
50    ///     serialize_signer(&signer)
51    /// }).await;
52    /// ```
53    pub async fn with_aad<F, Fut, R>(aad: String, f: F) -> R
54    where
55        F: FnOnce() -> Fut,
56        Fut: std::future::Future<Output = R>,
57    {
58        AAD_CONTEXT.scope(aad, f()).await
59    }
60
61    /// Runs a synchronous closure with AAD context set.
62    ///
63    /// This is useful when the encryption/decryption operation is synchronous
64    /// (like serde serialization) but needs access to the AAD context.
65    /// Unlike `with_aad`, this doesn't require the closure to return a Future,
66    /// avoiding Send/Sync issues with captured references.
67    ///
68    /// # Arguments
69    ///
70    /// * `aad` - The AAD string (typically the Redis key) to use for encryption
71    /// * `f` - A synchronous closure to execute with the AAD context
72    ///
73    /// # Example
74    ///
75    /// ```ignore
76    /// let key = "oz-relayer:signer:my-signer-id";
77    /// let result = EncryptionContext::with_aad_sync(key.to_string(), || {
78    ///     // Within this block, EncryptionContext::get() returns Some(key)
79    ///     serde_json::to_string(&signer)
80    /// })?;
81    /// ```
82    pub fn with_aad_sync<F, R>(aad: String, f: F) -> R
83    where
84        F: FnOnce() -> R,
85    {
86        AAD_CONTEXT.sync_scope(aad, f)
87    }
88
89    /// Gets the current AAD context, if set.
90    ///
91    /// Returns `Some(aad)` if called within a `with_aad` scope,
92    /// otherwise returns `None`.
93    ///
94    /// This is safe to call from synchronous code (like serde serializers)
95    /// as long as it's running within an async task that has set the context.
96    pub fn get() -> Option<String> {
97        AAD_CONTEXT.try_with(|s| s.clone()).ok()
98    }
99
100    /// Checks if AAD context is currently set.
101    pub fn is_set() -> bool {
102        AAD_CONTEXT.try_with(|_| ()).is_ok()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[tokio::test]
111    async fn test_with_aad_provides_context() {
112        let aad = "oz-relayer:signer:test-id".to_string();
113
114        let result =
115            EncryptionContext::with_aad(aad.clone(), || async { EncryptionContext::get() }).await;
116
117        assert_eq!(result, Some(aad));
118    }
119
120    #[tokio::test]
121    async fn test_get_returns_none_outside_scope() {
122        // Outside of with_aad scope, get() should return None
123        let result = EncryptionContext::get();
124        assert_eq!(result, None);
125    }
126
127    #[tokio::test]
128    async fn test_is_set_returns_correct_value() {
129        // Outside scope
130        assert!(!EncryptionContext::is_set());
131
132        // Inside scope
133        EncryptionContext::with_aad("test".to_string(), || async {
134            assert!(EncryptionContext::is_set());
135        })
136        .await;
137
138        // Back outside
139        assert!(!EncryptionContext::is_set());
140    }
141
142    #[tokio::test]
143    async fn test_nested_contexts() {
144        let outer_aad = "outer-key".to_string();
145        let inner_aad = "inner-key".to_string();
146
147        EncryptionContext::with_aad(outer_aad.clone(), || async {
148            // Outer context
149            assert_eq!(EncryptionContext::get(), Some(outer_aad.clone()));
150
151            // Nested context shadows outer
152            EncryptionContext::with_aad(inner_aad.clone(), || async {
153                assert_eq!(EncryptionContext::get(), Some(inner_aad.clone()));
154            })
155            .await;
156
157            // Back to outer context
158            assert_eq!(EncryptionContext::get(), Some(outer_aad));
159        })
160        .await;
161    }
162
163    #[tokio::test]
164    async fn test_context_works_with_sync_code() {
165        let aad = "sync-test-key".to_string();
166
167        EncryptionContext::with_aad(aad.clone(), || async {
168            // Simulate a sync function call within async context
169            fn sync_function() -> Option<String> {
170                EncryptionContext::get()
171            }
172
173            let result = sync_function();
174            assert_eq!(result, Some(aad));
175        })
176        .await;
177    }
178
179    #[tokio::test]
180    async fn test_with_aad_sync_provides_context() {
181        let aad = "oz-relayer:signer:sync-test-id".to_string();
182
183        // with_aad_sync should set context for synchronous code
184        let result = EncryptionContext::with_aad_sync(aad.clone(), EncryptionContext::get);
185
186        assert_eq!(result, Some(aad));
187    }
188
189    #[tokio::test]
190    async fn test_with_aad_sync_nested_contexts() {
191        let outer_aad = "outer-sync-key".to_string();
192        let inner_aad = "inner-sync-key".to_string();
193
194        let result = EncryptionContext::with_aad_sync(outer_aad.clone(), || {
195            // Outer context
196            let outer_result = EncryptionContext::get();
197            assert_eq!(outer_result, Some(outer_aad.clone()));
198
199            // Nested context shadows outer
200            let inner_result =
201                EncryptionContext::with_aad_sync(inner_aad.clone(), EncryptionContext::get);
202            assert_eq!(inner_result, Some(inner_aad));
203
204            // Back to outer context
205            EncryptionContext::get()
206        });
207
208        assert_eq!(result, Some(outer_aad));
209    }
210
211    #[tokio::test]
212    async fn test_with_aad_sync_returns_closure_result() {
213        let aad = "test-key".to_string();
214
215        // Test that with_aad_sync correctly returns the closure's result
216        let result: Result<i32, &str> = EncryptionContext::with_aad_sync(aad, || Ok(42));
217
218        assert_eq!(result, Ok(42));
219    }
220}