1#[cfg(not(target_arch = "wasm32"))]
2use std::fs::{self, OpenOptions};
3#[cfg(not(target_arch = "wasm32"))]
4use std::io::{Read, Seek, SeekFrom};
5use std::path::PathBuf;
6
7use citadel_core::types::{Argon2Profile, CipherId, KdfAlgorithm, SyncMode};
8use citadel_core::{Error, Result, DEFAULT_BUFFER_POOL_SIZE, PBKDF2_MIN_ITERATIONS};
9#[cfg(not(target_arch = "wasm32"))]
10use citadel_core::{FILE_HEADER_SIZE, KEY_FILE_SIZE};
11use citadel_crypto::hkdf_utils::RegionWrapKeys;
12use citadel_crypto::key_manager::{create_key_file, create_key_file_with_region_keys};
13#[cfg(not(target_arch = "wasm32"))]
14use citadel_crypto::key_manager::{open_key_file, open_key_file_with_region_keys};
15use citadel_crypto::page_cipher::compute_dek_id;
16#[cfg(not(target_arch = "wasm32"))]
17use citadel_io::durable;
18#[cfg(not(target_arch = "wasm32"))]
19use citadel_io::file_lock;
20#[cfg(not(target_arch = "wasm32"))]
21use citadel_io::file_manager::FileHeader;
22#[cfg(not(target_arch = "wasm32"))]
23use citadel_io::mmap_io::MmapPageIO;
24use citadel_io::traits::PageIO;
25use citadel_txn::manager::TxnManager;
26
27use crate::database::Database;
28
29pub struct DatabaseBuilder {
43 path: PathBuf,
44 key_path: Option<PathBuf>,
45 passphrase: Option<Vec<u8>>,
46 argon2_profile: Argon2Profile,
47 cache_size: usize,
48 cipher: CipherId,
49 kdf_algorithm: KdfAlgorithm,
50 pbkdf2_iterations: u32,
51 sync_mode: SyncMode,
52 enable_region_keys: bool,
53 secure_delete: bool,
54 #[cfg(feature = "audit-log")]
55 audit_config: crate::audit::AuditConfig,
56}
57
58impl DatabaseBuilder {
59 pub fn new(path: impl Into<PathBuf>) -> Self {
60 Self {
61 path: path.into(),
62 key_path: None,
63 passphrase: None,
64 argon2_profile: Argon2Profile::Desktop,
65 cache_size: DEFAULT_BUFFER_POOL_SIZE,
66 cipher: CipherId::Aes256Ctr,
67 kdf_algorithm: KdfAlgorithm::Argon2id,
68 pbkdf2_iterations: PBKDF2_MIN_ITERATIONS,
69 sync_mode: SyncMode::Full,
70 enable_region_keys: false,
71 secure_delete: false,
72 #[cfg(feature = "audit-log")]
73 audit_config: crate::audit::AuditConfig::default(),
74 }
75 }
76
77 pub fn passphrase(mut self, passphrase: &[u8]) -> Self {
78 self.passphrase = Some(passphrase.to_vec());
79 self
80 }
81
82 pub fn key_path(mut self, path: impl Into<PathBuf>) -> Self {
83 self.key_path = Some(path.into());
84 self
85 }
86
87 pub fn argon2_profile(mut self, profile: Argon2Profile) -> Self {
88 self.argon2_profile = profile;
89 self
90 }
91
92 pub fn cache_size(mut self, pages: usize) -> Self {
93 self.cache_size = pages;
94 self
95 }
96
97 pub fn cipher(mut self, cipher: CipherId) -> Self {
98 self.cipher = cipher;
99 self
100 }
101
102 pub fn kdf_algorithm(mut self, algorithm: KdfAlgorithm) -> Self {
108 self.kdf_algorithm = algorithm;
109 self
110 }
111
112 pub fn pbkdf2_iterations(mut self, iterations: u32) -> Self {
116 self.pbkdf2_iterations = iterations;
117 self
118 }
119
120 pub fn sync_mode(mut self, mode: SyncMode) -> Self {
121 self.sync_mode = mode;
122 self
123 }
124
125 pub fn enable_region_keys(mut self, enable: bool) -> Self {
132 self.enable_region_keys = enable;
133 self
134 }
135
136 pub fn enable_secure_delete(mut self, enable: bool) -> Self {
140 self.secure_delete = enable;
141 self
142 }
143
144 #[cfg(feature = "audit-log")]
148 pub fn audit_config(mut self, config: crate::audit::AuditConfig) -> Self {
149 self.audit_config = config;
150 self
151 }
152
153 #[cfg(not(target_arch = "wasm32"))]
155 fn resolve_key_path(&self) -> PathBuf {
156 self.key_path.clone().unwrap_or_else(|| {
157 let mut name = self.path.as_os_str().to_os_string();
158 name.push(".citadel-keys");
159 PathBuf::from(name)
160 })
161 }
162
163 #[cfg(not(target_arch = "wasm32"))]
164 fn create_page_io(file: std::fs::File) -> Box<dyn PageIO> {
165 #[cfg(all(target_os = "linux", feature = "io-uring"))]
166 {
167 if let Some(uring) = citadel_io::uring_io::UringPageIO::try_new(
168 file.try_clone().expect("failed to clone file handle"),
169 ) {
170 return Box::new(uring);
171 }
172 }
173 Box::new(MmapPageIO::try_new(file).expect("mmap init failed"))
174 }
175
176 fn resolve_kdf_params(&self) -> (u32, u32, u32) {
179 match self.kdf_algorithm {
180 KdfAlgorithm::Argon2id => {
181 let profile = self.argon2_profile;
182 (profile.m_cost(), profile.t_cost(), profile.p_cost())
183 }
184 KdfAlgorithm::Pbkdf2HmacSha256 => (self.pbkdf2_iterations, 0, 0),
185 }
186 }
187
188 #[cfg(feature = "fips")]
190 fn validate_fips(&self) -> Result<()> {
191 if self.kdf_algorithm != KdfAlgorithm::Pbkdf2HmacSha256 {
192 return Err(Error::FipsViolation(
193 "FIPS mode requires PBKDF2-HMAC-SHA256 (Argon2id is not NIST approved)".into(),
194 ));
195 }
196 if self.cipher == CipherId::ChaCha20 {
197 return Err(Error::FipsViolation(
198 "FIPS mode requires AES-256-CTR (ChaCha20 is not NIST approved)".into(),
199 ));
200 }
201 Ok(())
202 }
203
204 #[cfg(feature = "audit-log")]
207 fn finish(
208 self,
209 manager: TxnManager,
210 key_path: PathBuf,
211 file_id: u64,
212 audit_key: [u8; citadel_core::KEY_SIZE],
213 region_keys: Option<RegionWrapKeys>,
214 initial_event: Option<(crate::audit::AuditEventType, Vec<u8>)>,
215 ) -> Result<Database> {
216 use crate::audit;
217
218 let audit_log = if self.audit_config.enabled && !self.path.as_os_str().is_empty() {
219 let audit_path = audit::resolve_audit_path(&self.path);
220 let log = if audit_path.exists() {
221 audit::AuditLog::open_existing(&audit_path, file_id, audit_key, self.audit_config)?
222 } else {
223 audit::AuditLog::create(&audit_path, file_id, audit_key, self.audit_config)?
224 };
225 Some(log)
226 } else {
227 None
228 };
229
230 manager.set_secure_delete(self.secure_delete);
231 let db = Database::new(
232 manager,
233 self.path,
234 key_path,
235 file_id,
236 region_keys,
237 audit_log,
238 );
239
240 if let Some((event, detail)) = initial_event {
241 db.log_audit(event, &detail);
242 }
243
244 Ok(db)
245 }
246
247 #[cfg(not(feature = "audit-log"))]
248 fn finish(
249 self,
250 manager: TxnManager,
251 key_path: PathBuf,
252 file_id: u64,
253 _audit_key: [u8; citadel_core::KEY_SIZE],
254 region_keys: Option<RegionWrapKeys>,
255 _initial_event: Option<((), Vec<u8>)>,
256 ) -> Result<Database> {
257 manager.set_secure_delete(self.secure_delete);
258 Ok(Database::new(
259 manager,
260 self.path,
261 key_path,
262 file_id,
263 region_keys,
264 ))
265 }
266
267 #[cfg(not(target_arch = "wasm32"))]
269 pub fn create(self) -> Result<Database> {
270 #[cfg(feature = "fips")]
271 self.validate_fips()?;
272
273 let passphrase = self
274 .passphrase
275 .as_deref()
276 .ok_or(Error::PassphraseRequired)?;
277
278 let key_path = self.resolve_key_path();
279 let file_id: u64 = rand::random();
280
281 let (kf, keys, region_keys) = self.create_keys(passphrase, file_id)?;
282
283 durable::write_and_sync(&key_path, &kf.serialize())?;
284
285 let file = OpenOptions::new()
286 .read(true)
287 .write(true)
288 .create_new(true)
289 .open(&self.path)?;
290
291 file_lock::try_lock_exclusive(&file)?;
292
293 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
294 let io = Self::create_page_io(file);
295
296 let manager = TxnManager::create_with_sync(
297 io,
298 keys.dek,
299 keys.mac_key,
300 kf.current_epoch,
301 file_id,
302 dek_id,
303 self.cache_size,
304 self.sync_mode,
305 )?;
306
307 #[cfg(feature = "audit-log")]
308 let event = {
309 let detail = vec![self.cipher as u8, self.kdf_algorithm as u8];
310 Some((crate::audit::AuditEventType::DatabaseCreated, detail))
311 };
312 #[cfg(not(feature = "audit-log"))]
313 let event: Option<((), Vec<u8>)> = None;
314
315 self.finish(
316 manager,
317 key_path,
318 file_id,
319 keys.audit_key,
320 region_keys,
321 event,
322 )
323 }
324
325 pub fn create_in_memory(mut self) -> Result<Database> {
330 #[cfg(feature = "fips")]
331 self.validate_fips()?;
332
333 if self.enable_region_keys {
336 return Err(Error::RegionKeysRequireFile);
337 }
338
339 let passphrase = self
340 .passphrase
341 .as_deref()
342 .ok_or(Error::PassphraseRequired)?;
343
344 let file_id: u64 = rand::random();
345
346 let (_kf, keys, region_keys) = self.create_keys(passphrase, file_id)?;
347
348 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
349 let io: Box<dyn PageIO> = Box::new(citadel_io::memory_io::MemoryPageIO::new());
350
351 let manager = TxnManager::create_with_sync(
352 io,
353 keys.dek,
354 keys.mac_key,
355 1,
356 file_id,
357 dek_id,
358 self.cache_size,
359 self.sync_mode,
360 )?;
361
362 self.path = PathBuf::new();
364 self.finish(
365 manager,
366 PathBuf::new(),
367 file_id,
368 keys.audit_key,
369 region_keys,
370 None,
371 )
372 }
373
374 #[cfg(not(target_arch = "wasm32"))]
376 pub fn open(self) -> Result<Database> {
377 let passphrase = self
378 .passphrase
379 .as_deref()
380 .ok_or(Error::PassphraseRequired)?;
381
382 let key_path = self.resolve_key_path();
383
384 let mut file = OpenOptions::new().read(true).write(true).open(&self.path)?;
385
386 file_lock::try_lock_exclusive(&file)?;
387
388 let mut header_buf = [0u8; FILE_HEADER_SIZE];
389 file.seek(SeekFrom::Start(0))?;
390 file.read_exact(&mut header_buf)?;
391 let header = FileHeader::deserialize(&header_buf)?;
392
393 let key_data = fs::read(&key_path)?;
394 if key_data.len() != KEY_FILE_SIZE {
395 return Err(Error::Io(std::io::Error::new(
396 std::io::ErrorKind::InvalidData,
397 "key file has incorrect size",
398 )));
399 }
400 let key_buf: [u8; KEY_FILE_SIZE] = key_data.try_into().unwrap();
401 let (kf, keys, region_keys) = self.open_keys(&key_buf, passphrase, header.file_id)?;
402
403 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
404
405 let io = Self::create_page_io(file);
406
407 let manager = TxnManager::open_with_sync(
408 io,
409 keys.dek,
410 keys.mac_key,
411 kf.current_epoch,
412 self.cache_size,
413 self.sync_mode,
414 )?;
415
416 let slot = manager.current_slot();
417 if slot.dek_id != dek_id {
418 return Err(Error::BadPassphrase);
419 }
420
421 #[cfg(feature = "audit-log")]
422 let event = Some((crate::audit::AuditEventType::DatabaseOpened, vec![]));
423 #[cfg(not(feature = "audit-log"))]
424 let event: Option<((), Vec<u8>)> = None;
425
426 self.finish(
427 manager,
428 key_path,
429 header.file_id,
430 keys.audit_key,
431 region_keys,
432 event,
433 )
434 }
435
436 #[allow(clippy::type_complexity)]
440 fn create_keys(
441 &self,
442 passphrase: &[u8],
443 file_id: u64,
444 ) -> Result<(
445 citadel_crypto::key_manager::KeyFile,
446 citadel_crypto::hkdf_utils::DerivedKeys,
447 Option<RegionWrapKeys>,
448 )> {
449 let (m_cost, t_cost, p_cost) = self.resolve_kdf_params();
450 if self.enable_region_keys {
451 let (kf, keys, region) = create_key_file_with_region_keys(
452 passphrase,
453 file_id,
454 self.cipher,
455 self.kdf_algorithm,
456 m_cost,
457 t_cost,
458 p_cost,
459 )?;
460 Ok((kf, keys, Some(region)))
461 } else {
462 let (kf, keys) = create_key_file(
463 passphrase,
464 file_id,
465 self.cipher,
466 self.kdf_algorithm,
467 m_cost,
468 t_cost,
469 p_cost,
470 )?;
471 Ok((kf, keys, None))
472 }
473 }
474
475 #[cfg(not(target_arch = "wasm32"))]
477 #[allow(clippy::type_complexity)]
478 fn open_keys(
479 &self,
480 key_buf: &[u8; KEY_FILE_SIZE],
481 passphrase: &[u8],
482 expected_file_id: u64,
483 ) -> Result<(
484 citadel_crypto::key_manager::KeyFile,
485 citadel_crypto::hkdf_utils::DerivedKeys,
486 Option<RegionWrapKeys>,
487 )> {
488 if self.enable_region_keys {
489 let (kf, keys, region) =
490 open_key_file_with_region_keys(key_buf, passphrase, expected_file_id)?;
491 Ok((kf, keys, Some(region)))
492 } else {
493 let (kf, keys) = open_key_file(key_buf, passphrase, expected_file_id)?;
494 Ok((kf, keys, None))
495 }
496 }
497}