Skip to main content

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}