1#![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; const PEPPER_SIZE: usize = 32; const NONCE_SIZE: usize = 12; pub struct FileKeyProvider {
57 key_dir: PathBuf,
58}
59
60impl FileKeyProvider {
61 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 #[cfg(unix)]
92 provider.check_permissions()?;
93
94 Ok(provider)
95 }
96
97 pub fn init(key_dir: impl Into<PathBuf>) -> Result<(), KeyProviderError> {
108 let key_dir = key_dir.into();
109
110 fs::create_dir_all(&key_dir)?;
112
113 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 let current_link = key_dir.join("current");
122 create_symlink(kek_filename.as_ref(), ¤t_link)?;
123
124 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 #[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 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 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 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(¤t_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 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 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 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 let kek = generate_random_key(KEK_SIZE);
233 write_key_file(&kek_path, &kek)?;
234
235 let current_link = self.key_dir.join("current");
237 if current_link.exists() {
238 fs::remove_file(¤t_link)?;
239 }
240 create_symlink(kek_filename.as_ref(), ¤t_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 let cipher = ChaCha20Poly1305::new_from_slice(kek.expose_secret())
254 .map_err(|e| KeyProviderError::WrapFailed(format!("Invalid KEK: {e}")))?;
255
256 let mut nonce_bytes = [0u8; NONCE_SIZE];
258 OsRng.fill_bytes(&mut nonce_bytes);
259 let nonce = Nonce::from(nonce_bytes);
260
261 let ciphertext = cipher
263 .encrypt(&nonce, dek)
264 .map_err(|e| KeyProviderError::WrapFailed(format!("Encryption failed: {e}")))?;
265
266 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 let cipher = ChaCha20Poly1305::new_from_slice(kek.expose_secret())
287 .map_err(|e| KeyProviderError::UnwrapFailed(format!("Invalid KEK: {e}")))?;
288
289 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 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
319fn generate_random_key(size: usize) -> Vec<u8> {
321 let mut key = vec![0u8; size];
322 OsRng.fill_bytes(&mut key);
323 key
324}
325
326fn write_key_file(path: &Path, key: &[u8]) -> Result<(), KeyProviderError> {
328 let mut file = File::create(path)?;
329 file.write_all(key)?;
330
331 #[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
343fn 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}