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