fraiseql_core/security/kms/base.rs
1//! Base KMS provider trait with template method pattern.
2//!
3//! Provides public async methods with common logic and abstract hooks
4//! for provider-specific implementations.
5
6use std::collections::HashMap;
7
8use async_trait::async_trait;
9
10use crate::{
11 security::kms::{
12 error::{KmsError, KmsResult},
13 models::{DataKeyPair, EncryptedData, KeyPurpose, KeyReference, RotationPolicy},
14 },
15 utils::clock::{Clock, SystemClock},
16};
17
18/// Abstract base class for KMS providers.
19///
20/// Implements the Template Method pattern:
21/// - Public methods (encrypt, decrypt, etc.) handle common logic
22/// - Protected abstract methods (`do_encrypt`, `do_decrypt`, etc.) are implemented by concrete
23/// providers
24// Reason: used as dyn Trait (Arc<dyn BaseKmsProvider>); async_trait ensures Send bounds and
25// dyn-compatibility async_trait: dyn-dispatch required; remove when RTN + Send is stable (RFC 3425)
26#[async_trait]
27pub trait BaseKmsProvider: Send + Sync {
28 /// Unique provider identifier (e.g., 'vault', 'aws', 'gcp').
29 fn provider_name(&self) -> &str;
30
31 /// Return the current Unix timestamp in seconds as a signed integer.
32 ///
33 /// Override in tests to return a fixed timestamp, enabling deterministic
34 /// testing of key-rotation scheduling without real-time delays.
35 ///
36 /// The default implementation delegates to [`SystemClock`].
37 fn timestamp_secs(&self) -> i64 {
38 SystemClock.now_secs_i64()
39 }
40
41 // ─────────────────────────────────────────────────────────────
42 // Template Methods (public API)
43 // ─────────────────────────────────────────────────────────────
44
45 /// Encrypt data using the specified key.
46 ///
47 /// # Arguments
48 /// * `plaintext` - Data to encrypt
49 /// * `key_id` - Key identifier
50 /// * `context` - Additional authenticated data (AAD)
51 ///
52 /// # Returns
53 /// `EncryptedData` with ciphertext and metadata
54 ///
55 /// # Errors
56 /// Returns `KmsError::EncryptionFailed` if encryption fails
57 async fn encrypt(
58 &self,
59 plaintext: &[u8],
60 key_id: &str,
61 context: Option<HashMap<String, String>>,
62 ) -> KmsResult<EncryptedData> {
63 let ctx = context.unwrap_or_default();
64
65 let (ciphertext, algorithm) =
66 self.do_encrypt(plaintext, key_id, &ctx).await.map_err(|e| {
67 KmsError::EncryptionFailed {
68 message: format!("Provider encryption failed: {}", e),
69 }
70 })?;
71
72 Ok(EncryptedData::new(
73 ciphertext,
74 KeyReference::new(
75 self.provider_name().to_string(),
76 key_id.to_string(),
77 KeyPurpose::EncryptDecrypt,
78 self.timestamp_secs(),
79 ),
80 algorithm,
81 self.timestamp_secs(),
82 ctx,
83 ))
84 }
85
86 /// Decrypt data.
87 ///
88 /// # Arguments
89 /// * `encrypted` - `EncryptedData` to decrypt
90 /// * `context` - Override context (uses encrypted.context if not provided)
91 ///
92 /// # Returns
93 /// Decrypted plaintext bytes
94 ///
95 /// # Errors
96 /// Returns `KmsError::DecryptionFailed` if decryption fails
97 async fn decrypt(
98 &self,
99 encrypted: &EncryptedData,
100 context: Option<HashMap<String, String>>,
101 ) -> KmsResult<Vec<u8>> {
102 let ctx = context.unwrap_or_else(|| encrypted.context.clone());
103 let key_id = &encrypted.key_reference.key_id;
104
105 self.do_decrypt(&encrypted.ciphertext, key_id, &ctx).await.map_err(|e| {
106 KmsError::DecryptionFailed {
107 message: format!("Provider decryption failed: {}", e),
108 }
109 })
110 }
111
112 /// Generate a data encryption key (envelope encryption).
113 ///
114 /// # Arguments
115 /// * `key_id` - Master key to wrap the data key
116 /// * `context` - Additional authenticated data
117 ///
118 /// # Returns
119 /// `DataKeyPair` with plaintext and encrypted data key
120 async fn generate_data_key(
121 &self,
122 key_id: &str,
123 context: Option<HashMap<String, String>>,
124 ) -> KmsResult<DataKeyPair> {
125 let ctx = context.unwrap_or_default();
126
127 let (plaintext_key, encrypted_key_bytes) = self
128 .do_generate_data_key(key_id, &ctx)
129 .await
130 .map_err(|e| KmsError::EncryptionFailed {
131 message: format!("Data key generation failed: {}", e),
132 })?;
133
134 let key_ref = KeyReference::new(
135 self.provider_name().to_string(),
136 key_id.to_string(),
137 KeyPurpose::EncryptDecrypt,
138 self.timestamp_secs(),
139 );
140
141 Ok(DataKeyPair::new(
142 plaintext_key,
143 EncryptedData::new(
144 encrypted_key_bytes,
145 key_ref.clone(),
146 "data-key".to_string(),
147 self.timestamp_secs(),
148 ctx,
149 ),
150 key_ref,
151 ))
152 }
153
154 /// Rotate the specified key.
155 ///
156 /// # Errors
157 /// Returns `KmsError::RotationFailed` if rotation fails
158 async fn rotate_key(&self, key_id: &str) -> KmsResult<KeyReference> {
159 self.do_rotate_key(key_id).await.map_err(|e| KmsError::RotationFailed {
160 message: format!("Provider rotation failed: {}", e),
161 })?;
162
163 self.get_key_info(key_id).await
164 }
165
166 /// Get key metadata.
167 ///
168 /// # Errors
169 /// Returns `KmsError::KeyNotFound` if key does not exist
170 async fn get_key_info(&self, key_id: &str) -> KmsResult<KeyReference> {
171 let info = self.do_get_key_info(key_id).await.map_err(|_e| KmsError::KeyNotFound {
172 key_id: key_id.to_string(),
173 })?;
174
175 Ok(KeyReference::new(
176 self.provider_name().to_string(),
177 key_id.to_string(),
178 KeyPurpose::EncryptDecrypt,
179 info.created_at,
180 )
181 .with_alias(info.alias.unwrap_or_default()))
182 }
183
184 /// Get key rotation policy.
185 ///
186 /// # Errors
187 /// Returns `KmsError::KeyNotFound` if key does not exist
188 async fn get_rotation_policy(&self, key_id: &str) -> KmsResult<RotationPolicy> {
189 let policy =
190 self.do_get_rotation_policy(key_id).await.map_err(|_e| KmsError::KeyNotFound {
191 key_id: key_id.to_string(),
192 })?;
193
194 Ok(RotationPolicy {
195 enabled: policy.enabled,
196 rotation_period_days: policy.rotation_period_days,
197 last_rotation: policy.last_rotation,
198 next_rotation: policy.next_rotation,
199 })
200 }
201
202 // ─────────────────────────────────────────────────────────────
203 // Abstract Methods (provider-specific hooks)
204 // ─────────────────────────────────────────────────────────────
205
206 /// Provider-specific encryption.
207 ///
208 /// # Arguments
209 /// * `plaintext` - Data to encrypt
210 /// * `key_id` - Key identifier
211 /// * `context` - AAD context (never empty)
212 ///
213 /// # Returns
214 /// Tuple of (ciphertext, `algorithm_name`) on success
215 async fn do_encrypt(
216 &self,
217 plaintext: &[u8],
218 key_id: &str,
219 context: &HashMap<String, String>,
220 ) -> KmsResult<(String, String)>;
221
222 /// Provider-specific decryption.
223 ///
224 /// # Arguments
225 /// * `ciphertext` - Data to decrypt
226 /// * `key_id` - Key identifier
227 /// * `context` - AAD context (never empty)
228 ///
229 /// # Returns
230 /// Decrypted plaintext bytes
231 async fn do_decrypt(
232 &self,
233 ciphertext: &str,
234 key_id: &str,
235 context: &HashMap<String, String>,
236 ) -> KmsResult<Vec<u8>>;
237
238 /// Provider-specific data key generation.
239 ///
240 /// # Arguments
241 /// * `key_id` - Master key identifier
242 /// * `context` - AAD context (never empty)
243 ///
244 /// # Returns
245 /// Tuple of (`plaintext_key`, `encrypted_key_hex`)
246 async fn do_generate_data_key(
247 &self,
248 key_id: &str,
249 context: &HashMap<String, String>,
250 ) -> KmsResult<(Vec<u8>, String)>;
251
252 /// Provider-specific key rotation.
253 async fn do_rotate_key(&self, key_id: &str) -> KmsResult<()>;
254
255 /// Provider-specific key info retrieval.
256 ///
257 /// Returns `KeyInfo` struct with alias and `created_at`
258 async fn do_get_key_info(&self, key_id: &str) -> KmsResult<KeyInfo>;
259
260 /// Provider-specific rotation policy retrieval.
261 async fn do_get_rotation_policy(&self, key_id: &str) -> KmsResult<RotationPolicyInfo>;
262}
263
264/// Type alias for arc-wrapped dynamic KMS provider.
265///
266/// Used for thread-safe, reference-counted storage of KMS providers.
267pub type ArcKmsProvider = std::sync::Arc<dyn BaseKmsProvider>;
268
269/// Key information returned by provider.
270#[derive(Debug, Clone)]
271pub struct KeyInfo {
272 /// Human-readable alias for the key, if one is configured in the provider.
273 pub alias: Option<String>,
274 /// Unix timestamp (seconds) when the key was created.
275 pub created_at: i64,
276}
277
278/// Rotation policy info returned by provider.
279#[derive(Debug, Clone)]
280pub struct RotationPolicyInfo {
281 /// Whether automatic rotation is enabled for this key.
282 pub enabled: bool,
283 /// How often the key is rotated, expressed in days.
284 pub rotation_period_days: u32,
285 /// Unix timestamp (seconds) of the most recent rotation, if any.
286 pub last_rotation: Option<i64>,
287 /// Unix timestamp (seconds) when the next rotation is scheduled, if known.
288 pub next_rotation: Option<i64>,
289}
290
291#[cfg(test)]
292mod tests {
293 use crate::utils::clock::{Clock as _, SystemClock};
294
295 #[test]
296 fn test_system_clock_timestamp_is_positive() {
297 assert!(SystemClock.now_secs_i64() > 0);
298 }
299}