1use std::fs::{self, OpenOptions};
2use std::io::{Read, Seek, SeekFrom};
3use std::path::PathBuf;
4
5use citadel_core::types::{Argon2Profile, CipherId, KdfAlgorithm, SyncMode};
6use citadel_core::{
7 Error, Result, DEFAULT_BUFFER_POOL_SIZE, FILE_HEADER_SIZE, KEY_FILE_SIZE, PBKDF2_MIN_ITERATIONS,
8};
9use citadel_crypto::key_manager::{create_key_file, open_key_file};
10use citadel_crypto::page_cipher::compute_dek_id;
11use citadel_io::durable;
12use citadel_io::file_lock;
13use citadel_io::file_manager::FileHeader;
14use citadel_io::sync_io::SyncPageIO;
15use citadel_io::traits::PageIO;
16use citadel_txn::manager::TxnManager;
17
18use crate::database::Database;
19
20pub struct DatabaseBuilder {
34 path: PathBuf,
35 key_path: Option<PathBuf>,
36 passphrase: Option<Vec<u8>>,
37 argon2_profile: Argon2Profile,
38 cache_size: usize,
39 cipher: CipherId,
40 kdf_algorithm: KdfAlgorithm,
41 pbkdf2_iterations: u32,
42 sync_mode: SyncMode,
43 #[cfg(feature = "audit-log")]
44 audit_config: crate::audit::AuditConfig,
45}
46
47impl DatabaseBuilder {
48 pub fn new(path: impl Into<PathBuf>) -> Self {
49 Self {
50 path: path.into(),
51 key_path: None,
52 passphrase: None,
53 argon2_profile: Argon2Profile::Desktop,
54 cache_size: DEFAULT_BUFFER_POOL_SIZE,
55 cipher: CipherId::Aes256Ctr,
56 kdf_algorithm: KdfAlgorithm::Argon2id,
57 pbkdf2_iterations: PBKDF2_MIN_ITERATIONS,
58 sync_mode: SyncMode::Full,
59 #[cfg(feature = "audit-log")]
60 audit_config: crate::audit::AuditConfig::default(),
61 }
62 }
63
64 pub fn passphrase(mut self, passphrase: &[u8]) -> Self {
65 self.passphrase = Some(passphrase.to_vec());
66 self
67 }
68
69 pub fn key_path(mut self, path: impl Into<PathBuf>) -> Self {
70 self.key_path = Some(path.into());
71 self
72 }
73
74 pub fn argon2_profile(mut self, profile: Argon2Profile) -> Self {
75 self.argon2_profile = profile;
76 self
77 }
78
79 pub fn cache_size(mut self, pages: usize) -> Self {
80 self.cache_size = pages;
81 self
82 }
83
84 pub fn cipher(mut self, cipher: CipherId) -> Self {
85 self.cipher = cipher;
86 self
87 }
88
89 pub fn kdf_algorithm(mut self, algorithm: KdfAlgorithm) -> Self {
95 self.kdf_algorithm = algorithm;
96 self
97 }
98
99 pub fn pbkdf2_iterations(mut self, iterations: u32) -> Self {
103 self.pbkdf2_iterations = iterations;
104 self
105 }
106
107 pub fn sync_mode(mut self, mode: SyncMode) -> Self {
108 self.sync_mode = mode;
109 self
110 }
111
112 #[cfg(feature = "audit-log")]
116 pub fn audit_config(mut self, config: crate::audit::AuditConfig) -> Self {
117 self.audit_config = config;
118 self
119 }
120
121 fn resolve_key_path(&self) -> PathBuf {
123 self.key_path.clone().unwrap_or_else(|| {
124 let mut name = self.path.as_os_str().to_os_string();
125 name.push(".citadel-keys");
126 PathBuf::from(name)
127 })
128 }
129
130 fn create_page_io(file: std::fs::File) -> Box<dyn PageIO> {
131 #[cfg(all(target_os = "linux", feature = "io-uring"))]
132 {
133 if let Some(uring) = citadel_io::uring_io::UringPageIO::try_new(
134 file.try_clone().expect("failed to clone file handle"),
135 ) {
136 return Box::new(uring);
137 }
138 }
139 Box::new(SyncPageIO::new(file))
140 }
141
142 fn resolve_kdf_params(&self) -> (u32, u32, u32) {
145 match self.kdf_algorithm {
146 KdfAlgorithm::Argon2id => {
147 let profile = self.argon2_profile;
148 (profile.m_cost(), profile.t_cost(), profile.p_cost())
149 }
150 KdfAlgorithm::Pbkdf2HmacSha256 => (self.pbkdf2_iterations, 0, 0),
151 }
152 }
153
154 #[cfg(feature = "fips")]
156 fn validate_fips(&self) -> Result<()> {
157 if self.kdf_algorithm != KdfAlgorithm::Pbkdf2HmacSha256 {
158 return Err(Error::FipsViolation(
159 "FIPS mode requires PBKDF2-HMAC-SHA256 (Argon2id is not NIST approved)".into(),
160 ));
161 }
162 if self.cipher == CipherId::ChaCha20 {
163 return Err(Error::FipsViolation(
164 "FIPS mode requires AES-256-CTR (ChaCha20 is not NIST approved)".into(),
165 ));
166 }
167 Ok(())
168 }
169
170 #[cfg(feature = "audit-log")]
173 fn finish(
174 self,
175 manager: TxnManager,
176 key_path: PathBuf,
177 file_id: u64,
178 audit_key: [u8; citadel_core::KEY_SIZE],
179 initial_event: Option<(crate::audit::AuditEventType, Vec<u8>)>,
180 ) -> Result<Database> {
181 use crate::audit;
182
183 let audit_log = if self.audit_config.enabled && !self.path.as_os_str().is_empty() {
184 let audit_path = audit::resolve_audit_path(&self.path);
185 let log = if audit_path.exists() {
186 audit::AuditLog::open_existing(&audit_path, file_id, audit_key, self.audit_config)?
187 } else {
188 audit::AuditLog::create(&audit_path, file_id, audit_key, self.audit_config)?
189 };
190 Some(log)
191 } else {
192 None
193 };
194
195 let db = Database::new(manager, self.path, key_path, audit_log);
196
197 if let Some((event, detail)) = initial_event {
198 db.log_audit(event, &detail);
199 }
200
201 Ok(db)
202 }
203
204 #[cfg(not(feature = "audit-log"))]
205 fn finish(
206 self,
207 manager: TxnManager,
208 key_path: PathBuf,
209 _file_id: u64,
210 _audit_key: [u8; citadel_core::KEY_SIZE],
211 _initial_event: Option<((), Vec<u8>)>,
212 ) -> Result<Database> {
213 Ok(Database::new(manager, self.path, key_path))
214 }
215
216 pub fn create(self) -> Result<Database> {
218 #[cfg(feature = "fips")]
219 self.validate_fips()?;
220
221 let passphrase = self
222 .passphrase
223 .as_deref()
224 .ok_or(Error::PassphraseRequired)?;
225
226 let key_path = self.resolve_key_path();
227 let file_id: u64 = rand::random();
228 let (m_cost, t_cost, p_cost) = self.resolve_kdf_params();
229
230 let (kf, keys) = create_key_file(
231 passphrase,
232 file_id,
233 self.cipher,
234 self.kdf_algorithm,
235 m_cost,
236 t_cost,
237 p_cost,
238 )?;
239
240 durable::write_and_sync(&key_path, &kf.serialize())?;
242
243 let file = OpenOptions::new()
245 .read(true)
246 .write(true)
247 .create_new(true)
248 .open(&self.path)?;
249
250 file_lock::try_lock_exclusive(&file)?;
251
252 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
253 let io = Self::create_page_io(file);
254
255 let manager = TxnManager::create_with_sync(
256 io,
257 keys.dek,
258 keys.mac_key,
259 kf.current_epoch,
260 file_id,
261 dek_id,
262 self.cache_size,
263 self.sync_mode,
264 )?;
265
266 #[cfg(feature = "audit-log")]
267 let event = {
268 let detail = vec![self.cipher as u8, self.kdf_algorithm as u8];
269 Some((crate::audit::AuditEventType::DatabaseCreated, detail))
270 };
271 #[cfg(not(feature = "audit-log"))]
272 let event: Option<((), Vec<u8>)> = None;
273
274 self.finish(manager, key_path, file_id, keys.audit_key, event)
275 }
276
277 pub fn create_in_memory(mut self) -> Result<Database> {
282 #[cfg(feature = "fips")]
283 self.validate_fips()?;
284
285 let passphrase = self
286 .passphrase
287 .as_deref()
288 .ok_or(Error::PassphraseRequired)?;
289
290 let file_id: u64 = rand::random();
291 let (m_cost, t_cost, p_cost) = self.resolve_kdf_params();
292
293 let (_kf, keys) = create_key_file(
294 passphrase,
295 file_id,
296 self.cipher,
297 self.kdf_algorithm,
298 m_cost,
299 t_cost,
300 p_cost,
301 )?;
302
303 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
304 let io: Box<dyn PageIO> = Box::new(citadel_io::memory_io::MemoryPageIO::new());
305
306 let manager = TxnManager::create_with_sync(
307 io,
308 keys.dek,
309 keys.mac_key,
310 1,
311 file_id,
312 dek_id,
313 self.cache_size,
314 self.sync_mode,
315 )?;
316
317 self.path = PathBuf::new();
319 self.finish(manager, PathBuf::new(), file_id, keys.audit_key, None)
320 }
321
322 pub fn open(self) -> Result<Database> {
324 let passphrase = self
325 .passphrase
326 .as_deref()
327 .ok_or(Error::PassphraseRequired)?;
328
329 let key_path = self.resolve_key_path();
330
331 let mut file = OpenOptions::new().read(true).write(true).open(&self.path)?;
333
334 file_lock::try_lock_exclusive(&file)?;
335
336 let mut header_buf = [0u8; FILE_HEADER_SIZE];
338 file.seek(SeekFrom::Start(0))?;
339 file.read_exact(&mut header_buf)?;
340 let header = FileHeader::deserialize(&header_buf)?;
341
342 let key_data = fs::read(&key_path)?;
344 if key_data.len() != KEY_FILE_SIZE {
345 return Err(Error::Io(std::io::Error::new(
346 std::io::ErrorKind::InvalidData,
347 "key file has incorrect size",
348 )));
349 }
350 let key_buf: [u8; KEY_FILE_SIZE] = key_data.try_into().unwrap();
351 let (kf, keys) = open_key_file(&key_buf, passphrase, header.file_id)?;
352
353 let dek_id = compute_dek_id(&keys.mac_key, &keys.dek);
354
355 let io = Self::create_page_io(file);
356
357 let manager = TxnManager::open_with_sync(
358 io,
359 keys.dek,
360 keys.mac_key,
361 kf.current_epoch,
362 self.cache_size,
363 self.sync_mode,
364 )?;
365
366 let slot = manager.current_slot();
368 if slot.dek_id != dek_id {
369 return Err(Error::BadPassphrase);
370 }
371
372 #[cfg(feature = "audit-log")]
373 let event = Some((crate::audit::AuditEventType::DatabaseOpened, vec![]));
374 #[cfg(not(feature = "audit-log"))]
375 let event: Option<((), Vec<u8>)> = None;
376
377 self.finish(manager, key_path, header.file_id, keys.audit_key, event)
378 }
379}