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