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}