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;
86const VAULT_PASSPHRASE_VERSION: u8 = 3;
87
88const VAULT_LEGACY_VERSION: u8 = 1;
91
92const VAULT_AAD: &[u8] = b"reddb-vault";
93
94use reddb_file::VAULT_LOGICAL_EXPORT_AAD;
98
99const VAULT_MAGIC_SIZE: usize = 4;
101const VAULT_VERSION_SIZE: usize = 1;
102const VAULT_SALT_SIZE: usize = 16;
103const VAULT_PAYLOAD_LEN_SIZE: usize = 4;
104const VAULT_CHAIN_COUNT_SIZE: usize = 4;
105const VAULT_FIRST_PAGE_ID_SIZE: usize = 4;
106
107const NONCE_SIZE: usize = 12;
109
110const VAULT_HEADER_PREAMBLE_SIZE: usize =
112 VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE + VAULT_SALT_SIZE + VAULT_PAYLOAD_LEN_SIZE; const VAULT_HEADER_META_SIZE: usize =
117 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;
125
126const VAULT_HEADER_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_HEADER_META_SIZE;
129
130const VAULT_DATA_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_DATA_PREFIX_SIZE;
133
134pub struct KeyPair {
151 pub master_secret: Vec<u8>,
153 pub certificate: Vec<u8>,
155}
156
157impl KeyPair {
158 pub fn generate() -> Self {
160 let mut master_secret = vec![0u8; 32];
161 os_random::fill_bytes(&mut master_secret).expect("CSPRNG failed during keypair generation");
162 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
163 Self {
164 master_secret,
165 certificate: certificate.to_vec(),
166 }
167 }
168
169 pub fn from_master_secret(master_secret: Vec<u8>) -> Self {
172 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
173 Self {
174 master_secret,
175 certificate: certificate.to_vec(),
176 }
177 }
178
179 pub fn vault_key_from_certificate(certificate: &[u8]) -> SecureKey {
184 let key_bytes = derive_key(certificate, b"reddb-vault-seal", &vault_argon2_params());
185 SecureKey::new(&key_bytes)
186 }
187
188 pub fn sign(&self, data: &[u8]) -> Vec<u8> {
190 hmac_sha256(&self.master_secret, data).to_vec()
191 }
192
193 pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
195 let expected = self.sign(data);
196 constant_time_eq(&expected, signature)
197 }
198
199 pub fn certificate_hex(&self) -> String {
201 hex::encode(&self.certificate)
202 }
203}
204
205fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
207 crate::crypto::constant_time_eq(a, b)
208}
209
210#[derive(Debug)]
216pub enum VaultError {
217 NoKey,
219 InvalidCredential(String),
220 Encryption,
222 Decryption,
224 Io(std::io::Error),
226 Corrupt(String),
228 Pager(String),
230}
231
232impl std::fmt::Display for VaultError {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 Self::NoKey => write!(
236 f,
237 "no vault credential: set REDDB_CERTIFICATE or REDDB_VAULT_PASSPHRASE (or their _FILE variants)"
238 ),
239 Self::InvalidCredential(reason) => write!(f, "invalid vault credential: {reason}"),
240 Self::Encryption => write!(f, "vault encryption failed"),
241 Self::Decryption => write!(f, "vault decryption failed (wrong key or corrupt data)"),
242 Self::Io(err) => write!(f, "vault I/O error: {err}"),
243 Self::Corrupt(msg) => write!(f, "vault corrupt: {msg}"),
244 Self::Pager(msg) => write!(f, "vault pager error: {msg}"),
245 }
246 }
247}
248
249impl std::error::Error for VaultError {}
250
251impl From<VaultError> for AuthError {
252 fn from(err: VaultError) -> Self {
253 AuthError::Internal(err.to_string())
254 }
255}
256
257fn decode_certificate_hex(certificate_hex: &str) -> Result<Vec<u8>, VaultError> {
258 let certificate = hex::decode(certificate_hex.trim()).map_err(|_| VaultError::InvalidCredential("certificate must contain 64 hexadecimal characters; use REDDB_VAULT_PASSPHRASE for a password".into()))?;
259 if certificate.len() != 32 {
260 return Err(VaultError::InvalidCredential(
261 "certificate must contain exactly 32 bytes (64 hexadecimal characters)".into(),
262 ));
263 }
264 Ok(certificate)
265}
266
267#[derive(Default)]
275pub struct VaultState {
276 pub users: Vec<User>,
277 pub api_keys: Vec<(UserId, ApiKey)>,
281 pub bootstrapped: bool,
282 pub master_secret: Option<Vec<u8>>,
286 pub kv: std::collections::HashMap<String, String>,
290}
291
292impl std::fmt::Debug for VaultState {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("VaultState")
295 .field("users", &self.users.len())
296 .field("api_keys", &self.api_keys.len())
297 .field("bootstrapped", &self.bootstrapped)
298 .field(
299 "master_secret",
300 &self.master_secret.as_ref().map(|_| "<redacted>"),
301 )
302 .field("kv", &self.kv.len())
303 .finish()
304 }
305}
306
307impl VaultState {
308 pub fn serialize(&self) -> Vec<u8> {
310 let mut out = String::new();
311
312 if let Some(ref secret) = self.master_secret {
314 out.push_str(&format!("MASTER_SECRET:{}\n", hex::encode(secret)));
315 }
316
317 out.push_str(&format!("SEALED:{}\n", self.bootstrapped));
319
320 for user in &self.users {
333 let scram_field = match &user.scram_verifier {
334 Some(v) => format!(
335 "{}:{}:{}:{}",
336 hex::encode(&v.salt),
337 v.iter,
338 hex::encode(v.stored_key),
339 hex::encode(v.server_key),
340 ),
341 None => String::new(),
342 };
343 let tenant_field = user.tenant_id.clone().unwrap_or_default();
344 out.push_str(&format!(
345 "USER:{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
346 user.username,
347 user.password_hash,
348 user.role.as_str(),
349 user.enabled,
350 user.created_at,
351 user.updated_at,
352 scram_field,
353 tenant_field,
354 ));
355 }
356
357 for (owner, key) in &self.api_keys {
362 let tenant_field = owner.tenant.clone().unwrap_or_default();
363 let expires_field = key
364 .expires_at
365 .map(|deadline| deadline.to_string())
366 .unwrap_or_default();
367 out.push_str(&format!(
368 "KEY:{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
369 owner.username,
370 key.key,
371 key.name,
372 key.role.as_str(),
373 key.created_at,
374 tenant_field,
375 expires_field,
376 ));
377 }
378
379 for (k, v) in &self.kv {
381 out.push_str(&format!("KV:{}\t{}\n", k, hex::encode(v.as_bytes())));
382 }
383
384 out.into_bytes()
385 }
386
387 pub fn deserialize(data: &[u8]) -> Result<Self, VaultError> {
389 let text = std::str::from_utf8(data)
390 .map_err(|_| VaultError::Corrupt("payload is not valid UTF-8".into()))?;
391
392 let mut users = Vec::new();
393 let mut api_keys: Vec<(UserId, ApiKey)> = Vec::new();
394 let mut bootstrapped = false;
395 let mut master_secret: Option<Vec<u8>> = None;
396 let mut kv: std::collections::HashMap<String, String> = std::collections::HashMap::new();
397
398 for line in text.lines() {
399 if line.is_empty() {
400 continue;
401 }
402
403 if let Some(rest) = line.strip_prefix("MASTER_SECRET:") {
404 master_secret = Some(
405 hex::decode(rest)
406 .map_err(|_| VaultError::Corrupt("invalid MASTER_SECRET hex".into()))?,
407 );
408 } else if let Some(rest) = line.strip_prefix("SEALED:") {
409 bootstrapped = rest == "true";
410 } else if let Some(rest) = line.strip_prefix("USER:") {
411 let parts: Vec<&str> = rest.split('\t').collect();
412 if !(7..=9).contains(&parts.len()) {
416 return Err(VaultError::Corrupt(format!(
417 "USER line has {} fields, expected 7, 8, or 9",
418 parts.len()
419 )));
420 }
421 let role = Role::from_str(parts[2])
422 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[2])))?;
423 let enabled = parts[3] == "true";
424 let created_at: u128 = parts[4]
425 .parse()
426 .map_err(|_| VaultError::Corrupt("invalid created_at".into()))?;
427 let updated_at: u128 = parts[5]
428 .parse()
429 .map_err(|_| VaultError::Corrupt("invalid updated_at".into()))?;
430 let scram_verifier = parts
431 .get(6)
432 .map(|s| s.trim())
433 .filter(|s| !s.is_empty())
434 .map(parse_scram_field)
435 .transpose()?;
436 let tenant_id = parts
437 .get(7)
438 .map(|s| s.trim())
439 .filter(|s| !s.is_empty())
440 .map(|s| s.to_string());
441
442 users.push(User {
443 username: parts[0].to_string(),
444 tenant_id,
445 password_hash: parts[1].to_string(),
446 scram_verifier,
447 role,
448 api_keys: Vec::new(), created_at,
450 updated_at,
451 enabled,
452 });
453 } else if let Some(rest) = line.strip_prefix("KEY:") {
454 let parts: Vec<&str> = rest.split('\t').collect();
455 if !(5..=7).contains(&parts.len()) {
457 return Err(VaultError::Corrupt(format!(
458 "KEY line has {} fields, expected 5 to 7",
459 parts.len()
460 )));
461 }
462 let role = Role::from_str(parts[3])
463 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[3])))?;
464 let created_at: u128 = parts[4]
465 .parse()
466 .map_err(|_| VaultError::Corrupt("invalid key created_at".into()))?;
467 let tenant_id = parts
468 .get(5)
469 .map(|s| s.trim())
470 .filter(|s| !s.is_empty())
471 .map(|s| s.to_string());
472
473 api_keys.push((
474 UserId {
475 tenant: tenant_id,
476 username: parts[0].to_string(),
477 },
478 ApiKey {
479 key: parts[1].to_string(),
480 name: parts[2].to_string(),
481 role,
482 created_at,
483 expires_at: parts
484 .get(6)
485 .map(|s| s.trim())
486 .filter(|s| !s.is_empty())
487 .and_then(|s| s.parse::<u128>().ok()),
488 },
489 ));
490 } else if let Some(rest) = line.strip_prefix("KV:") {
491 let parts: Vec<&str> = rest.splitn(2, '\t').collect();
492 if parts.len() == 2 {
493 if let Ok(bytes) = hex::decode(parts[1]) {
494 if let Ok(value) = String::from_utf8(bytes) {
495 kv.insert(parts[0].to_string(), value);
496 }
497 }
498 }
499 } else {
500 }
502 }
503
504 for (owner, key) in &api_keys {
508 if let Some(user) = users
509 .iter_mut()
510 .find(|u| u.username == owner.username && u.tenant_id == owner.tenant)
511 {
512 user.api_keys.push(key.clone());
513 }
514 }
515
516 Ok(Self {
517 users,
518 api_keys,
519 bootstrapped,
520 master_secret,
521 kv,
522 })
523 }
524}
525
526pub struct Vault {
537 key: SecureKey,
538 salt: [u8; 16],
539 version: u8,
540}
541
542fn vault_argon2_params() -> Argon2Params {
545 Argon2Params {
546 m_cost: 16 * 1024, t_cost: 3,
548 p: 1,
549 tag_len: 32,
550 }
551}
552
553impl Vault {
554 pub fn has_saved_state(pager: &Pager) -> bool {
556 pager
557 .read_page_no_checksum(VAULT_HEADER_PAGE)
558 .ok()
559 .map(|page| {
560 let content = page.content();
561 content.len() >= VAULT_MAGIC_SIZE && &content[0..VAULT_MAGIC_SIZE] == VAULT_MAGIC
562 })
563 .unwrap_or(false)
564 }
565
566 pub fn open(pager: &Pager) -> Result<Self, VaultError> {
575 match (
576 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE"),
577 crate::utils::env_with_file_fallback("REDDB_VAULT_PASSPHRASE"),
578 ) {
579 (Some(_), Some(_)) => Err(VaultError::InvalidCredential(
580 "set either REDDB_CERTIFICATE or REDDB_VAULT_PASSPHRASE, not both".into(),
581 )),
582 (Some(certificate), None) => Self::with_certificate(pager, &certificate),
583 (None, Some(passphrase)) => Self::with_passphrase(pager, &passphrase),
584 (None, None) => Err(VaultError::NoKey),
585 }
586 }
587
588 fn salt_for_open(pager: &Pager) -> Result<[u8; 16], VaultError> {
589 match read_vault_salt_from_pager(pager) {
591 Ok(s) => Ok(s),
592 Err(_) => {
593 let mut salt = [0u8; 16];
595 let mut buf = [0u8; 16];
596 os_random::fill_bytes(&mut buf)
597 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
598 salt.copy_from_slice(&buf);
599 Ok(salt)
600 }
601 }
602 }
603
604 pub fn with_certificate(pager: &Pager, certificate_hex: &str) -> Result<Self, VaultError> {
610 Self::check_credential_format(pager, VAULT_VERSION)?;
611 let certificate = decode_certificate_hex(certificate_hex)?;
612
613 let key = KeyPair::vault_key_from_certificate(&certificate);
614
615 let salt = Self::salt_for_open(pager)?;
616
617 Ok(Self {
618 key,
619 salt,
620 version: VAULT_VERSION,
621 })
622 }
623
624 pub fn with_passphrase(pager: &Pager, passphrase: &str) -> Result<Self, VaultError> {
627 Self::check_credential_format(pager, VAULT_PASSPHRASE_VERSION)?;
628 let salt = Self::salt_for_open(pager)?;
629 let key = Self::passphrase_key(passphrase, &salt)?;
630 Ok(Self {
631 key,
632 salt,
633 version: VAULT_PASSPHRASE_VERSION,
634 })
635 }
636
637 fn passphrase_key(passphrase: &str, salt: &[u8; 16]) -> Result<SecureKey, VaultError> {
638 if passphrase.is_empty() || passphrase.len() > 4096 {
639 return Err(VaultError::InvalidCredential(
640 "passphrase must contain 1 to 4096 bytes".into(),
641 ));
642 }
643 let parameters = argon2::Params::new(19 * 1024, 2, 1, Some(32))
644 .expect("invariant: vault Argon2id parameters are valid");
645 let hasher = argon2::Argon2::new(
646 argon2::Algorithm::Argon2id,
647 argon2::Version::V0x13,
648 parameters,
649 );
650 let mut bytes = [0u8; 32];
651 hasher
652 .hash_password_into(passphrase.as_bytes(), salt, &mut bytes)
653 .map_err(|_| VaultError::Encryption)?;
654 let key = SecureKey::new(&bytes);
655 bytes.fill(0);
656 Ok(key)
657 }
658
659 fn check_credential_format(pager: &Pager, expected: u8) -> Result<(), VaultError> {
660 if Self::has_saved_state(pager) {
661 let page = pager
662 .read_page_no_checksum(VAULT_HEADER_PAGE)
663 .map_err(|err| VaultError::Pager(err.to_string()))?;
664 let version = page.content()[4];
665 if (version == VAULT_VERSION || version == VAULT_PASSPHRASE_VERSION)
666 && version != expected
667 {
668 return Err(VaultError::InvalidCredential(
669 if version == VAULT_PASSPHRASE_VERSION {
670 "this vault requires a passphrase"
671 } else {
672 "this vault requires a certificate"
673 }
674 .into(),
675 ));
676 }
677 }
678 Ok(())
679 }
680
681 pub fn from_env(pager: &Pager) -> Result<Self, VaultError> {
685 Self::open(pager)
686 }
687
688 pub fn with_certificate_bytes(pager: &Pager, certificate: &[u8]) -> Result<Self, VaultError> {
693 Self::check_credential_format(pager, VAULT_VERSION)?;
694 if certificate.len() != 32 {
695 return Err(VaultError::InvalidCredential(
696 "certificate must contain 32 bytes".into(),
697 ));
698 }
699 let key = KeyPair::vault_key_from_certificate(certificate);
700
701 let salt = match read_vault_salt_from_pager(pager) {
702 Ok(s) => s,
703 Err(_) => {
704 let mut s = [0u8; 16];
705 os_random::fill_bytes(&mut s)
706 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
707 s
708 }
709 };
710
711 Ok(Self {
712 key,
713 salt,
714 version: VAULT_VERSION,
715 })
716 }
717
718 pub fn seal_logical_export(&self, state: &VaultState) -> Result<String, VaultError> {
724 let plaintext = state.serialize();
725 let mut nonce = [0u8; NONCE_SIZE];
726 os_random::fill_bytes(&mut nonce)
727 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
728
729 let key_bytes: &[u8] = self.key.as_bytes();
730 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
731 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_LOGICAL_EXPORT_AAD, &plaintext);
732
733 Ok(reddb_file::encode_vault_logical_export(
736 &self.salt,
737 &nonce,
738 &ciphertext,
739 ))
740 }
741
742 pub fn unseal_logical_export(blob_hex: &str) -> Result<VaultState, VaultError> {
745 match (
746 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE"),
747 crate::utils::env_with_file_fallback("REDDB_VAULT_PASSPHRASE"),
748 ) {
749 (Some(_), Some(_)) => Err(VaultError::InvalidCredential(
750 "set a single vault credential".into(),
751 )),
752 (Some(certificate), None) => {
753 Self::unseal_logical_export_with_certificate(blob_hex, &certificate)
754 }
755 (None, Some(passphrase)) => {
756 Self::unseal_logical_export_with_passphrase(blob_hex, &passphrase)
757 }
758 _ => Err(VaultError::NoKey),
759 }
760 }
761
762 pub fn unseal_logical_export_with_certificate(
764 blob_hex: &str,
765 certificate_hex: &str,
766 ) -> Result<VaultState, VaultError> {
767 let (_salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
768 let certificate = decode_certificate_hex(certificate_hex)?;
769 let key = KeyPair::vault_key_from_certificate(&certificate);
770 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
771 }
772
773 pub fn unseal_logical_export_with_passphrase(
774 blob_hex: &str,
775 passphrase: &str,
776 ) -> Result<VaultState, VaultError> {
777 let (salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
778 let key = Self::passphrase_key(passphrase, &salt)?;
779 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
780 }
781
782 fn decode_logical_export(
783 blob_hex: &str,
784 ) -> Result<([u8; VAULT_SALT_SIZE], [u8; NONCE_SIZE], Vec<u8>), VaultError> {
785 let env = reddb_file::decode_vault_logical_export(blob_hex)
788 .map_err(|e| VaultError::Corrupt(e.to_string()))?;
789 Ok((env.salt, env.nonce, env.ciphertext))
790 }
791
792 fn decrypt_logical_export(
793 key: &SecureKey,
794 nonce: &[u8; NONCE_SIZE],
795 ciphertext: &[u8],
796 ) -> Result<VaultState, VaultError> {
797 let key_bytes: &[u8] = key.as_bytes();
798 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
799 let plaintext = aes256_gcm_decrypt(key_arr, nonce, VAULT_LOGICAL_EXPORT_AAD, ciphertext)
800 .map_err(|_| VaultError::Decryption)?;
801 VaultState::deserialize(&plaintext)
802 }
803
804 pub fn save(&self, pager: &Pager, state: &VaultState) -> Result<(), VaultError> {
818 let plaintext = state.serialize();
819
820 let mut nonce = [0u8; NONCE_SIZE];
822 os_random::fill_bytes(&mut nonce)
823 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
824
825 let key_bytes: &[u8] = self.key.as_bytes();
826 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
827 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_AAD, &plaintext);
828 let cipher_total = ciphertext.len();
832 let payload_len = (NONCE_SIZE + cipher_total) as u32; let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
840 let overflow = cipher_total.saturating_sub(header_chunk_len);
841 let chain_count = overflow.div_ceil(VAULT_DATA_CIPHER_CAPACITY);
842
843 while pager
857 .page_count()
858 .map_err(|e| VaultError::Pager(e.to_string()))?
859 <= VAULT_HEADER_PAGE
860 {
861 pager
862 .allocate_page(PageType::Vault)
863 .map_err(|e| VaultError::Pager(format!("reserve vault slot: {e}")))?;
864 }
865
866 let old_chain = self.read_existing_chain_ids(pager).unwrap_or_default();
876
877 let mut new_chain: Vec<u32> = Vec::with_capacity(chain_count);
884 for _ in 0..chain_count {
885 let page = pager
886 .allocate_page(PageType::Vault)
887 .map_err(|e| VaultError::Pager(format!("allocate vault data page: {e}")))?;
888 new_chain.push(page.page_id());
889 }
890
891 let mut cursor = header_chunk_len;
898 for i in 0..chain_count {
899 let next_id = if i + 1 < chain_count {
900 new_chain[i + 1]
901 } else {
902 0
903 };
904 let take = (cipher_total - cursor).min(VAULT_DATA_CIPHER_CAPACITY);
905 let frag = &ciphertext[cursor..cursor + take];
906 self.write_data_page(pager, new_chain[i], next_id, frag)?;
907 cursor += take;
908 }
909 debug_assert_eq!(cursor, cipher_total, "ciphertext spill accounting mismatch");
910
911 let first_data_page = new_chain.first().copied().unwrap_or(0);
919 self.write_header_page(
920 pager,
921 &nonce,
922 payload_len,
923 chain_count as u32,
924 first_data_page,
925 &ciphertext[..header_chunk_len],
926 )?;
927
928 pager
933 .flush()
934 .map_err(|e| VaultError::Pager(e.to_string()))?;
935
936 for &id in old_chain.iter() {
941 pager
942 .free_page(id)
943 .map_err(|e| VaultError::Pager(format!("free old vault page {id}: {e}")))?;
944 }
945
946 Ok(())
947 }
948
949 pub fn load(&self, pager: &Pager) -> Result<Option<VaultState>, VaultError> {
953 let page = match pager.read_page_no_checksum(VAULT_HEADER_PAGE) {
955 Ok(p) => p,
956 Err(_) => return Ok(None),
957 };
958
959 let page_content = page.content();
960
961 if page_content.len() < VAULT_HEADER_META_SIZE {
962 return Ok(None);
963 }
964 if &page_content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
965 return Ok(None); }
967
968 let version = page_content[4];
969 if version == VAULT_LEGACY_VERSION {
970 return Err(VaultError::Corrupt(
974 "vault was bootstrapped with the legacy 2-page format \
975 (pre-RedDB v0.3); re-bootstrap with `red bootstrap` to upgrade"
976 .to_string(),
977 ));
978 }
979 if version != self.version {
980 return Err(VaultError::Corrupt(format!(
981 "vault credential format mismatch: stored {version}, opened {}",
982 self.version
983 )));
984 }
985
986 let payload_len = u32::from_le_bytes(
988 page_content[21..25]
989 .try_into()
990 .map_err(|_| VaultError::Corrupt("bad payload length bytes".into()))?,
991 ) as usize;
992
993 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
994 let nonce: [u8; NONCE_SIZE] = page_content[nonce_start..nonce_start + NONCE_SIZE]
995 .try_into()
996 .map_err(|_| VaultError::Corrupt("bad nonce".into()))?;
997
998 let chain_count_off = nonce_start + NONCE_SIZE;
999 let chain_count = u32::from_le_bytes(
1000 page_content[chain_count_off..chain_count_off + 4]
1001 .try_into()
1002 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1003 ) as usize;
1004 let first_id_off = chain_count_off + 4;
1005 let mut next_id = u32::from_le_bytes(
1006 page_content[first_id_off..first_id_off + 4]
1007 .try_into()
1008 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1009 );
1010
1011 if payload_len < NONCE_SIZE {
1012 return Err(VaultError::Corrupt("payload too short for nonce".into()));
1013 }
1014 let cipher_total = payload_len - NONCE_SIZE;
1015
1016 let mut cipher = Vec::with_capacity(cipher_total);
1018 let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
1019 let header_cipher_start = VAULT_HEADER_META_SIZE;
1020 cipher.extend_from_slice(
1021 &page_content[header_cipher_start..header_cipher_start + header_chunk_len],
1022 );
1023
1024 let mut hops = 0usize;
1026 while cipher.len() < cipher_total {
1030 if hops >= chain_count {
1031 return Err(VaultError::Corrupt(format!(
1032 "vault chain shorter than declared: {} hops, expected {}",
1033 hops, chain_count
1034 )));
1035 }
1036 if next_id == 0 {
1037 return Err(VaultError::Corrupt(
1038 "vault chain ends prematurely (next_id == 0)".to_string(),
1039 ));
1040 }
1041
1042 let dp = pager
1043 .read_page_no_checksum(next_id)
1044 .map_err(|e| VaultError::Pager(format!("vault data page {next_id}: {e}")))?;
1045 let dp_content = dp.content();
1046 if dp_content.len() < VAULT_DATA_PREFIX_SIZE {
1047 return Err(VaultError::Corrupt(format!(
1048 "vault data page {next_id} truncated"
1049 )));
1050 }
1051 if &dp_content[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC {
1052 return Err(VaultError::Corrupt(format!(
1053 "vault data page {next_id} has bad magic"
1054 )));
1055 }
1056 let np = u32::from_le_bytes(
1057 dp_content[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1058 .try_into()
1059 .map_err(|_| VaultError::Corrupt("bad next_page_id bytes".into()))?,
1060 );
1061 let take = (cipher_total - cipher.len()).min(VAULT_DATA_CIPHER_CAPACITY);
1062 let frag_start = VAULT_DATA_PREFIX_SIZE;
1063 cipher.extend_from_slice(&dp_content[frag_start..frag_start + take]);
1064
1065 next_id = np;
1066 hops += 1;
1067 }
1068
1069 if cipher.len() != cipher_total {
1070 return Err(VaultError::Corrupt(format!(
1071 "vault truncated: expected {} cipher bytes, got {}",
1072 cipher_total,
1073 cipher.len()
1074 )));
1075 }
1076 if hops != chain_count {
1077 return Err(VaultError::Corrupt(format!(
1078 "vault chain length mismatch: walked {} pages, header says {}",
1079 hops, chain_count
1080 )));
1081 }
1082
1083 let key_bytes: &[u8] = self.key.as_bytes();
1085 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
1086 let plaintext = aes256_gcm_decrypt(key_arr, &nonce, VAULT_AAD, &cipher)
1087 .map_err(|_| VaultError::Decryption)?;
1088
1089 let state = VaultState::deserialize(&plaintext)?;
1090 Ok(Some(state))
1091 }
1092
1093 fn read_existing_chain_ids(&self, pager: &Pager) -> Result<Vec<u32>, VaultError> {
1100 let header = pager
1101 .read_page_no_checksum(VAULT_HEADER_PAGE)
1102 .map_err(|e| VaultError::Pager(e.to_string()))?;
1103 let content = header.content();
1104 if content.len() < VAULT_HEADER_META_SIZE {
1105 return Ok(Vec::new());
1106 }
1107 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1108 return Ok(Vec::new());
1109 }
1110 let version = content[4];
1111 if version != VAULT_VERSION && version != VAULT_PASSPHRASE_VERSION {
1112 return Ok(Vec::new());
1116 }
1117 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
1118 let chain_count_off = nonce_start + NONCE_SIZE;
1119 let chain_count = u32::from_le_bytes(
1120 content[chain_count_off..chain_count_off + 4]
1121 .try_into()
1122 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1123 ) as usize;
1124 let first_id_off = chain_count_off + 4;
1125 let mut id = u32::from_le_bytes(
1126 content[first_id_off..first_id_off + 4]
1127 .try_into()
1128 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1129 );
1130
1131 let mut out = Vec::with_capacity(chain_count);
1132 let mut hops = 0usize;
1133 while id != 0 && hops < chain_count {
1134 out.push(id);
1135 match pager.read_page_no_checksum(id) {
1138 Ok(dp) => {
1139 let dc = dp.content();
1140 if dc.len() < VAULT_DATA_PREFIX_SIZE
1141 || &dc[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC
1142 {
1143 break;
1144 }
1145 id = u32::from_le_bytes(
1146 dc[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1147 .try_into()
1148 .map_err(|_| VaultError::Corrupt("bad next_id".into()))?,
1149 );
1150 }
1151 Err(_) => break,
1152 }
1153 hops += 1;
1154 }
1155 Ok(out)
1156 }
1157
1158 fn write_header_page(
1162 &self,
1163 pager: &Pager,
1164 nonce: &[u8; NONCE_SIZE],
1165 payload_len: u32,
1166 chain_count: u32,
1167 first_data_page_id: u32,
1168 cipher_fragment: &[u8],
1169 ) -> Result<(), VaultError> {
1170 debug_assert!(cipher_fragment.len() <= VAULT_HEADER_CIPHER_CAPACITY);
1171
1172 let mut page = Page::new(PageType::Vault, VAULT_HEADER_PAGE);
1173 let bytes = page.as_bytes_mut();
1174 let mut off = HEADER_SIZE;
1175
1176 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_MAGIC);
1177 off += VAULT_MAGIC_SIZE;
1178
1179 bytes[off] = self.version;
1180 off += VAULT_VERSION_SIZE;
1181
1182 bytes[off..off + VAULT_SALT_SIZE].copy_from_slice(&self.salt);
1183 off += VAULT_SALT_SIZE;
1184
1185 bytes[off..off + 4].copy_from_slice(&payload_len.to_le_bytes());
1186 off += VAULT_PAYLOAD_LEN_SIZE;
1187
1188 bytes[off..off + NONCE_SIZE].copy_from_slice(nonce);
1189 off += NONCE_SIZE;
1190
1191 bytes[off..off + 4].copy_from_slice(&chain_count.to_le_bytes());
1192 off += VAULT_CHAIN_COUNT_SIZE;
1193
1194 bytes[off..off + 4].copy_from_slice(&first_data_page_id.to_le_bytes());
1195 off += VAULT_FIRST_PAGE_ID_SIZE;
1196
1197 debug_assert_eq!(off, HEADER_SIZE + VAULT_HEADER_META_SIZE);
1198
1199 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1200
1201 pager
1202 .write_page_no_checksum(VAULT_HEADER_PAGE, page)
1203 .map_err(|e| VaultError::Pager(e.to_string()))?;
1204 Ok(())
1205 }
1206
1207 fn write_data_page(
1209 &self,
1210 pager: &Pager,
1211 page_id: u32,
1212 next_page_id: u32,
1213 cipher_fragment: &[u8],
1214 ) -> Result<(), VaultError> {
1215 debug_assert!(cipher_fragment.len() <= VAULT_DATA_CIPHER_CAPACITY);
1216
1217 let mut page = Page::new(PageType::Vault, page_id);
1218 let bytes = page.as_bytes_mut();
1219 let mut off = HEADER_SIZE;
1220
1221 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_DATA_MAGIC);
1222 off += VAULT_MAGIC_SIZE;
1223
1224 bytes[off..off + 4].copy_from_slice(&next_page_id.to_le_bytes());
1225 off += 4;
1226
1227 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1228
1229 pager
1230 .write_page_no_checksum(page_id, page)
1231 .map_err(|e| VaultError::Pager(e.to_string()))?;
1232 Ok(())
1233 }
1234}
1235
1236fn parse_scram_field(field: &str) -> Result<crate::auth::scram::ScramVerifier, VaultError> {
1243 let parts: Vec<&str> = field.split(':').collect();
1244 if parts.len() != 4 {
1245 return Err(VaultError::Corrupt(format!(
1246 "SCRAM verifier has {} segments, expected 4",
1247 parts.len()
1248 )));
1249 }
1250 let salt =
1251 hex::decode(parts[0]).map_err(|_| VaultError::Corrupt("invalid SCRAM salt hex".into()))?;
1252 let iter: u32 = parts[1]
1253 .parse()
1254 .map_err(|_| VaultError::Corrupt("invalid SCRAM iter".into()))?;
1255 if iter < crate::auth::scram::MIN_ITER {
1256 return Err(VaultError::Corrupt(format!(
1257 "SCRAM iter {} below minimum {}",
1258 iter,
1259 crate::auth::scram::MIN_ITER
1260 )));
1261 }
1262 let stored_vec = hex::decode(parts[2])
1263 .map_err(|_| VaultError::Corrupt("invalid SCRAM stored_key hex".into()))?;
1264 let server_vec = hex::decode(parts[3])
1265 .map_err(|_| VaultError::Corrupt("invalid SCRAM server_key hex".into()))?;
1266 let stored_key: [u8; 32] = stored_vec
1267 .try_into()
1268 .map_err(|_| VaultError::Corrupt("SCRAM stored_key must be 32 bytes".into()))?;
1269 let server_key: [u8; 32] = server_vec
1270 .try_into()
1271 .map_err(|_| VaultError::Corrupt("SCRAM server_key must be 32 bytes".into()))?;
1272 Ok(crate::auth::scram::ScramVerifier {
1273 salt,
1274 iter,
1275 stored_key,
1276 server_key,
1277 })
1278}
1279
1280fn read_vault_salt_from_pager(pager: &Pager) -> Result<[u8; 16], VaultError> {
1287 let page = pager
1288 .read_page_no_checksum(VAULT_HEADER_PAGE)
1289 .map_err(|e| VaultError::Pager(format!("vault page read: {e}")))?;
1290
1291 let content = page.content();
1292 if content.len() < VAULT_HEADER_PREAMBLE_SIZE {
1293 return Err(VaultError::Corrupt("vault page too short".into()));
1294 }
1295 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1296 return Err(VaultError::Corrupt("bad magic bytes".into()));
1297 }
1298
1299 let mut salt = [0u8; VAULT_SALT_SIZE];
1300 salt.copy_from_slice(&content[5..21]);
1301 Ok(salt)
1302}
1303
1304#[cfg(test)]
1309mod tests {
1310 use super::*;
1311 use crate::auth::{now_ms, ApiKey, Role, User};
1312 use crate::storage::engine::pager::PagerConfig;
1313
1314 fn sample_state() -> VaultState {
1315 let now = now_ms();
1316 VaultState {
1317 users: vec![
1318 User {
1319 username: "alice".into(),
1320 tenant_id: None,
1321 password_hash: "argon2id$aabbccdd$eeff0011".into(),
1322 scram_verifier: None,
1323 role: Role::Admin,
1324 api_keys: vec![ApiKey {
1325 key: "rk_abc123".into(),
1326 name: "ci-token".into(),
1327 role: Role::Write,
1328 created_at: now,
1329 expires_at: None,
1330 }],
1331 created_at: now,
1332 updated_at: now,
1333 enabled: true,
1334 },
1335 User {
1336 username: "bob".into(),
1337 tenant_id: None,
1338 password_hash: "argon2id$11223344$55667788".into(),
1339 scram_verifier: None,
1340 role: Role::Read,
1341 api_keys: vec![],
1342 created_at: now,
1343 updated_at: now,
1344 enabled: false,
1345 },
1346 ],
1347 api_keys: vec![(
1348 UserId::platform("alice"),
1349 ApiKey {
1350 key: "rk_abc123".into(),
1351 name: "ci-token".into(),
1352 role: Role::Write,
1353 created_at: now,
1354 expires_at: None,
1355 },
1356 )],
1357 bootstrapped: true,
1358 master_secret: None,
1359 kv: std::collections::HashMap::new(),
1360 }
1361 }
1362
1363 fn temp_pager() -> (Pager, std::path::PathBuf) {
1365 use std::sync::atomic::{AtomicU64, Ordering};
1366 static COUNTER: AtomicU64 = AtomicU64::new(0);
1367 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1368 let tmp_dir =
1369 std::env::temp_dir().join(format!("reddb_vault_test_{}_{}", std::process::id(), id));
1370 std::fs::create_dir_all(&tmp_dir).unwrap();
1371 let db_path = tmp_dir.join("test.rdb");
1372 let pager = Pager::open(&db_path, PagerConfig::default()).unwrap();
1373 (pager, tmp_dir)
1374 }
1375
1376 fn env_lock() -> &'static std::sync::Mutex<()> {
1377 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
1378 LOCK.get_or_init(|| std::sync::Mutex::new(()))
1379 }
1380
1381 fn restore_env_var(name: &str, value: Option<std::ffi::OsString>) {
1382 unsafe {
1383 match value {
1384 Some(value) => std::env::set_var(name, value),
1385 None => std::env::remove_var(name),
1386 }
1387 }
1388 }
1389
1390 #[test]
1391 fn test_vault_state_serialize_deserialize_roundtrip() {
1392 let state = sample_state();
1393 let serialized = state.serialize();
1394 let text = std::str::from_utf8(&serialized).unwrap();
1395
1396 assert!(text.contains("SEALED:true"));
1398 assert!(text.contains("USER:alice\t"));
1399 assert!(text.contains("USER:bob\t"));
1400 assert!(text.contains("KEY:alice\trk_abc123\t"));
1401
1402 let restored = VaultState::deserialize(&serialized).unwrap();
1404 assert!(restored.bootstrapped);
1405 assert_eq!(restored.users.len(), 2);
1406
1407 let alice = restored
1408 .users
1409 .iter()
1410 .find(|u| u.username == "alice")
1411 .unwrap();
1412 assert_eq!(alice.role, Role::Admin);
1413 assert!(alice.enabled);
1414 assert_eq!(alice.password_hash, "argon2id$aabbccdd$eeff0011");
1415 assert_eq!(alice.api_keys.len(), 1);
1416 assert_eq!(alice.api_keys[0].key, "rk_abc123");
1417 assert_eq!(alice.api_keys[0].name, "ci-token");
1418 assert_eq!(alice.api_keys[0].role, Role::Write);
1419
1420 let bob = restored.users.iter().find(|u| u.username == "bob").unwrap();
1421 assert_eq!(bob.role, Role::Read);
1422 assert!(!bob.enabled);
1423 assert!(bob.api_keys.is_empty());
1424
1425 assert_eq!(restored.api_keys.len(), 1);
1426 assert_eq!(restored.api_keys[0].0.username, "alice");
1427 assert!(restored.api_keys[0].0.tenant.is_none());
1428 }
1429
1430 #[test]
1431 fn test_vault_state_empty() {
1432 let state = VaultState {
1433 users: vec![],
1434 api_keys: vec![],
1435 bootstrapped: false,
1436 master_secret: None,
1437 kv: std::collections::HashMap::new(),
1438 };
1439 let serialized = state.serialize();
1440 let restored = VaultState::deserialize(&serialized).unwrap();
1441 assert!(!restored.bootstrapped);
1442 assert!(restored.users.is_empty());
1443 assert!(restored.api_keys.is_empty());
1444 }
1445
1446 #[test]
1447 fn test_vault_state_deserialize_invalid_utf8() {
1448 let bad_data = vec![0xFF, 0xFE, 0xFD];
1449 let result = VaultState::deserialize(&bad_data);
1450 assert!(result.is_err());
1451 }
1452
1453 #[test]
1454 fn test_vault_state_deserialize_bad_user_line() {
1455 let bad = b"USER:only_two\tfields\n";
1456 let result = VaultState::deserialize(bad);
1457 assert!(result.is_err());
1458 }
1459
1460 #[test]
1461 fn test_vault_state_deserialize_bad_key_line() {
1462 let bad = b"KEY:too\tfew\n";
1463 let result = VaultState::deserialize(bad);
1464 assert!(result.is_err());
1465 }
1466
1467 #[test]
1468 fn test_vault_state_deserialize_unknown_line_skipped() {
1469 let data = b"SEALED:false\nFUTURE:some_data\n";
1470 let result = VaultState::deserialize(data).unwrap();
1471 assert!(!result.bootstrapped);
1472 }
1473
1474 #[test]
1475 fn test_vault_pager_save_load_roundtrip() {
1476 let (pager, tmp_dir) = temp_pager();
1477
1478 let kp = KeyPair::generate();
1479 let certificate = kp.certificate_hex();
1480 let vault = Vault::with_certificate(&pager, &certificate).unwrap();
1481
1482 let loaded = vault.load(&pager).unwrap();
1484 assert!(loaded.is_none());
1485
1486 let state = sample_state();
1488 vault.save(&pager, &state).unwrap();
1489
1490 let restored = vault.load(&pager).unwrap().unwrap();
1492 assert!(restored.bootstrapped);
1493 assert_eq!(restored.users.len(), 2);
1494 assert_eq!(restored.api_keys.len(), 1);
1495
1496 let alice = restored
1497 .users
1498 .iter()
1499 .find(|u| u.username == "alice")
1500 .unwrap();
1501 assert_eq!(alice.role, Role::Admin);
1502 assert_eq!(alice.api_keys.len(), 1);
1503
1504 let vault2 = Vault::with_certificate(&pager, &certificate).unwrap();
1506 let restored2 = vault2.load(&pager).unwrap().unwrap();
1507 assert!(restored2.bootstrapped);
1508 assert_eq!(restored2.users.len(), 2);
1509
1510 drop(pager);
1512 let _ = std::fs::remove_dir_all(&tmp_dir);
1513 }
1514
1515 #[test]
1516 fn test_vault_wrong_key_fails_decryption() {
1517 let (pager, tmp_dir) = temp_pager();
1518
1519 let kp = KeyPair::generate();
1521 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1522 let state = VaultState {
1523 users: vec![],
1524 api_keys: vec![],
1525 bootstrapped: true,
1526 master_secret: None,
1527 kv: std::collections::HashMap::new(),
1528 };
1529 vault.save(&pager, &state).unwrap();
1530
1531 let wrong = KeyPair::generate();
1533 let vault2 = Vault::with_certificate_bytes(&pager, &wrong.certificate).unwrap();
1534 let result = vault2.load(&pager);
1535
1536 assert!(result.is_err());
1537
1538 drop(pager);
1540 let _ = std::fs::remove_dir_all(&tmp_dir);
1541 }
1542
1543 #[test]
1544 fn test_vault_no_key_error() {
1545 let (pager, tmp_dir) = temp_pager();
1546
1547 let _guard = env_lock().lock().unwrap();
1548 let old = std::env::var_os("REDDB_CERTIFICATE");
1549 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1550 unsafe {
1551 std::env::remove_var("REDDB_CERTIFICATE");
1552 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1553 }
1554 let result = Vault::open(&pager);
1555 restore_env_var("REDDB_CERTIFICATE", old);
1556 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1557 assert!(matches!(result, Err(VaultError::NoKey)));
1558
1559 drop(pager);
1561 let _ = std::fs::remove_dir_all(&tmp_dir);
1562 }
1563
1564 #[test]
1565 fn test_vault_open_uses_certificate_env() {
1566 let (pager, tmp_dir) = temp_pager();
1567
1568 let _guard = env_lock().lock().unwrap();
1569 let old = std::env::var_os("REDDB_CERTIFICATE");
1570 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1571 let kp = KeyPair::generate();
1572 unsafe {
1573 std::env::set_var("REDDB_CERTIFICATE", kp.certificate_hex());
1574 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1575 }
1576
1577 let vault = Vault::open(&pager).unwrap();
1579 let state = VaultState {
1580 users: vec![],
1581 api_keys: vec![],
1582 bootstrapped: false,
1583 master_secret: None,
1584 kv: std::collections::HashMap::new(),
1585 };
1586 vault.save(&pager, &state).unwrap();
1587
1588 let vault2 = Vault::open(&pager).unwrap();
1590 let loaded = vault2.load(&pager).unwrap().unwrap();
1591 assert!(!loaded.bootstrapped);
1592
1593 restore_env_var("REDDB_CERTIFICATE", old);
1594 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1595 drop(pager);
1596 let _ = std::fs::remove_dir_all(&tmp_dir);
1597 }
1598
1599 #[test]
1600 fn test_vault_open_uses_certificate_file_env() {
1601 let (pager, tmp_dir) = temp_pager();
1602
1603 let _guard = env_lock().lock().unwrap();
1604 let old = std::env::var_os("REDDB_CERTIFICATE");
1605 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1606 let kp = KeyPair::generate();
1607 let cert_path = tmp_dir.join("certificate");
1608 std::fs::write(&cert_path, format!("{}\n", kp.certificate_hex())).unwrap();
1609 unsafe {
1610 std::env::remove_var("REDDB_CERTIFICATE");
1611 std::env::set_var("REDDB_CERTIFICATE_FILE", &cert_path);
1612 }
1613
1614 let vault = Vault::open(&pager).unwrap();
1615 let state = VaultState {
1616 users: vec![],
1617 api_keys: vec![],
1618 bootstrapped: false,
1619 master_secret: None,
1620 kv: std::collections::HashMap::new(),
1621 };
1622 vault.save(&pager, &state).unwrap();
1623
1624 let vault2 = Vault::open(&pager).unwrap();
1625 let loaded = vault2.load(&pager).unwrap().unwrap();
1626 assert!(!loaded.bootstrapped);
1627
1628 restore_env_var("REDDB_CERTIFICATE", old);
1629 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1630 drop(pager);
1631 let _ = std::fs::remove_dir_all(&tmp_dir);
1632 }
1633
1634 #[test]
1639 fn test_keypair_generate_deterministic_certificate() {
1640 let kp = KeyPair::generate();
1641 assert_eq!(kp.master_secret.len(), 32);
1642 assert_eq!(kp.certificate.len(), 32);
1643
1644 let kp2 = KeyPair::from_master_secret(kp.master_secret.clone());
1646 assert_eq!(kp.certificate, kp2.certificate);
1647 }
1648
1649 #[test]
1650 fn test_keypair_sign_verify() {
1651 let kp = KeyPair::generate();
1652 let data = b"session:abc123";
1653 let sig = kp.sign(data);
1654 assert!(kp.verify(data, &sig));
1655
1656 assert!(!kp.verify(b"session:wrong", &sig));
1658
1659 let mut bad_sig = sig.clone();
1661 bad_sig[0] ^= 0xFF;
1662 assert!(!kp.verify(data, &bad_sig));
1663 }
1664
1665 #[test]
1666 fn test_keypair_certificate_hex() {
1667 let kp = KeyPair::generate();
1668 let hex_str = kp.certificate_hex();
1669 assert_eq!(hex_str.len(), 64); let decoded = hex::decode(&hex_str).unwrap();
1671 assert_eq!(decoded, kp.certificate);
1672 }
1673
1674 #[test]
1675 fn test_vault_certificate_seal_roundtrip() {
1676 let (pager, tmp_dir) = temp_pager();
1677
1678 let kp = KeyPair::generate();
1680 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1681
1682 let state = VaultState {
1684 users: vec![],
1685 api_keys: vec![],
1686 bootstrapped: true,
1687 master_secret: Some(kp.master_secret.clone()),
1688 kv: std::collections::HashMap::new(),
1689 };
1690 vault.save(&pager, &state).unwrap();
1691
1692 let vault2 = Vault::with_certificate(&pager, &kp.certificate_hex()).unwrap();
1694 let loaded = vault2.load(&pager).unwrap().unwrap();
1695 assert!(loaded.bootstrapped);
1696 assert_eq!(loaded.master_secret, Some(kp.master_secret.clone()));
1697
1698 let kp2 = KeyPair::from_master_secret(loaded.master_secret.unwrap());
1700 assert_eq!(kp.certificate, kp2.certificate);
1701
1702 drop(pager);
1703 let _ = std::fs::remove_dir_all(&tmp_dir);
1704 }
1705
1706 #[test]
1707 fn test_vault_certificate_wrong_cert_fails() {
1708 let (pager, tmp_dir) = temp_pager();
1709
1710 let kp = KeyPair::generate();
1712 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1713 let state = VaultState {
1714 users: vec![],
1715 api_keys: vec![],
1716 bootstrapped: true,
1717 master_secret: Some(kp.master_secret.clone()),
1718 kv: std::collections::HashMap::new(),
1719 };
1720 vault.save(&pager, &state).unwrap();
1721
1722 let kp2 = KeyPair::generate();
1724 let vault2 = Vault::with_certificate_bytes(&pager, &kp2.certificate).unwrap();
1725 let result = vault2.load(&pager);
1726 assert!(result.is_err());
1727
1728 drop(pager);
1729 let _ = std::fs::remove_dir_all(&tmp_dir);
1730 }
1731
1732 #[test]
1733 fn test_vault_state_master_secret_serialization() {
1734 let secret = vec![0xAA; 32];
1735 let state = VaultState {
1736 users: vec![],
1737 api_keys: vec![],
1738 bootstrapped: true,
1739 master_secret: Some(secret.clone()),
1740 kv: std::collections::HashMap::new(),
1741 };
1742 let serialized = state.serialize();
1743 let text = std::str::from_utf8(&serialized).unwrap();
1744 assert!(text.contains("MASTER_SECRET:"));
1745 assert!(text.contains(&hex::encode(&secret)));
1746
1747 let restored = VaultState::deserialize(&serialized).unwrap();
1748 assert_eq!(restored.master_secret, Some(secret));
1749 assert!(restored.bootstrapped);
1750 }
1751
1752 #[test]
1753 fn test_vault_state_no_master_secret_backward_compat() {
1754 let data = b"SEALED:true\n";
1756 let restored = VaultState::deserialize(data).unwrap();
1757 assert!(restored.master_secret.is_none());
1758 assert!(restored.bootstrapped);
1759 }
1760
1761 #[test]
1762 fn test_vault_state_scram_verifier_roundtrip() {
1763 use crate::auth::scram::ScramVerifier;
1764
1765 let verifier = ScramVerifier::from_password(
1766 "hunter2",
1767 b"reddb-vault-test-salt".to_vec(),
1768 crate::auth::scram::DEFAULT_ITER,
1769 );
1770
1771 let now = now_ms();
1772 let state = VaultState {
1773 users: vec![User {
1774 username: "carol".into(),
1775 tenant_id: None,
1776 password_hash: "argon2id$abc$def".into(),
1777 scram_verifier: Some(verifier.clone()),
1778 role: Role::Admin,
1779 api_keys: vec![],
1780 created_at: now,
1781 updated_at: now,
1782 enabled: true,
1783 }],
1784 api_keys: vec![],
1785 bootstrapped: true,
1786 master_secret: None,
1787 kv: std::collections::HashMap::new(),
1788 };
1789
1790 let bytes = state.serialize();
1791 let restored = VaultState::deserialize(&bytes).unwrap();
1792 let carol = restored
1793 .users
1794 .iter()
1795 .find(|u| u.username == "carol")
1796 .unwrap();
1797 let v = carol.scram_verifier.as_ref().expect("verifier round-trips");
1798 assert_eq!(v.salt, verifier.salt);
1799 assert_eq!(v.iter, verifier.iter);
1800 assert_eq!(v.stored_key, verifier.stored_key);
1801 assert_eq!(v.server_key, verifier.server_key);
1802 }
1803
1804 #[test]
1805 fn test_vault_state_pre_tenant_user_line_still_parses() {
1806 let now = now_ms();
1810 let line = format!(
1811 "USER:dave\targon2id$x$y\tread\ttrue\t{}\t{}\t\nSEALED:false\n",
1812 now, now
1813 );
1814 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1815 let dave = restored
1816 .users
1817 .iter()
1818 .find(|u| u.username == "dave")
1819 .unwrap();
1820 assert!(dave.scram_verifier.is_none());
1821 assert!(dave.tenant_id.is_none());
1822 }
1823
1824 #[test]
1825 fn test_vault_state_tenant_user_line_still_parses() {
1826 let now = now_ms();
1827 let line = format!(
1828 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\nSEALED:false\n",
1829 now, now
1830 );
1831 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1832 let erin = restored
1833 .users
1834 .iter()
1835 .find(|u| u.username == "erin")
1836 .unwrap();
1837 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1838 }
1839
1840 #[test]
1841 fn test_vault_state_legacy_user_line_with_extra_ownership_field_is_ignored() {
1842 let now = now_ms();
1843 let line = format!(
1844 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\ttrue\nSEALED:false\n",
1845 now, now
1846 );
1847 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1848 let erin = restored
1849 .users
1850 .iter()
1851 .find(|u| u.username == "erin")
1852 .unwrap();
1853 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1854 }
1855
1856 #[test]
1857 fn test_vault_state_user_line_with_tenant_roundtrip() {
1858 let now = now_ms();
1859 let state = VaultState {
1860 users: vec![User {
1861 username: "alice".into(),
1862 tenant_id: Some("acme".into()),
1863 password_hash: "argon2id$x$y".into(),
1864 scram_verifier: None,
1865 role: Role::Write,
1866 api_keys: vec![],
1867 created_at: now,
1868 updated_at: now,
1869 enabled: true,
1870 }],
1871 api_keys: vec![],
1872 bootstrapped: true,
1873 master_secret: None,
1874 kv: std::collections::HashMap::new(),
1875 };
1876 let bytes = state.serialize();
1877 let text = std::str::from_utf8(&bytes).unwrap();
1878 assert!(text.contains("\tacme\n"));
1880
1881 let restored = VaultState::deserialize(&bytes).unwrap();
1882 let alice = restored
1883 .users
1884 .iter()
1885 .find(|u| u.username == "alice")
1886 .unwrap();
1887 assert_eq!(alice.tenant_id.as_deref(), Some("acme"));
1888 }
1889
1890 #[test]
1891 fn test_vault_state_key_line_with_tenant_reattaches_correctly() {
1892 let now = now_ms();
1895 let state = VaultState {
1896 users: vec![
1897 User {
1898 username: "alice".into(),
1899 tenant_id: Some("acme".into()),
1900 password_hash: "argon2id$x$y".into(),
1901 scram_verifier: None,
1902 role: Role::Write,
1903 api_keys: vec![],
1904 created_at: now,
1905 updated_at: now,
1906 enabled: true,
1907 },
1908 User {
1909 username: "alice".into(),
1910 tenant_id: Some("globex".into()),
1911 password_hash: "argon2id$a$b".into(),
1912 scram_verifier: None,
1913 role: Role::Read,
1914 api_keys: vec![],
1915 created_at: now,
1916 updated_at: now,
1917 enabled: true,
1918 },
1919 ],
1920 api_keys: vec![
1921 (
1922 UserId::scoped("acme", "alice"),
1923 ApiKey {
1924 key: "rk_acme_key".into(),
1925 name: "deploy".into(),
1926 role: Role::Write,
1927 created_at: now,
1928 expires_at: None,
1929 },
1930 ),
1931 (
1932 UserId::scoped("globex", "alice"),
1933 ApiKey {
1934 key: "rk_globex_key".into(),
1935 name: "ci".into(),
1936 role: Role::Read,
1937 created_at: now,
1938 expires_at: None,
1939 },
1940 ),
1941 ],
1942 bootstrapped: true,
1943 master_secret: None,
1944 kv: std::collections::HashMap::new(),
1945 };
1946 let bytes = state.serialize();
1947 let restored = VaultState::deserialize(&bytes).unwrap();
1948 assert_eq!(restored.api_keys.len(), 2);
1951 let acme_key = restored
1952 .api_keys
1953 .iter()
1954 .find(|(o, _)| o.tenant.as_deref() == Some("acme"))
1955 .unwrap();
1956 assert_eq!(acme_key.1.key, "rk_acme_key");
1957 let globex_key = restored
1958 .api_keys
1959 .iter()
1960 .find(|(o, _)| o.tenant.as_deref() == Some("globex"))
1961 .unwrap();
1962 assert_eq!(globex_key.1.key, "rk_globex_key");
1963 }
1964
1965 #[test]
1966 fn test_vault_state_scram_iter_below_min_rejected() {
1967 let now = now_ms();
1968 let stored_hex = "00".repeat(32);
1972 let server_hex = "11".repeat(32);
1973 let line = format!(
1974 "USER:eve\targon2id$x$y\tread\ttrue\t{}\t{}\tdeadbeef:1024:{}:{}\n",
1975 now, now, stored_hex, server_hex
1976 );
1977 match VaultState::deserialize(line.as_bytes()) {
1978 Err(VaultError::Corrupt(msg)) => assert!(msg.contains("below minimum")),
1979 Err(other) => panic!("expected Corrupt iter-floor error, got {other:?}"),
1980 Ok(_) => panic!("expected Corrupt iter-floor error, got Ok"),
1981 }
1982 }
1983
1984 #[test]
1985 fn test_constant_time_eq_function() {
1986 assert!(constant_time_eq(b"hello", b"hello"));
1987 assert!(!constant_time_eq(b"hello", b"world"));
1988 assert!(!constant_time_eq(b"short", b"longer"));
1989 assert!(constant_time_eq(b"", b""));
1990 }
1991}