sifredb_key_file/
lib.rs

1//! File-based key provider for `SifreDB`.
2//!
3//! This provider stores keys in the filesystem and is suitable for
4//! development and testing environments.
5//!
6//! # Security Warning
7//!
8//! This provider is NOT recommended for production use. Keys are stored
9//! in plaintext on disk. For production, use a KMS provider (AWS KMS, GCP KMS, etc.).
10
11#![warn(clippy::pedantic, clippy::nursery)]
12#![allow(clippy::missing_errors_doc)]
13
14use chacha20poly1305::{
15    aead::{Aead, KeyInit, OsRng},
16    ChaCha20Poly1305, Nonce,
17};
18use rand::RngCore;
19use secrecy::{ExposeSecret, SecretVec};
20use sifredb::error::KeyProviderError;
21use sifredb::key_provider::KeyProvider;
22use std::fs::{self, File};
23use std::io::{Read, Write};
24use std::path::{Path, PathBuf};
25
26const KEK_SIZE: usize = 32; // 256 bits
27const PEPPER_SIZE: usize = 32; // 256 bits
28const NONCE_SIZE: usize = 12; // 96 bits for ChaCha20-Poly1305
29
30/// File-based key provider for development and testing.
31///
32/// Keys are stored in the filesystem with the following structure:
33/// ```text
34/// keys/
35/// ├── kek_v1.key      (32 bytes, 0600 permissions)
36/// ├── kek_v2.key      (32 bytes, 0600 permissions)
37/// ├── current -> kek_v2.key  (symlink to active KEK)
38/// └── pepper.key      (32 bytes, 0600 permissions)
39/// ```
40///
41/// # Example
42///
43/// ```no_run
44/// use sifredb_key_file::FileKeyProvider;
45/// use sifredb::key_provider::KeyProvider;
46///
47/// // Initialize a new key directory
48/// FileKeyProvider::init("./keys").expect("Failed to initialize keys");
49///
50/// // Load the provider
51/// let provider = FileKeyProvider::new("./keys").expect("Failed to load provider");
52///
53/// // Use the provider
54/// let kek_id = provider.current_kek_id().expect("No active KEK");
55/// ```
56pub struct FileKeyProvider {
57    key_dir: PathBuf,
58}
59
60impl FileKeyProvider {
61    /// Creates a new `FileKeyProvider` from an existing key directory.
62    ///
63    /// # Arguments
64    ///
65    /// * `key_dir` - Directory containing key files
66    ///
67    /// # Errors
68    ///
69    /// Returns error if:
70    /// - Directory doesn't exist
71    /// - No current KEK symlink exists
72    /// - File permissions are incorrect (Unix only)
73    pub fn new(key_dir: impl Into<PathBuf>) -> Result<Self, KeyProviderError> {
74        let key_dir = key_dir.into();
75
76        if !key_dir.exists() {
77            return Err(KeyProviderError::CreationFailed(format!(
78                "Key directory does not exist: {}",
79                key_dir.display()
80            )));
81        }
82
83        let current_link = key_dir.join("current");
84        if !current_link.exists() {
85            return Err(KeyProviderError::NoActiveKek);
86        }
87
88        let provider = Self { key_dir };
89
90        // Verify file permissions on Unix
91        #[cfg(unix)]
92        provider.check_permissions()?;
93
94        Ok(provider)
95    }
96
97    /// Initializes a new key directory with a fresh KEK and pepper.
98    ///
99    /// This creates:
100    /// - A new KEK (`kek_v1.key`)
101    /// - A symlink pointing to the current KEK
102    /// - A pepper for blind indexes
103    ///
104    /// # Errors
105    ///
106    /// Returns error if directory creation or key generation fails.
107    pub fn init(key_dir: impl Into<PathBuf>) -> Result<(), KeyProviderError> {
108        let key_dir = key_dir.into();
109
110        // Create directory if it doesn't exist
111        fs::create_dir_all(&key_dir)?;
112
113        // Generate first KEK
114        let kek_id = "kek_v1";
115        let kek_filename = format!("{kek_id}.key");
116        let kek_path = key_dir.join(&kek_filename);
117        let kek = generate_random_key(KEK_SIZE);
118        write_key_file(&kek_path, &kek)?;
119
120        // Create symlink to current KEK (use relative path for portability)
121        let current_link = key_dir.join("current");
122        create_symlink(kek_filename.as_ref(), &current_link)?;
123
124        // Generate pepper
125        let pepper_path = key_dir.join("pepper.key");
126        let pepper = generate_random_key(PEPPER_SIZE);
127        write_key_file(&pepper_path, &pepper)?;
128
129        Ok(())
130    }
131
132    /// Checks file permissions on Unix systems.
133    #[cfg(unix)]
134    fn check_permissions(&self) -> Result<(), KeyProviderError> {
135        use std::os::unix::fs::PermissionsExt;
136
137        let entries = fs::read_dir(&self.key_dir)?;
138
139        for entry in entries {
140            let entry = entry?;
141            let path = entry.path();
142
143            // Skip symlinks and directories
144            if path.is_symlink() || path.is_dir() {
145                continue;
146            }
147
148            let metadata = fs::metadata(&path)?;
149            let permissions = metadata.permissions();
150            let mode = permissions.mode() & 0o777;
151
152            if mode != 0o600 {
153                return Err(KeyProviderError::CreationFailed(format!(
154                    "Insecure file permissions on {}: {:o} (expected 0600)",
155                    path.display(),
156                    mode
157                )));
158            }
159        }
160
161        Ok(())
162    }
163
164    /// Reads a KEK from disk.
165    fn read_kek(&self, kek_id: &str) -> Result<SecretVec<u8>, KeyProviderError> {
166        let kek_path = self.key_dir.join(format!("{kek_id}.key"));
167
168        if !kek_path.exists() {
169            return Err(KeyProviderError::KekNotFound(kek_id.to_string()));
170        }
171
172        let mut file = File::open(&kek_path)?;
173        let mut kek = vec![0u8; KEK_SIZE];
174        file.read_exact(&mut kek)?;
175
176        Ok(SecretVec::new(kek))
177    }
178
179    /// Resolves the current KEK symlink to get the KEK ID.
180    fn resolve_current_kek(&self) -> Result<String, KeyProviderError> {
181        let current_link = self.key_dir.join("current");
182
183        if !current_link.exists() {
184            return Err(KeyProviderError::NoActiveKek);
185        }
186
187        let target = fs::read_link(&current_link)?;
188        let filename = target.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
189            KeyProviderError::CreationFailed("Invalid current KEK symlink".to_string())
190        })?;
191
192        // Extract kek_id from "kek_v1.key" -> "kek_v1"
193        let kek_id = filename.strip_suffix(".key").ok_or_else(|| {
194            KeyProviderError::CreationFailed("Invalid KEK filename format".to_string())
195        })?;
196
197        Ok(kek_id.to_string())
198    }
199
200    /// Finds the next KEK version number.
201    fn next_kek_version(&self) -> Result<u32, KeyProviderError> {
202        let entries = fs::read_dir(&self.key_dir)?;
203        let mut max_version = 0u32;
204
205        for entry in entries {
206            let entry = entry?;
207            let filename = entry.file_name();
208            let filename_str = filename.to_string_lossy();
209
210            // Parse "kek_v1.key" -> 1
211            if let Some(version_str) =
212                filename_str.strip_prefix("kek_v").and_then(|s| s.strip_suffix(".key"))
213            {
214                if let Ok(version) = version_str.parse::<u32>() {
215                    max_version = max_version.max(version);
216                }
217            }
218        }
219
220        Ok(max_version + 1)
221    }
222}
223
224impl KeyProvider for FileKeyProvider {
225    fn create_kek(&self) -> Result<String, KeyProviderError> {
226        let version = self.next_kek_version()?;
227        let kek_id = format!("kek_v{version}");
228        let kek_filename = format!("{kek_id}.key");
229        let kek_path = self.key_dir.join(&kek_filename);
230
231        // Generate new KEK
232        let kek = generate_random_key(KEK_SIZE);
233        write_key_file(&kek_path, &kek)?;
234
235        // Update current symlink (use relative path for portability)
236        let current_link = self.key_dir.join("current");
237        if current_link.exists() {
238            fs::remove_file(&current_link)?;
239        }
240        create_symlink(kek_filename.as_ref(), &current_link)?;
241
242        Ok(kek_id)
243    }
244
245    fn current_kek_id(&self) -> Result<String, KeyProviderError> {
246        self.resolve_current_kek()
247    }
248
249    fn wrap_dek(&self, kek_id: &str, dek: &[u8]) -> Result<Vec<u8>, KeyProviderError> {
250        let kek = self.read_kek(kek_id)?;
251
252        // Use ChaCha20-Poly1305 to wrap the DEK
253        let cipher = ChaCha20Poly1305::new_from_slice(kek.expose_secret())
254            .map_err(|e| KeyProviderError::WrapFailed(format!("Invalid KEK: {e}")))?;
255
256        // Generate random nonce
257        let mut nonce_bytes = [0u8; NONCE_SIZE];
258        OsRng.fill_bytes(&mut nonce_bytes);
259        let nonce = Nonce::from(nonce_bytes);
260
261        // Encrypt DEK
262        let ciphertext = cipher
263            .encrypt(&nonce, dek)
264            .map_err(|e| KeyProviderError::WrapFailed(format!("Encryption failed: {e}")))?;
265
266        // Return nonce || ciphertext
267        let mut wrapped = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
268        wrapped.extend_from_slice(&nonce_bytes);
269        wrapped.extend_from_slice(&ciphertext);
270
271        Ok(wrapped)
272    }
273
274    fn unwrap_dek(
275        &self,
276        kek_id: &str,
277        wrapped_dek: &[u8],
278    ) -> Result<SecretVec<u8>, KeyProviderError> {
279        if wrapped_dek.len() < NONCE_SIZE {
280            return Err(KeyProviderError::UnwrapFailed("Wrapped DEK too short".to_string()));
281        }
282
283        let kek = self.read_kek(kek_id)?;
284
285        // Use ChaCha20-Poly1305 to unwrap the DEK
286        let cipher = ChaCha20Poly1305::new_from_slice(kek.expose_secret())
287            .map_err(|e| KeyProviderError::UnwrapFailed(format!("Invalid KEK: {e}")))?;
288
289        // Split nonce and ciphertext
290        let (nonce_bytes, ciphertext) = wrapped_dek.split_at(NONCE_SIZE);
291        let nonce_array: [u8; NONCE_SIZE] = nonce_bytes
292            .try_into()
293            .map_err(|_| KeyProviderError::UnwrapFailed("Invalid nonce size".to_string()))?;
294        let nonce = Nonce::from(nonce_array);
295
296        // Decrypt DEK
297        let plaintext = cipher
298            .decrypt(&nonce, ciphertext)
299            .map_err(|e| KeyProviderError::UnwrapFailed(format!("Decryption failed: {e}")))?;
300
301        Ok(SecretVec::new(plaintext))
302    }
303
304    fn get_pepper(&self) -> Result<Option<SecretVec<u8>>, KeyProviderError> {
305        let pepper_path = self.key_dir.join("pepper.key");
306
307        if !pepper_path.exists() {
308            return Ok(None);
309        }
310
311        let mut file = File::open(&pepper_path)?;
312        let mut pepper = vec![0u8; PEPPER_SIZE];
313        file.read_exact(&mut pepper)?;
314
315        Ok(Some(SecretVec::new(pepper)))
316    }
317}
318
319/// Generates a random key of the specified size.
320fn generate_random_key(size: usize) -> Vec<u8> {
321    let mut key = vec![0u8; size];
322    OsRng.fill_bytes(&mut key);
323    key
324}
325
326/// Writes a key to a file with secure permissions.
327fn write_key_file(path: &Path, key: &[u8]) -> Result<(), KeyProviderError> {
328    let mut file = File::create(path)?;
329    file.write_all(key)?;
330
331    // Set permissions to 0600 on Unix
332    #[cfg(unix)]
333    {
334        use std::os::unix::fs::PermissionsExt;
335        let mut permissions = file.metadata()?.permissions();
336        permissions.set_mode(0o600);
337        fs::set_permissions(path, permissions)?;
338    }
339
340    Ok(())
341}
342
343/// Creates a symlink (cross-platform).
344fn create_symlink(target: &Path, link: &Path) -> Result<(), KeyProviderError> {
345    #[cfg(unix)]
346    {
347        std::os::unix::fs::symlink(target, link)?;
348    }
349
350    #[cfg(windows)]
351    {
352        std::os::windows::fs::symlink_file(target, link)?;
353    }
354
355    Ok(())
356}