Skip to main content

fraiseql_core/security/kms/
models.rs

1//! KMS domain models for key management.
2//!
3//! Provides immutable value objects for representing encrypted data,
4//! key references, and rotation policies.
5
6use std::{collections::HashMap, fmt};
7
8use serde::{Deserialize, Serialize};
9use zeroize::Zeroizing;
10
11/// Intended use of the key.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum KeyPurpose {
15    /// Key used for encryption/decryption
16    EncryptDecrypt,
17    /// Key used for signing/verification
18    SignVerify,
19    /// Key used for message authentication codes
20    Mac,
21}
22
23impl fmt::Display for KeyPurpose {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::EncryptDecrypt => write!(f, "encrypt_decrypt"),
27            Self::SignVerify => write!(f, "sign_verify"),
28            Self::Mac => write!(f, "mac"),
29        }
30    }
31}
32
33/// Current state of the key.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum KeyState {
37    /// Key is active and can be used
38    Enabled,
39    /// Key is disabled and cannot be used
40    Disabled,
41    /// Key is pending deletion
42    PendingDeletion,
43    /// Key has been destroyed
44    Destroyed,
45}
46
47impl fmt::Display for KeyState {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            Self::Enabled => write!(f, "enabled"),
51            Self::Disabled => write!(f, "disabled"),
52            Self::PendingDeletion => write!(f, "pending_deletion"),
53            Self::Destroyed => write!(f, "destroyed"),
54        }
55    }
56}
57
58/// Immutable reference to a key in KMS.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct KeyReference {
61    /// Provider identifier (e.g., 'vault', 'aws', 'gcp')
62    pub provider:   String,
63    /// Provider-specific key identifier
64    pub key_id:     String,
65    /// Human-readable alias (optional)
66    pub key_alias:  Option<String>,
67    /// Intended use of the key
68    pub purpose:    KeyPurpose,
69    /// When the key was created (Unix timestamp)
70    pub created_at: i64,
71}
72
73impl KeyReference {
74    /// Create a new key reference.
75    pub fn new(provider: String, key_id: String, purpose: KeyPurpose, created_at: i64) -> Self {
76        Self {
77            provider,
78            key_id,
79            key_alias: None,
80            purpose,
81            created_at,
82        }
83    }
84
85    /// Set the key alias.
86    #[must_use]
87    pub fn with_alias(mut self, alias: String) -> Self {
88        self.key_alias = Some(alias);
89        self
90    }
91
92    /// Get the fully qualified key identifier.
93    #[must_use]
94    pub fn qualified_id(&self) -> String {
95        format!("{}:{}", self.provider, self.key_id)
96    }
97}
98
99/// Encrypted data with metadata.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct EncryptedData {
102    /// The encrypted bytes (as hex string for JSON compatibility)
103    pub ciphertext:    String,
104    /// Reference to the key used
105    pub key_reference: KeyReference,
106    /// Encryption algorithm used
107    pub algorithm:     String,
108    /// When encryption occurred (Unix timestamp)
109    pub encrypted_at:  i64,
110    /// Additional authenticated data (AAD)
111    pub context:       HashMap<String, String>,
112}
113
114impl EncryptedData {
115    /// Create new encrypted data.
116    pub fn new(
117        ciphertext: String,
118        key_reference: KeyReference,
119        algorithm: String,
120        encrypted_at: i64,
121        context: HashMap<String, String>,
122    ) -> Self {
123        Self {
124            ciphertext,
125            key_reference,
126            algorithm,
127            encrypted_at,
128            context,
129        }
130    }
131}
132
133/// Data key pair for envelope encryption.
134#[derive(Debug, Clone)]
135pub struct DataKeyPair {
136    /// Use immediately, never persist
137    pub plaintext_key: Zeroizing<Vec<u8>>,
138    /// Persist alongside encrypted data
139    pub encrypted_key: EncryptedData,
140    /// Master key used for wrapping
141    pub key_reference: KeyReference,
142}
143
144impl DataKeyPair {
145    /// Create a new data key pair.
146    pub fn new(
147        plaintext_key: Vec<u8>,
148        encrypted_key: EncryptedData,
149        key_reference: KeyReference,
150    ) -> Self {
151        Self {
152            plaintext_key: Zeroizing::new(plaintext_key),
153            encrypted_key,
154            key_reference,
155        }
156    }
157}
158
159/// Key rotation configuration.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct RotationPolicy {
162    /// Whether automatic rotation is enabled
163    pub enabled:              bool,
164    /// Days between rotations
165    pub rotation_period_days: u32,
166    /// When key was last rotated (Unix timestamp, None if never)
167    pub last_rotation:        Option<i64>,
168    /// When key will next be rotated (Unix timestamp, None if not scheduled)
169    pub next_rotation:        Option<i64>,
170}
171
172impl RotationPolicy {
173    /// Create a new rotation policy.
174    pub fn new(enabled: bool, rotation_period_days: u32) -> Self {
175        Self {
176            enabled,
177            rotation_period_days,
178            last_rotation: None,
179            next_rotation: None,
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_key_reference_qualified_id() {
190        let key_ref = KeyReference::new(
191            "vault".to_string(),
192            "my-key-123".to_string(),
193            KeyPurpose::EncryptDecrypt,
194            1_000_000,
195        );
196        assert_eq!(key_ref.qualified_id(), "vault:my-key-123");
197    }
198
199    #[test]
200    fn test_key_reference_with_alias() {
201        let key_ref = KeyReference::new(
202            "vault".to_string(),
203            "my-key-123".to_string(),
204            KeyPurpose::EncryptDecrypt,
205            1_000_000,
206        )
207        .with_alias("production-key".to_string());
208
209        assert_eq!(key_ref.key_alias, Some("production-key".to_string()));
210    }
211
212    #[test]
213    fn test_key_purpose_display() {
214        assert_eq!(KeyPurpose::EncryptDecrypt.to_string(), "encrypt_decrypt");
215        assert_eq!(KeyPurpose::SignVerify.to_string(), "sign_verify");
216        assert_eq!(KeyPurpose::Mac.to_string(), "mac");
217    }
218
219    #[test]
220    fn test_key_state_display() {
221        assert_eq!(KeyState::Enabled.to_string(), "enabled");
222        assert_eq!(KeyState::Disabled.to_string(), "disabled");
223    }
224
225    #[test]
226    fn test_rotation_policy_new() {
227        let policy = RotationPolicy::new(true, 90);
228        assert!(policy.enabled);
229        assert_eq!(policy.rotation_period_days, 90);
230        assert_eq!(policy.last_rotation, None);
231        assert_eq!(policy.next_rotation, None);
232    }
233}