1use crate::crypto::aes_gcm::{aes256_gcm_decrypt, aes256_gcm_encrypt};
67use crate::crypto::hmac::hmac_sha256;
68use crate::crypto::os_random;
69use crate::storage::encryption::argon2id::{derive_key, Argon2Params};
70use crate::storage::encryption::key::SecureKey;
71use crate::storage::engine::page::{Page, PageType, CONTENT_SIZE, HEADER_SIZE};
72use crate::storage::engine::pager::Pager;
73
74use super::{ApiKey, AuthError, Role, User, UserId};
75
76const VAULT_MAGIC: &[u8; 4] = b"RDVT";
81const VAULT_DATA_MAGIC: &[u8; 4] = b"RDVD";
82
83const VAULT_VERSION: u8 = 2;
86
87const VAULT_LEGACY_VERSION: u8 = 1;
90
91const VAULT_AAD: &[u8] = b"reddb-vault";
92const VAULT_LOGICAL_EXPORT_MAGIC: &[u8; 4] = b"RDVX";
93const VAULT_LOGICAL_EXPORT_VERSION: u8 = 1;
94const VAULT_LOGICAL_EXPORT_AAD: &[u8] = b"reddb-vault-logical-export-v1";
95
96const VAULT_MAGIC_SIZE: usize = 4;
98const VAULT_VERSION_SIZE: usize = 1;
99const VAULT_SALT_SIZE: usize = 16;
100const VAULT_PAYLOAD_LEN_SIZE: usize = 4;
101const VAULT_CHAIN_COUNT_SIZE: usize = 4;
102const VAULT_FIRST_PAGE_ID_SIZE: usize = 4;
103
104const NONCE_SIZE: usize = 12;
106
107const VAULT_HEADER_PREAMBLE_SIZE: usize =
109 VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE + VAULT_SALT_SIZE + VAULT_PAYLOAD_LEN_SIZE; const VAULT_HEADER_META_SIZE: usize =
114 VAULT_HEADER_PREAMBLE_SIZE + NONCE_SIZE + VAULT_CHAIN_COUNT_SIZE + VAULT_FIRST_PAGE_ID_SIZE; const VAULT_DATA_PREFIX_SIZE: usize = VAULT_MAGIC_SIZE + 4; const VAULT_HEADER_PAGE: u32 = 2;
122
123const VAULT_HEADER_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_HEADER_META_SIZE;
126
127const VAULT_DATA_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_DATA_PREFIX_SIZE;
130
131pub struct KeyPair {
148 pub master_secret: Vec<u8>,
150 pub certificate: Vec<u8>,
152}
153
154impl KeyPair {
155 pub fn generate() -> Self {
157 let mut master_secret = vec![0u8; 32];
158 os_random::fill_bytes(&mut master_secret).expect("CSPRNG failed during keypair generation");
159 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
160 Self {
161 master_secret,
162 certificate: certificate.to_vec(),
163 }
164 }
165
166 pub fn from_master_secret(master_secret: Vec<u8>) -> Self {
169 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
170 Self {
171 master_secret,
172 certificate: certificate.to_vec(),
173 }
174 }
175
176 pub fn vault_key_from_certificate(certificate: &[u8]) -> SecureKey {
181 let key_bytes = derive_key(certificate, b"reddb-vault-seal", &vault_argon2_params());
182 SecureKey::new(&key_bytes)
183 }
184
185 pub fn sign(&self, data: &[u8]) -> Vec<u8> {
187 hmac_sha256(&self.master_secret, data).to_vec()
188 }
189
190 pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
192 let expected = self.sign(data);
193 constant_time_eq(&expected, signature)
194 }
195
196 pub fn certificate_hex(&self) -> String {
198 hex::encode(&self.certificate)
199 }
200}
201
202fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
204 if a.len() != b.len() {
205 return false;
206 }
207 let mut diff: u8 = 0;
208 for (x, y) in a.iter().zip(b.iter()) {
209 diff |= x ^ y;
210 }
211 diff == 0
212}
213
214#[derive(Debug)]
220pub enum VaultError {
221 NoKey,
223 Encryption,
225 Decryption,
227 Io(std::io::Error),
229 Corrupt(String),
231 Pager(String),
233}
234
235impl std::fmt::Display for VaultError {
236 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237 match self {
238 Self::NoKey => write!(
239 f,
240 "no vault key: set REDDB_CERTIFICATE (or REDDB_VAULT_KEY) or provide a certificate"
241 ),
242 Self::Encryption => write!(f, "vault encryption failed"),
243 Self::Decryption => write!(f, "vault decryption failed (wrong key or corrupt data)"),
244 Self::Io(err) => write!(f, "vault I/O error: {err}"),
245 Self::Corrupt(msg) => write!(f, "vault corrupt: {msg}"),
246 Self::Pager(msg) => write!(f, "vault pager error: {msg}"),
247 }
248 }
249}
250
251impl std::error::Error for VaultError {}
252
253impl From<VaultError> for AuthError {
254 fn from(err: VaultError) -> Self {
255 AuthError::Internal(err.to_string())
256 }
257}
258
259#[derive(Debug, Default)]
267pub struct VaultState {
268 pub users: Vec<User>,
269 pub api_keys: Vec<(UserId, ApiKey)>,
273 pub bootstrapped: bool,
274 pub master_secret: Option<Vec<u8>>,
278 pub kv: std::collections::HashMap<String, String>,
282}
283
284impl VaultState {
285 pub fn serialize(&self) -> Vec<u8> {
287 let mut out = String::new();
288
289 if let Some(ref secret) = self.master_secret {
291 out.push_str(&format!("MASTER_SECRET:{}\n", hex::encode(secret)));
292 }
293
294 out.push_str(&format!("SEALED:{}\n", self.bootstrapped));
296
297 for user in &self.users {
310 let scram_field = match &user.scram_verifier {
311 Some(v) => format!(
312 "{}:{}:{}:{}",
313 hex::encode(&v.salt),
314 v.iter,
315 hex::encode(v.stored_key),
316 hex::encode(v.server_key),
317 ),
318 None => String::new(),
319 };
320 let tenant_field = user.tenant_id.clone().unwrap_or_default();
321 out.push_str(&format!(
322 "USER:{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
323 user.username,
324 user.password_hash,
325 user.role.as_str(),
326 user.enabled,
327 user.created_at,
328 user.updated_at,
329 scram_field,
330 tenant_field,
331 user.system_owned,
332 ));
333 }
334
335 for (owner, key) in &self.api_keys {
339 let tenant_field = owner.tenant.clone().unwrap_or_default();
340 out.push_str(&format!(
341 "KEY:{}\t{}\t{}\t{}\t{}\t{}\n",
342 owner.username,
343 key.key,
344 key.name,
345 key.role.as_str(),
346 key.created_at,
347 tenant_field,
348 ));
349 }
350
351 for (k, v) in &self.kv {
353 out.push_str(&format!("KV:{}\t{}\n", k, hex::encode(v.as_bytes())));
354 }
355
356 out.into_bytes()
357 }
358
359 pub fn deserialize(data: &[u8]) -> Result<Self, VaultError> {
361 let text = std::str::from_utf8(data)
362 .map_err(|_| VaultError::Corrupt("payload is not valid UTF-8".into()))?;
363
364 let mut users = Vec::new();
365 let mut api_keys: Vec<(UserId, ApiKey)> = Vec::new();
366 let mut bootstrapped = false;
367 let mut master_secret: Option<Vec<u8>> = None;
368 let mut kv: std::collections::HashMap<String, String> = std::collections::HashMap::new();
369
370 for line in text.lines() {
371 if line.is_empty() {
372 continue;
373 }
374
375 if let Some(rest) = line.strip_prefix("MASTER_SECRET:") {
376 master_secret = Some(
377 hex::decode(rest)
378 .map_err(|_| VaultError::Corrupt("invalid MASTER_SECRET hex".into()))?,
379 );
380 } else if let Some(rest) = line.strip_prefix("SEALED:") {
381 bootstrapped = rest == "true";
382 } else if let Some(rest) = line.strip_prefix("USER:") {
383 let parts: Vec<&str> = rest.split('\t').collect();
384 if !(7..=9).contains(&parts.len()) {
387 return Err(VaultError::Corrupt(format!(
388 "USER line has {} fields, expected 7, 8, or 9",
389 parts.len()
390 )));
391 }
392 let role = Role::from_str(parts[2])
393 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[2])))?;
394 let enabled = parts[3] == "true";
395 let created_at: u128 = parts[4]
396 .parse()
397 .map_err(|_| VaultError::Corrupt("invalid created_at".into()))?;
398 let updated_at: u128 = parts[5]
399 .parse()
400 .map_err(|_| VaultError::Corrupt("invalid updated_at".into()))?;
401 let scram_verifier = parts
402 .get(6)
403 .map(|s| s.trim())
404 .filter(|s| !s.is_empty())
405 .map(parse_scram_field)
406 .transpose()?;
407 let tenant_id = parts
408 .get(7)
409 .map(|s| s.trim())
410 .filter(|s| !s.is_empty())
411 .map(|s| s.to_string());
412 let system_owned = parts.get(8).map(|s| s.trim() == "true").unwrap_or(false);
413
414 users.push(User {
415 username: parts[0].to_string(),
416 tenant_id,
417 password_hash: parts[1].to_string(),
418 scram_verifier,
419 role,
420 api_keys: Vec::new(), created_at,
422 updated_at,
423 enabled,
424 system_owned,
425 });
426 } else if let Some(rest) = line.strip_prefix("KEY:") {
427 let parts: Vec<&str> = rest.split('\t').collect();
428 if parts.len() != 5 && parts.len() != 6 {
430 return Err(VaultError::Corrupt(format!(
431 "KEY line has {} fields, expected 5 or 6",
432 parts.len()
433 )));
434 }
435 let role = Role::from_str(parts[3])
436 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[3])))?;
437 let created_at: u128 = parts[4]
438 .parse()
439 .map_err(|_| VaultError::Corrupt("invalid key created_at".into()))?;
440 let tenant_id = parts
441 .get(5)
442 .map(|s| s.trim())
443 .filter(|s| !s.is_empty())
444 .map(|s| s.to_string());
445
446 api_keys.push((
447 UserId {
448 tenant: tenant_id,
449 username: parts[0].to_string(),
450 },
451 ApiKey {
452 key: parts[1].to_string(),
453 name: parts[2].to_string(),
454 role,
455 created_at,
456 },
457 ));
458 } else if let Some(rest) = line.strip_prefix("KV:") {
459 let parts: Vec<&str> = rest.splitn(2, '\t').collect();
460 if parts.len() == 2 {
461 if let Ok(bytes) = hex::decode(parts[1]) {
462 if let Ok(value) = String::from_utf8(bytes) {
463 kv.insert(parts[0].to_string(), value);
464 }
465 }
466 }
467 } else {
468 }
470 }
471
472 for (owner, key) in &api_keys {
476 if let Some(user) = users
477 .iter_mut()
478 .find(|u| u.username == owner.username && u.tenant_id == owner.tenant)
479 {
480 user.api_keys.push(key.clone());
481 }
482 }
483
484 Ok(Self {
485 users,
486 api_keys,
487 bootstrapped,
488 master_secret,
489 kv,
490 })
491 }
492}
493
494pub struct Vault {
505 key: SecureKey,
506 salt: [u8; 16],
507}
508
509fn vault_argon2_params() -> Argon2Params {
512 Argon2Params {
513 m_cost: 16 * 1024, t_cost: 3,
515 p: 1,
516 tag_len: 32,
517 }
518}
519
520impl Vault {
521 pub fn has_saved_state(pager: &Pager) -> bool {
523 pager
524 .read_page_no_checksum(VAULT_HEADER_PAGE)
525 .ok()
526 .map(|page| {
527 let content = page.content();
528 content.len() >= VAULT_MAGIC_SIZE && &content[0..VAULT_MAGIC_SIZE] == VAULT_MAGIC
529 })
530 .unwrap_or(false)
531 }
532
533 pub fn open(pager: &Pager, passphrase: Option<&str>) -> Result<Self, VaultError> {
542 if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
544 return Self::with_certificate(pager, &cert_hex);
545 }
546
547 let passphrase_str = std::env::var("REDDB_VAULT_KEY")
549 .ok()
550 .or_else(|| passphrase.map(|s| s.to_string()))
551 .ok_or(VaultError::NoKey)?;
552
553 let salt = match read_vault_salt_from_pager(pager) {
555 Ok(s) => s,
556 Err(_) => {
557 let mut salt = [0u8; 16];
559 let mut buf = [0u8; 16];
560 os_random::fill_bytes(&mut buf)
561 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
562 salt.copy_from_slice(&buf);
563 salt
564 }
565 };
566
567 let key_bytes = derive_key(passphrase_str.as_bytes(), &salt, &vault_argon2_params());
568 let key = SecureKey::new(&key_bytes);
569
570 Ok(Self { key, salt })
571 }
572
573 pub fn with_certificate(pager: &Pager, certificate_hex: &str) -> Result<Self, VaultError> {
579 let certificate = hex::decode(certificate_hex).map_err(|_| VaultError::NoKey)?;
580
581 let key = KeyPair::vault_key_from_certificate(&certificate);
582
583 let salt = match read_vault_salt_from_pager(pager) {
585 Ok(s) => s,
586 Err(_) => {
587 let mut s = [0u8; 16];
589 os_random::fill_bytes(&mut s)
590 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
591 s
592 }
593 };
594
595 Ok(Self { key, salt })
596 }
597
598 pub fn from_env(pager: &Pager) -> Result<Self, VaultError> {
602 if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
603 return Self::with_certificate(pager, &cert_hex);
604 }
605 if let Ok(passphrase) = std::env::var("REDDB_VAULT_KEY") {
606 return Self::open_with_passphrase(pager, &passphrase);
607 }
608 Err(VaultError::NoKey)
609 }
610
611 fn open_with_passphrase(pager: &Pager, passphrase: &str) -> Result<Self, VaultError> {
613 let salt = match read_vault_salt_from_pager(pager) {
614 Ok(s) => s,
615 Err(_) => {
616 let mut s = [0u8; 16];
617 os_random::fill_bytes(&mut s)
618 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
619 s
620 }
621 };
622
623 let key_bytes = derive_key(passphrase.as_bytes(), &salt, &vault_argon2_params());
624 let key = SecureKey::new(&key_bytes);
625 Ok(Self { key, salt })
626 }
627
628 pub fn with_certificate_bytes(pager: &Pager, certificate: &[u8]) -> Result<Self, VaultError> {
633 let key = KeyPair::vault_key_from_certificate(certificate);
634
635 let salt = match read_vault_salt_from_pager(pager) {
636 Ok(s) => s,
637 Err(_) => {
638 let mut s = [0u8; 16];
639 os_random::fill_bytes(&mut s)
640 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
641 s
642 }
643 };
644
645 Ok(Self { key, salt })
646 }
647
648 pub fn seal_logical_export(&self, state: &VaultState) -> Result<String, VaultError> {
654 let plaintext = state.serialize();
655 let mut nonce = [0u8; NONCE_SIZE];
656 os_random::fill_bytes(&mut nonce)
657 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
658
659 let key_bytes: &[u8] = self.key.as_bytes();
660 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
661 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_LOGICAL_EXPORT_AAD, &plaintext);
662
663 let mut out = Vec::with_capacity(
664 VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE + VAULT_SALT_SIZE + NONCE_SIZE + ciphertext.len(),
665 );
666 out.extend_from_slice(VAULT_LOGICAL_EXPORT_MAGIC);
667 out.push(VAULT_LOGICAL_EXPORT_VERSION);
668 out.extend_from_slice(&self.salt);
669 out.extend_from_slice(&nonce);
670 out.extend_from_slice(&ciphertext);
671 Ok(hex::encode(out))
672 }
673
674 pub fn unseal_logical_export(
678 blob_hex: &str,
679 passphrase: Option<&str>,
680 ) -> Result<VaultState, VaultError> {
681 let (salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
682
683 let key = if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
684 let certificate = hex::decode(cert_hex).map_err(|_| VaultError::NoKey)?;
685 KeyPair::vault_key_from_certificate(&certificate)
686 } else {
687 let passphrase_str = std::env::var("REDDB_VAULT_KEY")
688 .ok()
689 .or_else(|| passphrase.map(|s| s.to_string()))
690 .ok_or(VaultError::NoKey)?;
691 let key_bytes = derive_key(passphrase_str.as_bytes(), &salt, &vault_argon2_params());
692 SecureKey::new(&key_bytes)
693 };
694
695 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
696 }
697
698 pub fn unseal_logical_export_with_passphrase(
700 blob_hex: &str,
701 passphrase: &str,
702 ) -> Result<VaultState, VaultError> {
703 let (salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
704 let key_bytes = derive_key(passphrase.as_bytes(), &salt, &vault_argon2_params());
705 let key = SecureKey::new(&key_bytes);
706 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
707 }
708
709 fn decode_logical_export(
710 blob_hex: &str,
711 ) -> Result<([u8; VAULT_SALT_SIZE], [u8; NONCE_SIZE], Vec<u8>), VaultError> {
712 let blob = hex::decode(blob_hex).map_err(|_| VaultError::Corrupt("bad hex".into()))?;
713 let min_len = VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE + VAULT_SALT_SIZE + NONCE_SIZE + 16;
714 if blob.len() < min_len {
715 return Err(VaultError::Corrupt("logical vault export too short".into()));
716 }
717 if &blob[0..VAULT_MAGIC_SIZE] != VAULT_LOGICAL_EXPORT_MAGIC {
718 return Err(VaultError::Corrupt(
719 "bad logical vault export magic".to_string(),
720 ));
721 }
722 let version = blob[VAULT_MAGIC_SIZE];
723 if version != VAULT_LOGICAL_EXPORT_VERSION {
724 return Err(VaultError::Corrupt(format!(
725 "unsupported logical vault export version: {version}"
726 )));
727 }
728
729 let mut off = VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE;
730 let salt: [u8; VAULT_SALT_SIZE] = blob[off..off + VAULT_SALT_SIZE]
731 .try_into()
732 .map_err(|_| VaultError::Corrupt("bad logical export salt".into()))?;
733 off += VAULT_SALT_SIZE;
734 let nonce: [u8; NONCE_SIZE] = blob[off..off + NONCE_SIZE]
735 .try_into()
736 .map_err(|_| VaultError::Corrupt("bad logical export nonce".into()))?;
737 off += NONCE_SIZE;
738 Ok((salt, nonce, blob[off..].to_vec()))
739 }
740
741 fn decrypt_logical_export(
742 key: &SecureKey,
743 nonce: &[u8; NONCE_SIZE],
744 ciphertext: &[u8],
745 ) -> Result<VaultState, VaultError> {
746 let key_bytes: &[u8] = key.as_bytes();
747 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
748 let plaintext = aes256_gcm_decrypt(key_arr, nonce, VAULT_LOGICAL_EXPORT_AAD, ciphertext)
749 .map_err(|_| VaultError::Decryption)?;
750 VaultState::deserialize(&plaintext)
751 }
752
753 pub fn save(&self, pager: &Pager, state: &VaultState) -> Result<(), VaultError> {
767 let plaintext = state.serialize();
768
769 let mut nonce = [0u8; NONCE_SIZE];
771 os_random::fill_bytes(&mut nonce)
772 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
773
774 let key_bytes: &[u8] = self.key.as_bytes();
775 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
776 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_AAD, &plaintext);
777 let cipher_total = ciphertext.len();
781 let payload_len = (NONCE_SIZE + cipher_total) as u32; let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
789 let overflow = cipher_total.saturating_sub(header_chunk_len);
790 let chain_count = overflow.div_ceil(VAULT_DATA_CIPHER_CAPACITY);
791
792 while pager
806 .page_count()
807 .map_err(|e| VaultError::Pager(e.to_string()))?
808 <= VAULT_HEADER_PAGE
809 {
810 pager
811 .allocate_page(PageType::Vault)
812 .map_err(|e| VaultError::Pager(format!("reserve vault slot: {e}")))?;
813 }
814
815 let old_chain = self.read_existing_chain_ids(pager).unwrap_or_default();
825
826 let mut new_chain: Vec<u32> = Vec::with_capacity(chain_count);
833 for _ in 0..chain_count {
834 let page = pager
835 .allocate_page(PageType::Vault)
836 .map_err(|e| VaultError::Pager(format!("allocate vault data page: {e}")))?;
837 new_chain.push(page.page_id());
838 }
839
840 let mut cursor = header_chunk_len;
847 for i in 0..chain_count {
848 let next_id = if i + 1 < chain_count {
849 new_chain[i + 1]
850 } else {
851 0
852 };
853 let take = (cipher_total - cursor).min(VAULT_DATA_CIPHER_CAPACITY);
854 let frag = &ciphertext[cursor..cursor + take];
855 self.write_data_page(pager, new_chain[i], next_id, frag)?;
856 cursor += take;
857 }
858 debug_assert_eq!(cursor, cipher_total, "ciphertext spill accounting mismatch");
859
860 let first_data_page = new_chain.first().copied().unwrap_or(0);
868 self.write_header_page(
869 pager,
870 &nonce,
871 payload_len,
872 chain_count as u32,
873 first_data_page,
874 &ciphertext[..header_chunk_len],
875 )?;
876
877 pager
882 .flush()
883 .map_err(|e| VaultError::Pager(e.to_string()))?;
884
885 for &id in old_chain.iter() {
890 pager
891 .free_page(id)
892 .map_err(|e| VaultError::Pager(format!("free old vault page {id}: {e}")))?;
893 }
894
895 Ok(())
896 }
897
898 pub fn load(&self, pager: &Pager) -> Result<Option<VaultState>, VaultError> {
902 let page = match pager.read_page_no_checksum(VAULT_HEADER_PAGE) {
904 Ok(p) => p,
905 Err(_) => return Ok(None),
906 };
907
908 let page_content = page.content();
909
910 if page_content.len() < VAULT_HEADER_META_SIZE {
911 return Ok(None);
912 }
913 if &page_content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
914 return Ok(None); }
916
917 let version = page_content[4];
918 if version == VAULT_LEGACY_VERSION {
919 return Err(VaultError::Corrupt(
923 "vault was bootstrapped with the legacy 2-page format \
924 (pre-RedDB v0.3); re-bootstrap with `red bootstrap` to upgrade"
925 .to_string(),
926 ));
927 }
928 if version != VAULT_VERSION {
929 return Err(VaultError::Corrupt(format!(
930 "unsupported vault version: {} (expected {})",
931 version, VAULT_VERSION
932 )));
933 }
934
935 let payload_len = u32::from_le_bytes(
937 page_content[21..25]
938 .try_into()
939 .map_err(|_| VaultError::Corrupt("bad payload length bytes".into()))?,
940 ) as usize;
941
942 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
943 let nonce: [u8; NONCE_SIZE] = page_content[nonce_start..nonce_start + NONCE_SIZE]
944 .try_into()
945 .map_err(|_| VaultError::Corrupt("bad nonce".into()))?;
946
947 let chain_count_off = nonce_start + NONCE_SIZE;
948 let chain_count = u32::from_le_bytes(
949 page_content[chain_count_off..chain_count_off + 4]
950 .try_into()
951 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
952 ) as usize;
953 let first_id_off = chain_count_off + 4;
954 let mut next_id = u32::from_le_bytes(
955 page_content[first_id_off..first_id_off + 4]
956 .try_into()
957 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
958 );
959
960 if payload_len < NONCE_SIZE {
961 return Err(VaultError::Corrupt("payload too short for nonce".into()));
962 }
963 let cipher_total = payload_len - NONCE_SIZE;
964
965 let mut cipher = Vec::with_capacity(cipher_total);
967 let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
968 let header_cipher_start = VAULT_HEADER_META_SIZE;
969 cipher.extend_from_slice(
970 &page_content[header_cipher_start..header_cipher_start + header_chunk_len],
971 );
972
973 let mut hops = 0usize;
975 while cipher.len() < cipher_total {
979 if hops >= chain_count {
980 return Err(VaultError::Corrupt(format!(
981 "vault chain shorter than declared: {} hops, expected {}",
982 hops, chain_count
983 )));
984 }
985 if next_id == 0 {
986 return Err(VaultError::Corrupt(
987 "vault chain ends prematurely (next_id == 0)".to_string(),
988 ));
989 }
990
991 let dp = pager
992 .read_page_no_checksum(next_id)
993 .map_err(|e| VaultError::Pager(format!("vault data page {next_id}: {e}")))?;
994 let dp_content = dp.content();
995 if dp_content.len() < VAULT_DATA_PREFIX_SIZE {
996 return Err(VaultError::Corrupt(format!(
997 "vault data page {next_id} truncated"
998 )));
999 }
1000 if &dp_content[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC {
1001 return Err(VaultError::Corrupt(format!(
1002 "vault data page {next_id} has bad magic"
1003 )));
1004 }
1005 let np = u32::from_le_bytes(
1006 dp_content[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1007 .try_into()
1008 .map_err(|_| VaultError::Corrupt("bad next_page_id bytes".into()))?,
1009 );
1010 let take = (cipher_total - cipher.len()).min(VAULT_DATA_CIPHER_CAPACITY);
1011 let frag_start = VAULT_DATA_PREFIX_SIZE;
1012 cipher.extend_from_slice(&dp_content[frag_start..frag_start + take]);
1013
1014 next_id = np;
1015 hops += 1;
1016 }
1017
1018 if cipher.len() != cipher_total {
1019 return Err(VaultError::Corrupt(format!(
1020 "vault truncated: expected {} cipher bytes, got {}",
1021 cipher_total,
1022 cipher.len()
1023 )));
1024 }
1025 if hops != chain_count {
1026 return Err(VaultError::Corrupt(format!(
1027 "vault chain length mismatch: walked {} pages, header says {}",
1028 hops, chain_count
1029 )));
1030 }
1031
1032 let key_bytes: &[u8] = self.key.as_bytes();
1034 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
1035 let plaintext = aes256_gcm_decrypt(key_arr, &nonce, VAULT_AAD, &cipher)
1036 .map_err(|_| VaultError::Decryption)?;
1037
1038 let state = VaultState::deserialize(&plaintext)?;
1039 Ok(Some(state))
1040 }
1041
1042 fn read_existing_chain_ids(&self, pager: &Pager) -> Result<Vec<u32>, VaultError> {
1049 let header = pager
1050 .read_page_no_checksum(VAULT_HEADER_PAGE)
1051 .map_err(|e| VaultError::Pager(e.to_string()))?;
1052 let content = header.content();
1053 if content.len() < VAULT_HEADER_META_SIZE {
1054 return Ok(Vec::new());
1055 }
1056 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1057 return Ok(Vec::new());
1058 }
1059 let version = content[4];
1060 if version != VAULT_VERSION {
1061 return Ok(Vec::new());
1065 }
1066 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
1067 let chain_count_off = nonce_start + NONCE_SIZE;
1068 let chain_count = u32::from_le_bytes(
1069 content[chain_count_off..chain_count_off + 4]
1070 .try_into()
1071 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1072 ) as usize;
1073 let first_id_off = chain_count_off + 4;
1074 let mut id = u32::from_le_bytes(
1075 content[first_id_off..first_id_off + 4]
1076 .try_into()
1077 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1078 );
1079
1080 let mut out = Vec::with_capacity(chain_count);
1081 let mut hops = 0usize;
1082 while id != 0 && hops < chain_count {
1083 out.push(id);
1084 match pager.read_page_no_checksum(id) {
1087 Ok(dp) => {
1088 let dc = dp.content();
1089 if dc.len() < VAULT_DATA_PREFIX_SIZE
1090 || &dc[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC
1091 {
1092 break;
1093 }
1094 id = u32::from_le_bytes(
1095 dc[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1096 .try_into()
1097 .map_err(|_| VaultError::Corrupt("bad next_id".into()))?,
1098 );
1099 }
1100 Err(_) => break,
1101 }
1102 hops += 1;
1103 }
1104 Ok(out)
1105 }
1106
1107 fn write_header_page(
1111 &self,
1112 pager: &Pager,
1113 nonce: &[u8; NONCE_SIZE],
1114 payload_len: u32,
1115 chain_count: u32,
1116 first_data_page_id: u32,
1117 cipher_fragment: &[u8],
1118 ) -> Result<(), VaultError> {
1119 debug_assert!(cipher_fragment.len() <= VAULT_HEADER_CIPHER_CAPACITY);
1120
1121 let mut page = Page::new(PageType::Vault, VAULT_HEADER_PAGE);
1122 let bytes = page.as_bytes_mut();
1123 let mut off = HEADER_SIZE;
1124
1125 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_MAGIC);
1126 off += VAULT_MAGIC_SIZE;
1127
1128 bytes[off] = VAULT_VERSION;
1129 off += VAULT_VERSION_SIZE;
1130
1131 bytes[off..off + VAULT_SALT_SIZE].copy_from_slice(&self.salt);
1132 off += VAULT_SALT_SIZE;
1133
1134 bytes[off..off + 4].copy_from_slice(&payload_len.to_le_bytes());
1135 off += VAULT_PAYLOAD_LEN_SIZE;
1136
1137 bytes[off..off + NONCE_SIZE].copy_from_slice(nonce);
1138 off += NONCE_SIZE;
1139
1140 bytes[off..off + 4].copy_from_slice(&chain_count.to_le_bytes());
1141 off += VAULT_CHAIN_COUNT_SIZE;
1142
1143 bytes[off..off + 4].copy_from_slice(&first_data_page_id.to_le_bytes());
1144 off += VAULT_FIRST_PAGE_ID_SIZE;
1145
1146 debug_assert_eq!(off, HEADER_SIZE + VAULT_HEADER_META_SIZE);
1147
1148 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1149
1150 pager
1151 .write_page_no_checksum(VAULT_HEADER_PAGE, page)
1152 .map_err(|e| VaultError::Pager(e.to_string()))?;
1153 Ok(())
1154 }
1155
1156 fn write_data_page(
1158 &self,
1159 pager: &Pager,
1160 page_id: u32,
1161 next_page_id: u32,
1162 cipher_fragment: &[u8],
1163 ) -> Result<(), VaultError> {
1164 debug_assert!(cipher_fragment.len() <= VAULT_DATA_CIPHER_CAPACITY);
1165
1166 let mut page = Page::new(PageType::Vault, page_id);
1167 let bytes = page.as_bytes_mut();
1168 let mut off = HEADER_SIZE;
1169
1170 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_DATA_MAGIC);
1171 off += VAULT_MAGIC_SIZE;
1172
1173 bytes[off..off + 4].copy_from_slice(&next_page_id.to_le_bytes());
1174 off += 4;
1175
1176 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1177
1178 pager
1179 .write_page_no_checksum(page_id, page)
1180 .map_err(|e| VaultError::Pager(e.to_string()))?;
1181 Ok(())
1182 }
1183}
1184
1185fn parse_scram_field(field: &str) -> Result<crate::auth::scram::ScramVerifier, VaultError> {
1192 let parts: Vec<&str> = field.split(':').collect();
1193 if parts.len() != 4 {
1194 return Err(VaultError::Corrupt(format!(
1195 "SCRAM verifier has {} segments, expected 4",
1196 parts.len()
1197 )));
1198 }
1199 let salt =
1200 hex::decode(parts[0]).map_err(|_| VaultError::Corrupt("invalid SCRAM salt hex".into()))?;
1201 let iter: u32 = parts[1]
1202 .parse()
1203 .map_err(|_| VaultError::Corrupt("invalid SCRAM iter".into()))?;
1204 if iter < crate::auth::scram::MIN_ITER {
1205 return Err(VaultError::Corrupt(format!(
1206 "SCRAM iter {} below minimum {}",
1207 iter,
1208 crate::auth::scram::MIN_ITER
1209 )));
1210 }
1211 let stored_vec = hex::decode(parts[2])
1212 .map_err(|_| VaultError::Corrupt("invalid SCRAM stored_key hex".into()))?;
1213 let server_vec = hex::decode(parts[3])
1214 .map_err(|_| VaultError::Corrupt("invalid SCRAM server_key hex".into()))?;
1215 let stored_key: [u8; 32] = stored_vec
1216 .try_into()
1217 .map_err(|_| VaultError::Corrupt("SCRAM stored_key must be 32 bytes".into()))?;
1218 let server_key: [u8; 32] = server_vec
1219 .try_into()
1220 .map_err(|_| VaultError::Corrupt("SCRAM server_key must be 32 bytes".into()))?;
1221 Ok(crate::auth::scram::ScramVerifier {
1222 salt,
1223 iter,
1224 stored_key,
1225 server_key,
1226 })
1227}
1228
1229fn read_vault_salt_from_pager(pager: &Pager) -> Result<[u8; 16], VaultError> {
1236 let page = pager
1237 .read_page_no_checksum(VAULT_HEADER_PAGE)
1238 .map_err(|e| VaultError::Pager(format!("vault page read: {e}")))?;
1239
1240 let content = page.content();
1241 if content.len() < VAULT_HEADER_PREAMBLE_SIZE {
1242 return Err(VaultError::Corrupt("vault page too short".into()));
1243 }
1244 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1245 return Err(VaultError::Corrupt("bad magic bytes".into()));
1246 }
1247
1248 let mut salt = [0u8; VAULT_SALT_SIZE];
1249 salt.copy_from_slice(&content[5..21]);
1250 Ok(salt)
1251}
1252
1253#[cfg(test)]
1258mod tests {
1259 use super::*;
1260 use crate::auth::{now_ms, ApiKey, Role, User};
1261 use crate::storage::engine::pager::PagerConfig;
1262
1263 fn sample_state() -> VaultState {
1264 let now = now_ms();
1265 VaultState {
1266 users: vec![
1267 User {
1268 username: "alice".into(),
1269 tenant_id: None,
1270 password_hash: "argon2id$aabbccdd$eeff0011".into(),
1271 scram_verifier: None,
1272 role: Role::Admin,
1273 api_keys: vec![ApiKey {
1274 key: "rk_abc123".into(),
1275 name: "ci-token".into(),
1276 role: Role::Write,
1277 created_at: now,
1278 }],
1279 created_at: now,
1280 updated_at: now,
1281 enabled: true,
1282 system_owned: false,
1283 },
1284 User {
1285 username: "bob".into(),
1286 tenant_id: None,
1287 password_hash: "argon2id$11223344$55667788".into(),
1288 scram_verifier: None,
1289 role: Role::Read,
1290 api_keys: vec![],
1291 created_at: now,
1292 updated_at: now,
1293 enabled: false,
1294 system_owned: false,
1295 },
1296 ],
1297 api_keys: vec![(
1298 UserId::platform("alice"),
1299 ApiKey {
1300 key: "rk_abc123".into(),
1301 name: "ci-token".into(),
1302 role: Role::Write,
1303 created_at: now,
1304 },
1305 )],
1306 bootstrapped: true,
1307 master_secret: None,
1308 kv: std::collections::HashMap::new(),
1309 }
1310 }
1311
1312 fn temp_pager() -> (Pager, std::path::PathBuf) {
1314 use std::sync::atomic::{AtomicU64, Ordering};
1315 static COUNTER: AtomicU64 = AtomicU64::new(0);
1316 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1317 let tmp_dir =
1318 std::env::temp_dir().join(format!("reddb_vault_test_{}_{}", std::process::id(), id));
1319 std::fs::create_dir_all(&tmp_dir).unwrap();
1320 let db_path = tmp_dir.join("test.rdb");
1321 let pager = Pager::open(&db_path, PagerConfig::default()).unwrap();
1322 (pager, tmp_dir)
1323 }
1324
1325 #[test]
1326 fn test_vault_state_serialize_deserialize_roundtrip() {
1327 let state = sample_state();
1328 let serialized = state.serialize();
1329 let text = std::str::from_utf8(&serialized).unwrap();
1330
1331 assert!(text.contains("SEALED:true"));
1333 assert!(text.contains("USER:alice\t"));
1334 assert!(text.contains("USER:bob\t"));
1335 assert!(text.contains("KEY:alice\trk_abc123\t"));
1336
1337 let restored = VaultState::deserialize(&serialized).unwrap();
1339 assert!(restored.bootstrapped);
1340 assert_eq!(restored.users.len(), 2);
1341
1342 let alice = restored
1343 .users
1344 .iter()
1345 .find(|u| u.username == "alice")
1346 .unwrap();
1347 assert_eq!(alice.role, Role::Admin);
1348 assert!(alice.enabled);
1349 assert_eq!(alice.password_hash, "argon2id$aabbccdd$eeff0011");
1350 assert_eq!(alice.api_keys.len(), 1);
1351 assert_eq!(alice.api_keys[0].key, "rk_abc123");
1352 assert_eq!(alice.api_keys[0].name, "ci-token");
1353 assert_eq!(alice.api_keys[0].role, Role::Write);
1354
1355 let bob = restored.users.iter().find(|u| u.username == "bob").unwrap();
1356 assert_eq!(bob.role, Role::Read);
1357 assert!(!bob.enabled);
1358 assert!(bob.api_keys.is_empty());
1359
1360 assert_eq!(restored.api_keys.len(), 1);
1361 assert_eq!(restored.api_keys[0].0.username, "alice");
1362 assert!(restored.api_keys[0].0.tenant.is_none());
1363 }
1364
1365 #[test]
1366 fn test_vault_state_empty() {
1367 let state = VaultState {
1368 users: vec![],
1369 api_keys: vec![],
1370 bootstrapped: false,
1371 master_secret: None,
1372 kv: std::collections::HashMap::new(),
1373 };
1374 let serialized = state.serialize();
1375 let restored = VaultState::deserialize(&serialized).unwrap();
1376 assert!(!restored.bootstrapped);
1377 assert!(restored.users.is_empty());
1378 assert!(restored.api_keys.is_empty());
1379 }
1380
1381 #[test]
1382 fn test_vault_state_deserialize_invalid_utf8() {
1383 let bad_data = vec![0xFF, 0xFE, 0xFD];
1384 let result = VaultState::deserialize(&bad_data);
1385 assert!(result.is_err());
1386 }
1387
1388 #[test]
1389 fn test_vault_state_deserialize_bad_user_line() {
1390 let bad = b"USER:only_two\tfields\n";
1391 let result = VaultState::deserialize(bad);
1392 assert!(result.is_err());
1393 }
1394
1395 #[test]
1396 fn test_vault_state_deserialize_bad_key_line() {
1397 let bad = b"KEY:too\tfew\n";
1398 let result = VaultState::deserialize(bad);
1399 assert!(result.is_err());
1400 }
1401
1402 #[test]
1403 fn test_vault_state_deserialize_unknown_line_skipped() {
1404 let data = b"SEALED:false\nFUTURE:some_data\n";
1405 let result = VaultState::deserialize(data).unwrap();
1406 assert!(!result.bootstrapped);
1407 }
1408
1409 #[test]
1410 fn test_vault_pager_save_load_roundtrip() {
1411 let (pager, tmp_dir) = temp_pager();
1412
1413 let vault = Vault::open(&pager, Some("test-passphrase-42")).unwrap();
1414
1415 let loaded = vault.load(&pager).unwrap();
1417 assert!(loaded.is_none());
1418
1419 let state = sample_state();
1421 vault.save(&pager, &state).unwrap();
1422
1423 let restored = vault.load(&pager).unwrap().unwrap();
1425 assert!(restored.bootstrapped);
1426 assert_eq!(restored.users.len(), 2);
1427 assert_eq!(restored.api_keys.len(), 1);
1428
1429 let alice = restored
1430 .users
1431 .iter()
1432 .find(|u| u.username == "alice")
1433 .unwrap();
1434 assert_eq!(alice.role, Role::Admin);
1435 assert_eq!(alice.api_keys.len(), 1);
1436
1437 let vault2 = Vault::open(&pager, Some("test-passphrase-42")).unwrap();
1439 let restored2 = vault2.load(&pager).unwrap().unwrap();
1440 assert!(restored2.bootstrapped);
1441 assert_eq!(restored2.users.len(), 2);
1442
1443 drop(pager);
1445 let _ = std::fs::remove_dir_all(&tmp_dir);
1446 }
1447
1448 #[test]
1449 fn test_vault_wrong_key_fails_decryption() {
1450 let (pager, tmp_dir) = temp_pager();
1451
1452 let vault = Vault::open(&pager, Some("correct-key")).unwrap();
1454 let state = VaultState {
1455 users: vec![],
1456 api_keys: vec![],
1457 bootstrapped: true,
1458 master_secret: None,
1459 kv: std::collections::HashMap::new(),
1460 };
1461 vault.save(&pager, &state).unwrap();
1462
1463 let vault2 = Vault::open(&pager, Some("wrong-key")).unwrap();
1465 let result = vault2.load(&pager);
1466
1467 assert!(result.is_err());
1468
1469 drop(pager);
1471 let _ = std::fs::remove_dir_all(&tmp_dir);
1472 }
1473
1474 #[test]
1475 fn test_vault_no_key_error() {
1476 let (pager, tmp_dir) = temp_pager();
1477
1478 let result = Vault::open(&pager, None);
1479 let has_env_key =
1483 std::env::var("REDDB_VAULT_KEY").is_ok() || std::env::var("REDDB_CERTIFICATE").is_ok();
1484 match has_env_key {
1485 true => {
1486 assert!(result.is_ok());
1488 }
1489 false => {
1490 assert!(matches!(result, Err(VaultError::NoKey)));
1491 }
1492 }
1493
1494 drop(pager);
1496 let _ = std::fs::remove_dir_all(&tmp_dir);
1497 }
1498
1499 #[test]
1500 fn test_vault_passphrase_argument() {
1501 let (pager, tmp_dir) = temp_pager();
1502
1503 let vault = Vault::open(&pager, Some("my-passphrase")).unwrap();
1505 let state = VaultState {
1506 users: vec![],
1507 api_keys: vec![],
1508 bootstrapped: false,
1509 master_secret: None,
1510 kv: std::collections::HashMap::new(),
1511 };
1512 vault.save(&pager, &state).unwrap();
1513
1514 let vault2 = Vault::open(&pager, Some("my-passphrase")).unwrap();
1516 let loaded = vault2.load(&pager).unwrap().unwrap();
1517 assert!(!loaded.bootstrapped);
1518
1519 drop(pager);
1520 let _ = std::fs::remove_dir_all(&tmp_dir);
1521 }
1522
1523 #[test]
1528 fn test_keypair_generate_deterministic_certificate() {
1529 let kp = KeyPair::generate();
1530 assert_eq!(kp.master_secret.len(), 32);
1531 assert_eq!(kp.certificate.len(), 32);
1532
1533 let kp2 = KeyPair::from_master_secret(kp.master_secret.clone());
1535 assert_eq!(kp.certificate, kp2.certificate);
1536 }
1537
1538 #[test]
1539 fn test_keypair_sign_verify() {
1540 let kp = KeyPair::generate();
1541 let data = b"session:abc123";
1542 let sig = kp.sign(data);
1543 assert!(kp.verify(data, &sig));
1544
1545 assert!(!kp.verify(b"session:wrong", &sig));
1547
1548 let mut bad_sig = sig.clone();
1550 bad_sig[0] ^= 0xFF;
1551 assert!(!kp.verify(data, &bad_sig));
1552 }
1553
1554 #[test]
1555 fn test_keypair_certificate_hex() {
1556 let kp = KeyPair::generate();
1557 let hex_str = kp.certificate_hex();
1558 assert_eq!(hex_str.len(), 64); let decoded = hex::decode(&hex_str).unwrap();
1560 assert_eq!(decoded, kp.certificate);
1561 }
1562
1563 #[test]
1564 fn test_vault_certificate_seal_roundtrip() {
1565 let (pager, tmp_dir) = temp_pager();
1566
1567 let kp = KeyPair::generate();
1569 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1570
1571 let state = VaultState {
1573 users: vec![],
1574 api_keys: vec![],
1575 bootstrapped: true,
1576 master_secret: Some(kp.master_secret.clone()),
1577 kv: std::collections::HashMap::new(),
1578 };
1579 vault.save(&pager, &state).unwrap();
1580
1581 let vault2 = Vault::with_certificate(&pager, &kp.certificate_hex()).unwrap();
1583 let loaded = vault2.load(&pager).unwrap().unwrap();
1584 assert!(loaded.bootstrapped);
1585 assert_eq!(loaded.master_secret, Some(kp.master_secret.clone()));
1586
1587 let kp2 = KeyPair::from_master_secret(loaded.master_secret.unwrap());
1589 assert_eq!(kp.certificate, kp2.certificate);
1590
1591 drop(pager);
1592 let _ = std::fs::remove_dir_all(&tmp_dir);
1593 }
1594
1595 #[test]
1596 fn test_vault_certificate_wrong_cert_fails() {
1597 let (pager, tmp_dir) = temp_pager();
1598
1599 let kp = KeyPair::generate();
1601 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1602 let state = VaultState {
1603 users: vec![],
1604 api_keys: vec![],
1605 bootstrapped: true,
1606 master_secret: Some(kp.master_secret.clone()),
1607 kv: std::collections::HashMap::new(),
1608 };
1609 vault.save(&pager, &state).unwrap();
1610
1611 let kp2 = KeyPair::generate();
1613 let vault2 = Vault::with_certificate_bytes(&pager, &kp2.certificate).unwrap();
1614 let result = vault2.load(&pager);
1615 assert!(result.is_err());
1616
1617 drop(pager);
1618 let _ = std::fs::remove_dir_all(&tmp_dir);
1619 }
1620
1621 #[test]
1622 fn test_vault_state_master_secret_serialization() {
1623 let secret = vec![0xAA; 32];
1624 let state = VaultState {
1625 users: vec![],
1626 api_keys: vec![],
1627 bootstrapped: true,
1628 master_secret: Some(secret.clone()),
1629 kv: std::collections::HashMap::new(),
1630 };
1631 let serialized = state.serialize();
1632 let text = std::str::from_utf8(&serialized).unwrap();
1633 assert!(text.contains("MASTER_SECRET:"));
1634 assert!(text.contains(&hex::encode(&secret)));
1635
1636 let restored = VaultState::deserialize(&serialized).unwrap();
1637 assert_eq!(restored.master_secret, Some(secret));
1638 assert!(restored.bootstrapped);
1639 }
1640
1641 #[test]
1642 fn test_vault_state_no_master_secret_backward_compat() {
1643 let data = b"SEALED:true\n";
1645 let restored = VaultState::deserialize(data).unwrap();
1646 assert!(restored.master_secret.is_none());
1647 assert!(restored.bootstrapped);
1648 }
1649
1650 #[test]
1651 fn test_vault_state_scram_verifier_roundtrip() {
1652 use crate::auth::scram::ScramVerifier;
1653
1654 let verifier = ScramVerifier::from_password(
1655 "hunter2",
1656 b"reddb-vault-test-salt".to_vec(),
1657 crate::auth::scram::DEFAULT_ITER,
1658 );
1659
1660 let now = now_ms();
1661 let state = VaultState {
1662 users: vec![User {
1663 username: "carol".into(),
1664 tenant_id: None,
1665 password_hash: "argon2id$abc$def".into(),
1666 scram_verifier: Some(verifier.clone()),
1667 role: Role::Admin,
1668 api_keys: vec![],
1669 created_at: now,
1670 updated_at: now,
1671 enabled: true,
1672 system_owned: true,
1673 }],
1674 api_keys: vec![],
1675 bootstrapped: true,
1676 master_secret: None,
1677 kv: std::collections::HashMap::new(),
1678 };
1679
1680 let bytes = state.serialize();
1681 let restored = VaultState::deserialize(&bytes).unwrap();
1682 let carol = restored
1683 .users
1684 .iter()
1685 .find(|u| u.username == "carol")
1686 .unwrap();
1687 let v = carol.scram_verifier.as_ref().expect("verifier round-trips");
1688 assert_eq!(v.salt, verifier.salt);
1689 assert_eq!(v.iter, verifier.iter);
1690 assert_eq!(v.stored_key, verifier.stored_key);
1691 assert_eq!(v.server_key, verifier.server_key);
1692 }
1693
1694 #[test]
1695 fn test_vault_state_pre_tenant_user_line_still_parses() {
1696 let now = now_ms();
1700 let line = format!(
1701 "USER:dave\targon2id$x$y\tread\ttrue\t{}\t{}\t\nSEALED:false\n",
1702 now, now
1703 );
1704 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1705 let dave = restored
1706 .users
1707 .iter()
1708 .find(|u| u.username == "dave")
1709 .unwrap();
1710 assert!(dave.scram_verifier.is_none());
1711 assert!(dave.tenant_id.is_none());
1712 assert!(!dave.system_owned);
1713 }
1714
1715 #[test]
1716 fn test_vault_state_legacy_tenant_user_line_defaults_system_owned_false() {
1717 let now = now_ms();
1718 let line = format!(
1719 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\nSEALED:false\n",
1720 now, now
1721 );
1722 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1723 let erin = restored
1724 .users
1725 .iter()
1726 .find(|u| u.username == "erin")
1727 .unwrap();
1728 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1729 assert!(!erin.system_owned);
1730 }
1731
1732 #[test]
1733 fn test_vault_state_user_line_with_tenant_roundtrip() {
1734 let now = now_ms();
1735 let state = VaultState {
1736 users: vec![User {
1737 username: "alice".into(),
1738 tenant_id: Some("acme".into()),
1739 password_hash: "argon2id$x$y".into(),
1740 scram_verifier: None,
1741 role: Role::Write,
1742 api_keys: vec![],
1743 created_at: now,
1744 updated_at: now,
1745 enabled: true,
1746 system_owned: true,
1747 }],
1748 api_keys: vec![],
1749 bootstrapped: true,
1750 master_secret: None,
1751 kv: std::collections::HashMap::new(),
1752 };
1753 let bytes = state.serialize();
1754 let text = std::str::from_utf8(&bytes).unwrap();
1755 assert!(text.contains("\tacme\ttrue\n"));
1757
1758 let restored = VaultState::deserialize(&bytes).unwrap();
1759 let alice = restored
1760 .users
1761 .iter()
1762 .find(|u| u.username == "alice")
1763 .unwrap();
1764 assert_eq!(alice.tenant_id.as_deref(), Some("acme"));
1765 assert!(alice.system_owned);
1766 }
1767
1768 #[test]
1769 fn test_vault_state_key_line_with_tenant_reattaches_correctly() {
1770 let now = now_ms();
1773 let state = VaultState {
1774 users: vec![
1775 User {
1776 username: "alice".into(),
1777 tenant_id: Some("acme".into()),
1778 password_hash: "argon2id$x$y".into(),
1779 scram_verifier: None,
1780 role: Role::Write,
1781 api_keys: vec![],
1782 created_at: now,
1783 updated_at: now,
1784 enabled: true,
1785 system_owned: false,
1786 },
1787 User {
1788 username: "alice".into(),
1789 tenant_id: Some("globex".into()),
1790 password_hash: "argon2id$a$b".into(),
1791 scram_verifier: None,
1792 role: Role::Read,
1793 api_keys: vec![],
1794 created_at: now,
1795 updated_at: now,
1796 enabled: true,
1797 system_owned: false,
1798 },
1799 ],
1800 api_keys: vec![
1801 (
1802 UserId::scoped("acme", "alice"),
1803 ApiKey {
1804 key: "rk_acme_key".into(),
1805 name: "deploy".into(),
1806 role: Role::Write,
1807 created_at: now,
1808 },
1809 ),
1810 (
1811 UserId::scoped("globex", "alice"),
1812 ApiKey {
1813 key: "rk_globex_key".into(),
1814 name: "ci".into(),
1815 role: Role::Read,
1816 created_at: now,
1817 },
1818 ),
1819 ],
1820 bootstrapped: true,
1821 master_secret: None,
1822 kv: std::collections::HashMap::new(),
1823 };
1824 let bytes = state.serialize();
1825 let restored = VaultState::deserialize(&bytes).unwrap();
1826 assert_eq!(restored.api_keys.len(), 2);
1829 let acme_key = restored
1830 .api_keys
1831 .iter()
1832 .find(|(o, _)| o.tenant.as_deref() == Some("acme"))
1833 .unwrap();
1834 assert_eq!(acme_key.1.key, "rk_acme_key");
1835 let globex_key = restored
1836 .api_keys
1837 .iter()
1838 .find(|(o, _)| o.tenant.as_deref() == Some("globex"))
1839 .unwrap();
1840 assert_eq!(globex_key.1.key, "rk_globex_key");
1841 }
1842
1843 #[test]
1844 fn test_vault_state_scram_iter_below_min_rejected() {
1845 let now = now_ms();
1846 let stored_hex = "00".repeat(32);
1850 let server_hex = "11".repeat(32);
1851 let line = format!(
1852 "USER:eve\targon2id$x$y\tread\ttrue\t{}\t{}\tdeadbeef:1024:{}:{}\n",
1853 now, now, stored_hex, server_hex
1854 );
1855 match VaultState::deserialize(line.as_bytes()) {
1856 Err(VaultError::Corrupt(msg)) => assert!(msg.contains("below minimum")),
1857 Err(other) => panic!("expected Corrupt iter-floor error, got {other:?}"),
1858 Ok(_) => panic!("expected Corrupt iter-floor error, got Ok"),
1859 }
1860 }
1861
1862 #[test]
1863 fn test_constant_time_eq_function() {
1864 assert!(constant_time_eq(b"hello", b"hello"));
1865 assert!(!constant_time_eq(b"hello", b"world"));
1866 assert!(!constant_time_eq(b"short", b"longer"));
1867 assert!(constant_time_eq(b"", b""));
1868 }
1869}