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";
92
93use reddb_file::VAULT_LOGICAL_EXPORT_AAD;
97
98const VAULT_MAGIC_SIZE: usize = 4;
100const VAULT_VERSION_SIZE: usize = 1;
101const VAULT_SALT_SIZE: usize = 16;
102const VAULT_PAYLOAD_LEN_SIZE: usize = 4;
103const VAULT_CHAIN_COUNT_SIZE: usize = 4;
104const VAULT_FIRST_PAGE_ID_SIZE: usize = 4;
105
106const NONCE_SIZE: usize = 12;
108
109const VAULT_HEADER_PREAMBLE_SIZE: usize =
111 VAULT_MAGIC_SIZE + VAULT_VERSION_SIZE + VAULT_SALT_SIZE + VAULT_PAYLOAD_LEN_SIZE; const VAULT_HEADER_META_SIZE: usize =
116 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;
124
125const VAULT_HEADER_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_HEADER_META_SIZE;
128
129const VAULT_DATA_CIPHER_CAPACITY: usize = CONTENT_SIZE - VAULT_DATA_PREFIX_SIZE;
132
133pub struct KeyPair {
150 pub master_secret: Vec<u8>,
152 pub certificate: Vec<u8>,
154}
155
156impl KeyPair {
157 pub fn generate() -> Self {
159 let mut master_secret = vec![0u8; 32];
160 os_random::fill_bytes(&mut master_secret).expect("CSPRNG failed during keypair generation");
161 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
162 Self {
163 master_secret,
164 certificate: certificate.to_vec(),
165 }
166 }
167
168 pub fn from_master_secret(master_secret: Vec<u8>) -> Self {
171 let certificate = hmac_sha256(&master_secret, b"reddb-certificate-v1");
172 Self {
173 master_secret,
174 certificate: certificate.to_vec(),
175 }
176 }
177
178 pub fn vault_key_from_certificate(certificate: &[u8]) -> SecureKey {
183 let key_bytes = derive_key(certificate, b"reddb-vault-seal", &vault_argon2_params());
184 SecureKey::new(&key_bytes)
185 }
186
187 pub fn sign(&self, data: &[u8]) -> Vec<u8> {
189 hmac_sha256(&self.master_secret, data).to_vec()
190 }
191
192 pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
194 let expected = self.sign(data);
195 constant_time_eq(&expected, signature)
196 }
197
198 pub fn certificate_hex(&self) -> String {
200 hex::encode(&self.certificate)
201 }
202}
203
204fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
206 if a.len() != b.len() {
207 return false;
208 }
209 let mut diff: u8 = 0;
210 for (x, y) in a.iter().zip(b.iter()) {
211 diff |= x ^ y;
212 }
213 diff == 0
214}
215
216#[derive(Debug)]
222pub enum VaultError {
223 NoKey,
225 Encryption,
227 Decryption,
229 Io(std::io::Error),
231 Corrupt(String),
233 Pager(String),
235}
236
237impl std::fmt::Display for VaultError {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 match self {
240 Self::NoKey => write!(
241 f,
242 "no vault certificate: set REDDB_CERTIFICATE or REDDB_CERTIFICATE_FILE"
243 ),
244 Self::Encryption => write!(f, "vault encryption failed"),
245 Self::Decryption => write!(f, "vault decryption failed (wrong key or corrupt data)"),
246 Self::Io(err) => write!(f, "vault I/O error: {err}"),
247 Self::Corrupt(msg) => write!(f, "vault corrupt: {msg}"),
248 Self::Pager(msg) => write!(f, "vault pager error: {msg}"),
249 }
250 }
251}
252
253impl std::error::Error for VaultError {}
254
255impl From<VaultError> for AuthError {
256 fn from(err: VaultError) -> Self {
257 AuthError::Internal(err.to_string())
258 }
259}
260
261fn decode_certificate_hex(certificate_hex: &str) -> Result<Vec<u8>, VaultError> {
262 let certificate = hex::decode(certificate_hex.trim()).map_err(|_| VaultError::NoKey)?;
263 if certificate.len() != 32 {
264 return Err(VaultError::NoKey);
265 }
266 Ok(certificate)
267}
268
269#[derive(Debug, Default)]
277pub struct VaultState {
278 pub users: Vec<User>,
279 pub api_keys: Vec<(UserId, ApiKey)>,
283 pub bootstrapped: bool,
284 pub master_secret: Option<Vec<u8>>,
288 pub kv: std::collections::HashMap<String, String>,
292}
293
294impl VaultState {
295 pub fn serialize(&self) -> Vec<u8> {
297 let mut out = String::new();
298
299 if let Some(ref secret) = self.master_secret {
301 out.push_str(&format!("MASTER_SECRET:{}\n", hex::encode(secret)));
302 }
303
304 out.push_str(&format!("SEALED:{}\n", self.bootstrapped));
306
307 for user in &self.users {
320 let scram_field = match &user.scram_verifier {
321 Some(v) => format!(
322 "{}:{}:{}:{}",
323 hex::encode(&v.salt),
324 v.iter,
325 hex::encode(v.stored_key),
326 hex::encode(v.server_key),
327 ),
328 None => String::new(),
329 };
330 let tenant_field = user.tenant_id.clone().unwrap_or_default();
331 out.push_str(&format!(
332 "USER:{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
333 user.username,
334 user.password_hash,
335 user.role.as_str(),
336 user.enabled,
337 user.created_at,
338 user.updated_at,
339 scram_field,
340 tenant_field,
341 ));
342 }
343
344 for (owner, key) in &self.api_keys {
348 let tenant_field = owner.tenant.clone().unwrap_or_default();
349 out.push_str(&format!(
350 "KEY:{}\t{}\t{}\t{}\t{}\t{}\n",
351 owner.username,
352 key.key,
353 key.name,
354 key.role.as_str(),
355 key.created_at,
356 tenant_field,
357 ));
358 }
359
360 for (k, v) in &self.kv {
362 out.push_str(&format!("KV:{}\t{}\n", k, hex::encode(v.as_bytes())));
363 }
364
365 out.into_bytes()
366 }
367
368 pub fn deserialize(data: &[u8]) -> Result<Self, VaultError> {
370 let text = std::str::from_utf8(data)
371 .map_err(|_| VaultError::Corrupt("payload is not valid UTF-8".into()))?;
372
373 let mut users = Vec::new();
374 let mut api_keys: Vec<(UserId, ApiKey)> = Vec::new();
375 let mut bootstrapped = false;
376 let mut master_secret: Option<Vec<u8>> = None;
377 let mut kv: std::collections::HashMap<String, String> = std::collections::HashMap::new();
378
379 for line in text.lines() {
380 if line.is_empty() {
381 continue;
382 }
383
384 if let Some(rest) = line.strip_prefix("MASTER_SECRET:") {
385 master_secret = Some(
386 hex::decode(rest)
387 .map_err(|_| VaultError::Corrupt("invalid MASTER_SECRET hex".into()))?,
388 );
389 } else if let Some(rest) = line.strip_prefix("SEALED:") {
390 bootstrapped = rest == "true";
391 } else if let Some(rest) = line.strip_prefix("USER:") {
392 let parts: Vec<&str> = rest.split('\t').collect();
393 if !(7..=9).contains(&parts.len()) {
397 return Err(VaultError::Corrupt(format!(
398 "USER line has {} fields, expected 7, 8, or 9",
399 parts.len()
400 )));
401 }
402 let role = Role::from_str(parts[2])
403 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[2])))?;
404 let enabled = parts[3] == "true";
405 let created_at: u128 = parts[4]
406 .parse()
407 .map_err(|_| VaultError::Corrupt("invalid created_at".into()))?;
408 let updated_at: u128 = parts[5]
409 .parse()
410 .map_err(|_| VaultError::Corrupt("invalid updated_at".into()))?;
411 let scram_verifier = parts
412 .get(6)
413 .map(|s| s.trim())
414 .filter(|s| !s.is_empty())
415 .map(parse_scram_field)
416 .transpose()?;
417 let tenant_id = parts
418 .get(7)
419 .map(|s| s.trim())
420 .filter(|s| !s.is_empty())
421 .map(|s| s.to_string());
422
423 users.push(User {
424 username: parts[0].to_string(),
425 tenant_id,
426 password_hash: parts[1].to_string(),
427 scram_verifier,
428 role,
429 api_keys: Vec::new(), created_at,
431 updated_at,
432 enabled,
433 });
434 } else if let Some(rest) = line.strip_prefix("KEY:") {
435 let parts: Vec<&str> = rest.split('\t').collect();
436 if parts.len() != 5 && parts.len() != 6 {
438 return Err(VaultError::Corrupt(format!(
439 "KEY line has {} fields, expected 5 or 6",
440 parts.len()
441 )));
442 }
443 let role = Role::from_str(parts[3])
444 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[3])))?;
445 let created_at: u128 = parts[4]
446 .parse()
447 .map_err(|_| VaultError::Corrupt("invalid key created_at".into()))?;
448 let tenant_id = parts
449 .get(5)
450 .map(|s| s.trim())
451 .filter(|s| !s.is_empty())
452 .map(|s| s.to_string());
453
454 api_keys.push((
455 UserId {
456 tenant: tenant_id,
457 username: parts[0].to_string(),
458 },
459 ApiKey {
460 key: parts[1].to_string(),
461 name: parts[2].to_string(),
462 role,
463 created_at,
464 },
465 ));
466 } else if let Some(rest) = line.strip_prefix("KV:") {
467 let parts: Vec<&str> = rest.splitn(2, '\t').collect();
468 if parts.len() == 2 {
469 if let Ok(bytes) = hex::decode(parts[1]) {
470 if let Ok(value) = String::from_utf8(bytes) {
471 kv.insert(parts[0].to_string(), value);
472 }
473 }
474 }
475 } else {
476 }
478 }
479
480 for (owner, key) in &api_keys {
484 if let Some(user) = users
485 .iter_mut()
486 .find(|u| u.username == owner.username && u.tenant_id == owner.tenant)
487 {
488 user.api_keys.push(key.clone());
489 }
490 }
491
492 Ok(Self {
493 users,
494 api_keys,
495 bootstrapped,
496 master_secret,
497 kv,
498 })
499 }
500}
501
502pub struct Vault {
513 key: SecureKey,
514 salt: [u8; 16],
515}
516
517fn vault_argon2_params() -> Argon2Params {
520 Argon2Params {
521 m_cost: 16 * 1024, t_cost: 3,
523 p: 1,
524 tag_len: 32,
525 }
526}
527
528impl Vault {
529 pub fn has_saved_state(pager: &Pager) -> bool {
531 pager
532 .read_page_no_checksum(VAULT_HEADER_PAGE)
533 .ok()
534 .map(|page| {
535 let content = page.content();
536 content.len() >= VAULT_MAGIC_SIZE && &content[0..VAULT_MAGIC_SIZE] == VAULT_MAGIC
537 })
538 .unwrap_or(false)
539 }
540
541 pub fn open(pager: &Pager) -> Result<Self, VaultError> {
550 let cert_hex =
551 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE").ok_or(VaultError::NoKey)?;
552 Self::with_certificate(pager, &cert_hex)
553 }
554
555 fn salt_for_open(pager: &Pager) -> Result<[u8; 16], VaultError> {
556 match read_vault_salt_from_pager(pager) {
558 Ok(s) => Ok(s),
559 Err(_) => {
560 let mut salt = [0u8; 16];
562 let mut buf = [0u8; 16];
563 os_random::fill_bytes(&mut buf)
564 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
565 salt.copy_from_slice(&buf);
566 Ok(salt)
567 }
568 }
569 }
570
571 pub fn with_certificate(pager: &Pager, certificate_hex: &str) -> Result<Self, VaultError> {
577 let certificate = decode_certificate_hex(certificate_hex)?;
578
579 let key = KeyPair::vault_key_from_certificate(&certificate);
580
581 let salt = Self::salt_for_open(pager)?;
582
583 Ok(Self { key, salt })
584 }
585
586 pub fn from_env(pager: &Pager) -> Result<Self, VaultError> {
590 Self::open(pager)
591 }
592
593 pub fn with_certificate_bytes(pager: &Pager, certificate: &[u8]) -> Result<Self, VaultError> {
598 if certificate.len() != 32 {
599 return Err(VaultError::NoKey);
600 }
601 let key = KeyPair::vault_key_from_certificate(certificate);
602
603 let salt = match read_vault_salt_from_pager(pager) {
604 Ok(s) => s,
605 Err(_) => {
606 let mut s = [0u8; 16];
607 os_random::fill_bytes(&mut s)
608 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
609 s
610 }
611 };
612
613 Ok(Self { key, salt })
614 }
615
616 pub fn seal_logical_export(&self, state: &VaultState) -> Result<String, VaultError> {
622 let plaintext = state.serialize();
623 let mut nonce = [0u8; NONCE_SIZE];
624 os_random::fill_bytes(&mut nonce)
625 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
626
627 let key_bytes: &[u8] = self.key.as_bytes();
628 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
629 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_LOGICAL_EXPORT_AAD, &plaintext);
630
631 Ok(reddb_file::encode_vault_logical_export(
634 &self.salt,
635 &nonce,
636 &ciphertext,
637 ))
638 }
639
640 pub fn unseal_logical_export(blob_hex: &str) -> Result<VaultState, VaultError> {
643 let cert_hex =
644 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE").ok_or(VaultError::NoKey)?;
645 Self::unseal_logical_export_with_certificate(blob_hex, &cert_hex)
646 }
647
648 pub fn unseal_logical_export_with_certificate(
650 blob_hex: &str,
651 certificate_hex: &str,
652 ) -> Result<VaultState, VaultError> {
653 let (_salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
654 let certificate = decode_certificate_hex(certificate_hex)?;
655 let key = KeyPair::vault_key_from_certificate(&certificate);
656 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
657 }
658
659 fn decode_logical_export(
660 blob_hex: &str,
661 ) -> Result<([u8; VAULT_SALT_SIZE], [u8; NONCE_SIZE], Vec<u8>), VaultError> {
662 let env = reddb_file::decode_vault_logical_export(blob_hex)
665 .map_err(|e| VaultError::Corrupt(e.to_string()))?;
666 Ok((env.salt, env.nonce, env.ciphertext))
667 }
668
669 fn decrypt_logical_export(
670 key: &SecureKey,
671 nonce: &[u8; NONCE_SIZE],
672 ciphertext: &[u8],
673 ) -> Result<VaultState, VaultError> {
674 let key_bytes: &[u8] = key.as_bytes();
675 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
676 let plaintext = aes256_gcm_decrypt(key_arr, nonce, VAULT_LOGICAL_EXPORT_AAD, ciphertext)
677 .map_err(|_| VaultError::Decryption)?;
678 VaultState::deserialize(&plaintext)
679 }
680
681 pub fn save(&self, pager: &Pager, state: &VaultState) -> Result<(), VaultError> {
695 let plaintext = state.serialize();
696
697 let mut nonce = [0u8; NONCE_SIZE];
699 os_random::fill_bytes(&mut nonce)
700 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
701
702 let key_bytes: &[u8] = self.key.as_bytes();
703 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
704 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_AAD, &plaintext);
705 let cipher_total = ciphertext.len();
709 let payload_len = (NONCE_SIZE + cipher_total) as u32; let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
717 let overflow = cipher_total.saturating_sub(header_chunk_len);
718 let chain_count = overflow.div_ceil(VAULT_DATA_CIPHER_CAPACITY);
719
720 while pager
734 .page_count()
735 .map_err(|e| VaultError::Pager(e.to_string()))?
736 <= VAULT_HEADER_PAGE
737 {
738 pager
739 .allocate_page(PageType::Vault)
740 .map_err(|e| VaultError::Pager(format!("reserve vault slot: {e}")))?;
741 }
742
743 let old_chain = self.read_existing_chain_ids(pager).unwrap_or_default();
753
754 let mut new_chain: Vec<u32> = Vec::with_capacity(chain_count);
761 for _ in 0..chain_count {
762 let page = pager
763 .allocate_page(PageType::Vault)
764 .map_err(|e| VaultError::Pager(format!("allocate vault data page: {e}")))?;
765 new_chain.push(page.page_id());
766 }
767
768 let mut cursor = header_chunk_len;
775 for i in 0..chain_count {
776 let next_id = if i + 1 < chain_count {
777 new_chain[i + 1]
778 } else {
779 0
780 };
781 let take = (cipher_total - cursor).min(VAULT_DATA_CIPHER_CAPACITY);
782 let frag = &ciphertext[cursor..cursor + take];
783 self.write_data_page(pager, new_chain[i], next_id, frag)?;
784 cursor += take;
785 }
786 debug_assert_eq!(cursor, cipher_total, "ciphertext spill accounting mismatch");
787
788 let first_data_page = new_chain.first().copied().unwrap_or(0);
796 self.write_header_page(
797 pager,
798 &nonce,
799 payload_len,
800 chain_count as u32,
801 first_data_page,
802 &ciphertext[..header_chunk_len],
803 )?;
804
805 pager
810 .flush()
811 .map_err(|e| VaultError::Pager(e.to_string()))?;
812
813 for &id in old_chain.iter() {
818 pager
819 .free_page(id)
820 .map_err(|e| VaultError::Pager(format!("free old vault page {id}: {e}")))?;
821 }
822
823 Ok(())
824 }
825
826 pub fn load(&self, pager: &Pager) -> Result<Option<VaultState>, VaultError> {
830 let page = match pager.read_page_no_checksum(VAULT_HEADER_PAGE) {
832 Ok(p) => p,
833 Err(_) => return Ok(None),
834 };
835
836 let page_content = page.content();
837
838 if page_content.len() < VAULT_HEADER_META_SIZE {
839 return Ok(None);
840 }
841 if &page_content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
842 return Ok(None); }
844
845 let version = page_content[4];
846 if version == VAULT_LEGACY_VERSION {
847 return Err(VaultError::Corrupt(
851 "vault was bootstrapped with the legacy 2-page format \
852 (pre-RedDB v0.3); re-bootstrap with `red bootstrap` to upgrade"
853 .to_string(),
854 ));
855 }
856 if version != VAULT_VERSION {
857 return Err(VaultError::Corrupt(format!(
858 "unsupported vault version: {} (expected {})",
859 version, VAULT_VERSION
860 )));
861 }
862
863 let payload_len = u32::from_le_bytes(
865 page_content[21..25]
866 .try_into()
867 .map_err(|_| VaultError::Corrupt("bad payload length bytes".into()))?,
868 ) as usize;
869
870 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
871 let nonce: [u8; NONCE_SIZE] = page_content[nonce_start..nonce_start + NONCE_SIZE]
872 .try_into()
873 .map_err(|_| VaultError::Corrupt("bad nonce".into()))?;
874
875 let chain_count_off = nonce_start + NONCE_SIZE;
876 let chain_count = u32::from_le_bytes(
877 page_content[chain_count_off..chain_count_off + 4]
878 .try_into()
879 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
880 ) as usize;
881 let first_id_off = chain_count_off + 4;
882 let mut next_id = u32::from_le_bytes(
883 page_content[first_id_off..first_id_off + 4]
884 .try_into()
885 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
886 );
887
888 if payload_len < NONCE_SIZE {
889 return Err(VaultError::Corrupt("payload too short for nonce".into()));
890 }
891 let cipher_total = payload_len - NONCE_SIZE;
892
893 let mut cipher = Vec::with_capacity(cipher_total);
895 let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
896 let header_cipher_start = VAULT_HEADER_META_SIZE;
897 cipher.extend_from_slice(
898 &page_content[header_cipher_start..header_cipher_start + header_chunk_len],
899 );
900
901 let mut hops = 0usize;
903 while cipher.len() < cipher_total {
907 if hops >= chain_count {
908 return Err(VaultError::Corrupt(format!(
909 "vault chain shorter than declared: {} hops, expected {}",
910 hops, chain_count
911 )));
912 }
913 if next_id == 0 {
914 return Err(VaultError::Corrupt(
915 "vault chain ends prematurely (next_id == 0)".to_string(),
916 ));
917 }
918
919 let dp = pager
920 .read_page_no_checksum(next_id)
921 .map_err(|e| VaultError::Pager(format!("vault data page {next_id}: {e}")))?;
922 let dp_content = dp.content();
923 if dp_content.len() < VAULT_DATA_PREFIX_SIZE {
924 return Err(VaultError::Corrupt(format!(
925 "vault data page {next_id} truncated"
926 )));
927 }
928 if &dp_content[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC {
929 return Err(VaultError::Corrupt(format!(
930 "vault data page {next_id} has bad magic"
931 )));
932 }
933 let np = u32::from_le_bytes(
934 dp_content[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
935 .try_into()
936 .map_err(|_| VaultError::Corrupt("bad next_page_id bytes".into()))?,
937 );
938 let take = (cipher_total - cipher.len()).min(VAULT_DATA_CIPHER_CAPACITY);
939 let frag_start = VAULT_DATA_PREFIX_SIZE;
940 cipher.extend_from_slice(&dp_content[frag_start..frag_start + take]);
941
942 next_id = np;
943 hops += 1;
944 }
945
946 if cipher.len() != cipher_total {
947 return Err(VaultError::Corrupt(format!(
948 "vault truncated: expected {} cipher bytes, got {}",
949 cipher_total,
950 cipher.len()
951 )));
952 }
953 if hops != chain_count {
954 return Err(VaultError::Corrupt(format!(
955 "vault chain length mismatch: walked {} pages, header says {}",
956 hops, chain_count
957 )));
958 }
959
960 let key_bytes: &[u8] = self.key.as_bytes();
962 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
963 let plaintext = aes256_gcm_decrypt(key_arr, &nonce, VAULT_AAD, &cipher)
964 .map_err(|_| VaultError::Decryption)?;
965
966 let state = VaultState::deserialize(&plaintext)?;
967 Ok(Some(state))
968 }
969
970 fn read_existing_chain_ids(&self, pager: &Pager) -> Result<Vec<u32>, VaultError> {
977 let header = pager
978 .read_page_no_checksum(VAULT_HEADER_PAGE)
979 .map_err(|e| VaultError::Pager(e.to_string()))?;
980 let content = header.content();
981 if content.len() < VAULT_HEADER_META_SIZE {
982 return Ok(Vec::new());
983 }
984 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
985 return Ok(Vec::new());
986 }
987 let version = content[4];
988 if version != VAULT_VERSION {
989 return Ok(Vec::new());
993 }
994 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
995 let chain_count_off = nonce_start + NONCE_SIZE;
996 let chain_count = u32::from_le_bytes(
997 content[chain_count_off..chain_count_off + 4]
998 .try_into()
999 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1000 ) as usize;
1001 let first_id_off = chain_count_off + 4;
1002 let mut id = u32::from_le_bytes(
1003 content[first_id_off..first_id_off + 4]
1004 .try_into()
1005 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1006 );
1007
1008 let mut out = Vec::with_capacity(chain_count);
1009 let mut hops = 0usize;
1010 while id != 0 && hops < chain_count {
1011 out.push(id);
1012 match pager.read_page_no_checksum(id) {
1015 Ok(dp) => {
1016 let dc = dp.content();
1017 if dc.len() < VAULT_DATA_PREFIX_SIZE
1018 || &dc[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC
1019 {
1020 break;
1021 }
1022 id = u32::from_le_bytes(
1023 dc[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1024 .try_into()
1025 .map_err(|_| VaultError::Corrupt("bad next_id".into()))?,
1026 );
1027 }
1028 Err(_) => break,
1029 }
1030 hops += 1;
1031 }
1032 Ok(out)
1033 }
1034
1035 fn write_header_page(
1039 &self,
1040 pager: &Pager,
1041 nonce: &[u8; NONCE_SIZE],
1042 payload_len: u32,
1043 chain_count: u32,
1044 first_data_page_id: u32,
1045 cipher_fragment: &[u8],
1046 ) -> Result<(), VaultError> {
1047 debug_assert!(cipher_fragment.len() <= VAULT_HEADER_CIPHER_CAPACITY);
1048
1049 let mut page = Page::new(PageType::Vault, VAULT_HEADER_PAGE);
1050 let bytes = page.as_bytes_mut();
1051 let mut off = HEADER_SIZE;
1052
1053 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_MAGIC);
1054 off += VAULT_MAGIC_SIZE;
1055
1056 bytes[off] = VAULT_VERSION;
1057 off += VAULT_VERSION_SIZE;
1058
1059 bytes[off..off + VAULT_SALT_SIZE].copy_from_slice(&self.salt);
1060 off += VAULT_SALT_SIZE;
1061
1062 bytes[off..off + 4].copy_from_slice(&payload_len.to_le_bytes());
1063 off += VAULT_PAYLOAD_LEN_SIZE;
1064
1065 bytes[off..off + NONCE_SIZE].copy_from_slice(nonce);
1066 off += NONCE_SIZE;
1067
1068 bytes[off..off + 4].copy_from_slice(&chain_count.to_le_bytes());
1069 off += VAULT_CHAIN_COUNT_SIZE;
1070
1071 bytes[off..off + 4].copy_from_slice(&first_data_page_id.to_le_bytes());
1072 off += VAULT_FIRST_PAGE_ID_SIZE;
1073
1074 debug_assert_eq!(off, HEADER_SIZE + VAULT_HEADER_META_SIZE);
1075
1076 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1077
1078 pager
1079 .write_page_no_checksum(VAULT_HEADER_PAGE, page)
1080 .map_err(|e| VaultError::Pager(e.to_string()))?;
1081 Ok(())
1082 }
1083
1084 fn write_data_page(
1086 &self,
1087 pager: &Pager,
1088 page_id: u32,
1089 next_page_id: u32,
1090 cipher_fragment: &[u8],
1091 ) -> Result<(), VaultError> {
1092 debug_assert!(cipher_fragment.len() <= VAULT_DATA_CIPHER_CAPACITY);
1093
1094 let mut page = Page::new(PageType::Vault, page_id);
1095 let bytes = page.as_bytes_mut();
1096 let mut off = HEADER_SIZE;
1097
1098 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_DATA_MAGIC);
1099 off += VAULT_MAGIC_SIZE;
1100
1101 bytes[off..off + 4].copy_from_slice(&next_page_id.to_le_bytes());
1102 off += 4;
1103
1104 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1105
1106 pager
1107 .write_page_no_checksum(page_id, page)
1108 .map_err(|e| VaultError::Pager(e.to_string()))?;
1109 Ok(())
1110 }
1111}
1112
1113fn parse_scram_field(field: &str) -> Result<crate::auth::scram::ScramVerifier, VaultError> {
1120 let parts: Vec<&str> = field.split(':').collect();
1121 if parts.len() != 4 {
1122 return Err(VaultError::Corrupt(format!(
1123 "SCRAM verifier has {} segments, expected 4",
1124 parts.len()
1125 )));
1126 }
1127 let salt =
1128 hex::decode(parts[0]).map_err(|_| VaultError::Corrupt("invalid SCRAM salt hex".into()))?;
1129 let iter: u32 = parts[1]
1130 .parse()
1131 .map_err(|_| VaultError::Corrupt("invalid SCRAM iter".into()))?;
1132 if iter < crate::auth::scram::MIN_ITER {
1133 return Err(VaultError::Corrupt(format!(
1134 "SCRAM iter {} below minimum {}",
1135 iter,
1136 crate::auth::scram::MIN_ITER
1137 )));
1138 }
1139 let stored_vec = hex::decode(parts[2])
1140 .map_err(|_| VaultError::Corrupt("invalid SCRAM stored_key hex".into()))?;
1141 let server_vec = hex::decode(parts[3])
1142 .map_err(|_| VaultError::Corrupt("invalid SCRAM server_key hex".into()))?;
1143 let stored_key: [u8; 32] = stored_vec
1144 .try_into()
1145 .map_err(|_| VaultError::Corrupt("SCRAM stored_key must be 32 bytes".into()))?;
1146 let server_key: [u8; 32] = server_vec
1147 .try_into()
1148 .map_err(|_| VaultError::Corrupt("SCRAM server_key must be 32 bytes".into()))?;
1149 Ok(crate::auth::scram::ScramVerifier {
1150 salt,
1151 iter,
1152 stored_key,
1153 server_key,
1154 })
1155}
1156
1157fn read_vault_salt_from_pager(pager: &Pager) -> Result<[u8; 16], VaultError> {
1164 let page = pager
1165 .read_page_no_checksum(VAULT_HEADER_PAGE)
1166 .map_err(|e| VaultError::Pager(format!("vault page read: {e}")))?;
1167
1168 let content = page.content();
1169 if content.len() < VAULT_HEADER_PREAMBLE_SIZE {
1170 return Err(VaultError::Corrupt("vault page too short".into()));
1171 }
1172 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1173 return Err(VaultError::Corrupt("bad magic bytes".into()));
1174 }
1175
1176 let mut salt = [0u8; VAULT_SALT_SIZE];
1177 salt.copy_from_slice(&content[5..21]);
1178 Ok(salt)
1179}
1180
1181#[cfg(test)]
1186mod tests {
1187 use super::*;
1188 use crate::auth::{now_ms, ApiKey, Role, User};
1189 use crate::storage::engine::pager::PagerConfig;
1190
1191 fn sample_state() -> VaultState {
1192 let now = now_ms();
1193 VaultState {
1194 users: vec![
1195 User {
1196 username: "alice".into(),
1197 tenant_id: None,
1198 password_hash: "argon2id$aabbccdd$eeff0011".into(),
1199 scram_verifier: None,
1200 role: Role::Admin,
1201 api_keys: vec![ApiKey {
1202 key: "rk_abc123".into(),
1203 name: "ci-token".into(),
1204 role: Role::Write,
1205 created_at: now,
1206 }],
1207 created_at: now,
1208 updated_at: now,
1209 enabled: true,
1210 },
1211 User {
1212 username: "bob".into(),
1213 tenant_id: None,
1214 password_hash: "argon2id$11223344$55667788".into(),
1215 scram_verifier: None,
1216 role: Role::Read,
1217 api_keys: vec![],
1218 created_at: now,
1219 updated_at: now,
1220 enabled: false,
1221 },
1222 ],
1223 api_keys: vec![(
1224 UserId::platform("alice"),
1225 ApiKey {
1226 key: "rk_abc123".into(),
1227 name: "ci-token".into(),
1228 role: Role::Write,
1229 created_at: now,
1230 },
1231 )],
1232 bootstrapped: true,
1233 master_secret: None,
1234 kv: std::collections::HashMap::new(),
1235 }
1236 }
1237
1238 fn temp_pager() -> (Pager, std::path::PathBuf) {
1240 use std::sync::atomic::{AtomicU64, Ordering};
1241 static COUNTER: AtomicU64 = AtomicU64::new(0);
1242 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1243 let tmp_dir =
1244 std::env::temp_dir().join(format!("reddb_vault_test_{}_{}", std::process::id(), id));
1245 std::fs::create_dir_all(&tmp_dir).unwrap();
1246 let db_path = tmp_dir.join("test.rdb");
1247 let pager = Pager::open(&db_path, PagerConfig::default()).unwrap();
1248 (pager, tmp_dir)
1249 }
1250
1251 fn env_lock() -> &'static std::sync::Mutex<()> {
1252 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
1253 LOCK.get_or_init(|| std::sync::Mutex::new(()))
1254 }
1255
1256 fn restore_env_var(name: &str, value: Option<std::ffi::OsString>) {
1257 unsafe {
1258 match value {
1259 Some(value) => std::env::set_var(name, value),
1260 None => std::env::remove_var(name),
1261 }
1262 }
1263 }
1264
1265 #[test]
1266 fn test_vault_state_serialize_deserialize_roundtrip() {
1267 let state = sample_state();
1268 let serialized = state.serialize();
1269 let text = std::str::from_utf8(&serialized).unwrap();
1270
1271 assert!(text.contains("SEALED:true"));
1273 assert!(text.contains("USER:alice\t"));
1274 assert!(text.contains("USER:bob\t"));
1275 assert!(text.contains("KEY:alice\trk_abc123\t"));
1276
1277 let restored = VaultState::deserialize(&serialized).unwrap();
1279 assert!(restored.bootstrapped);
1280 assert_eq!(restored.users.len(), 2);
1281
1282 let alice = restored
1283 .users
1284 .iter()
1285 .find(|u| u.username == "alice")
1286 .unwrap();
1287 assert_eq!(alice.role, Role::Admin);
1288 assert!(alice.enabled);
1289 assert_eq!(alice.password_hash, "argon2id$aabbccdd$eeff0011");
1290 assert_eq!(alice.api_keys.len(), 1);
1291 assert_eq!(alice.api_keys[0].key, "rk_abc123");
1292 assert_eq!(alice.api_keys[0].name, "ci-token");
1293 assert_eq!(alice.api_keys[0].role, Role::Write);
1294
1295 let bob = restored.users.iter().find(|u| u.username == "bob").unwrap();
1296 assert_eq!(bob.role, Role::Read);
1297 assert!(!bob.enabled);
1298 assert!(bob.api_keys.is_empty());
1299
1300 assert_eq!(restored.api_keys.len(), 1);
1301 assert_eq!(restored.api_keys[0].0.username, "alice");
1302 assert!(restored.api_keys[0].0.tenant.is_none());
1303 }
1304
1305 #[test]
1306 fn test_vault_state_empty() {
1307 let state = VaultState {
1308 users: vec![],
1309 api_keys: vec![],
1310 bootstrapped: false,
1311 master_secret: None,
1312 kv: std::collections::HashMap::new(),
1313 };
1314 let serialized = state.serialize();
1315 let restored = VaultState::deserialize(&serialized).unwrap();
1316 assert!(!restored.bootstrapped);
1317 assert!(restored.users.is_empty());
1318 assert!(restored.api_keys.is_empty());
1319 }
1320
1321 #[test]
1322 fn test_vault_state_deserialize_invalid_utf8() {
1323 let bad_data = vec![0xFF, 0xFE, 0xFD];
1324 let result = VaultState::deserialize(&bad_data);
1325 assert!(result.is_err());
1326 }
1327
1328 #[test]
1329 fn test_vault_state_deserialize_bad_user_line() {
1330 let bad = b"USER:only_two\tfields\n";
1331 let result = VaultState::deserialize(bad);
1332 assert!(result.is_err());
1333 }
1334
1335 #[test]
1336 fn test_vault_state_deserialize_bad_key_line() {
1337 let bad = b"KEY:too\tfew\n";
1338 let result = VaultState::deserialize(bad);
1339 assert!(result.is_err());
1340 }
1341
1342 #[test]
1343 fn test_vault_state_deserialize_unknown_line_skipped() {
1344 let data = b"SEALED:false\nFUTURE:some_data\n";
1345 let result = VaultState::deserialize(data).unwrap();
1346 assert!(!result.bootstrapped);
1347 }
1348
1349 #[test]
1350 fn test_vault_pager_save_load_roundtrip() {
1351 let (pager, tmp_dir) = temp_pager();
1352
1353 let kp = KeyPair::generate();
1354 let certificate = kp.certificate_hex();
1355 let vault = Vault::with_certificate(&pager, &certificate).unwrap();
1356
1357 let loaded = vault.load(&pager).unwrap();
1359 assert!(loaded.is_none());
1360
1361 let state = sample_state();
1363 vault.save(&pager, &state).unwrap();
1364
1365 let restored = vault.load(&pager).unwrap().unwrap();
1367 assert!(restored.bootstrapped);
1368 assert_eq!(restored.users.len(), 2);
1369 assert_eq!(restored.api_keys.len(), 1);
1370
1371 let alice = restored
1372 .users
1373 .iter()
1374 .find(|u| u.username == "alice")
1375 .unwrap();
1376 assert_eq!(alice.role, Role::Admin);
1377 assert_eq!(alice.api_keys.len(), 1);
1378
1379 let vault2 = Vault::with_certificate(&pager, &certificate).unwrap();
1381 let restored2 = vault2.load(&pager).unwrap().unwrap();
1382 assert!(restored2.bootstrapped);
1383 assert_eq!(restored2.users.len(), 2);
1384
1385 drop(pager);
1387 let _ = std::fs::remove_dir_all(&tmp_dir);
1388 }
1389
1390 #[test]
1391 fn test_vault_wrong_key_fails_decryption() {
1392 let (pager, tmp_dir) = temp_pager();
1393
1394 let kp = KeyPair::generate();
1396 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1397 let state = VaultState {
1398 users: vec![],
1399 api_keys: vec![],
1400 bootstrapped: true,
1401 master_secret: None,
1402 kv: std::collections::HashMap::new(),
1403 };
1404 vault.save(&pager, &state).unwrap();
1405
1406 let wrong = KeyPair::generate();
1408 let vault2 = Vault::with_certificate_bytes(&pager, &wrong.certificate).unwrap();
1409 let result = vault2.load(&pager);
1410
1411 assert!(result.is_err());
1412
1413 drop(pager);
1415 let _ = std::fs::remove_dir_all(&tmp_dir);
1416 }
1417
1418 #[test]
1419 fn test_vault_no_key_error() {
1420 let (pager, tmp_dir) = temp_pager();
1421
1422 let _guard = env_lock().lock().unwrap();
1423 let old = std::env::var_os("REDDB_CERTIFICATE");
1424 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1425 unsafe {
1426 std::env::remove_var("REDDB_CERTIFICATE");
1427 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1428 }
1429 let result = Vault::open(&pager);
1430 restore_env_var("REDDB_CERTIFICATE", old);
1431 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1432 assert!(matches!(result, Err(VaultError::NoKey)));
1433
1434 drop(pager);
1436 let _ = std::fs::remove_dir_all(&tmp_dir);
1437 }
1438
1439 #[test]
1440 fn test_vault_open_uses_certificate_env() {
1441 let (pager, tmp_dir) = temp_pager();
1442
1443 let _guard = env_lock().lock().unwrap();
1444 let old = std::env::var_os("REDDB_CERTIFICATE");
1445 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1446 let kp = KeyPair::generate();
1447 unsafe {
1448 std::env::set_var("REDDB_CERTIFICATE", kp.certificate_hex());
1449 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1450 }
1451
1452 let vault = Vault::open(&pager).unwrap();
1454 let state = VaultState {
1455 users: vec![],
1456 api_keys: vec![],
1457 bootstrapped: false,
1458 master_secret: None,
1459 kv: std::collections::HashMap::new(),
1460 };
1461 vault.save(&pager, &state).unwrap();
1462
1463 let vault2 = Vault::open(&pager).unwrap();
1465 let loaded = vault2.load(&pager).unwrap().unwrap();
1466 assert!(!loaded.bootstrapped);
1467
1468 restore_env_var("REDDB_CERTIFICATE", old);
1469 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1470 drop(pager);
1471 let _ = std::fs::remove_dir_all(&tmp_dir);
1472 }
1473
1474 #[test]
1475 fn test_vault_open_uses_certificate_file_env() {
1476 let (pager, tmp_dir) = temp_pager();
1477
1478 let _guard = env_lock().lock().unwrap();
1479 let old = std::env::var_os("REDDB_CERTIFICATE");
1480 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1481 let kp = KeyPair::generate();
1482 let cert_path = tmp_dir.join("certificate");
1483 std::fs::write(&cert_path, format!("{}\n", kp.certificate_hex())).unwrap();
1484 unsafe {
1485 std::env::remove_var("REDDB_CERTIFICATE");
1486 std::env::set_var("REDDB_CERTIFICATE_FILE", &cert_path);
1487 }
1488
1489 let vault = Vault::open(&pager).unwrap();
1490 let state = VaultState {
1491 users: vec![],
1492 api_keys: vec![],
1493 bootstrapped: false,
1494 master_secret: None,
1495 kv: std::collections::HashMap::new(),
1496 };
1497 vault.save(&pager, &state).unwrap();
1498
1499 let vault2 = Vault::open(&pager).unwrap();
1500 let loaded = vault2.load(&pager).unwrap().unwrap();
1501 assert!(!loaded.bootstrapped);
1502
1503 restore_env_var("REDDB_CERTIFICATE", old);
1504 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1505 drop(pager);
1506 let _ = std::fs::remove_dir_all(&tmp_dir);
1507 }
1508
1509 #[test]
1514 fn test_keypair_generate_deterministic_certificate() {
1515 let kp = KeyPair::generate();
1516 assert_eq!(kp.master_secret.len(), 32);
1517 assert_eq!(kp.certificate.len(), 32);
1518
1519 let kp2 = KeyPair::from_master_secret(kp.master_secret.clone());
1521 assert_eq!(kp.certificate, kp2.certificate);
1522 }
1523
1524 #[test]
1525 fn test_keypair_sign_verify() {
1526 let kp = KeyPair::generate();
1527 let data = b"session:abc123";
1528 let sig = kp.sign(data);
1529 assert!(kp.verify(data, &sig));
1530
1531 assert!(!kp.verify(b"session:wrong", &sig));
1533
1534 let mut bad_sig = sig.clone();
1536 bad_sig[0] ^= 0xFF;
1537 assert!(!kp.verify(data, &bad_sig));
1538 }
1539
1540 #[test]
1541 fn test_keypair_certificate_hex() {
1542 let kp = KeyPair::generate();
1543 let hex_str = kp.certificate_hex();
1544 assert_eq!(hex_str.len(), 64); let decoded = hex::decode(&hex_str).unwrap();
1546 assert_eq!(decoded, kp.certificate);
1547 }
1548
1549 #[test]
1550 fn test_vault_certificate_seal_roundtrip() {
1551 let (pager, tmp_dir) = temp_pager();
1552
1553 let kp = KeyPair::generate();
1555 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1556
1557 let state = VaultState {
1559 users: vec![],
1560 api_keys: vec![],
1561 bootstrapped: true,
1562 master_secret: Some(kp.master_secret.clone()),
1563 kv: std::collections::HashMap::new(),
1564 };
1565 vault.save(&pager, &state).unwrap();
1566
1567 let vault2 = Vault::with_certificate(&pager, &kp.certificate_hex()).unwrap();
1569 let loaded = vault2.load(&pager).unwrap().unwrap();
1570 assert!(loaded.bootstrapped);
1571 assert_eq!(loaded.master_secret, Some(kp.master_secret.clone()));
1572
1573 let kp2 = KeyPair::from_master_secret(loaded.master_secret.unwrap());
1575 assert_eq!(kp.certificate, kp2.certificate);
1576
1577 drop(pager);
1578 let _ = std::fs::remove_dir_all(&tmp_dir);
1579 }
1580
1581 #[test]
1582 fn test_vault_certificate_wrong_cert_fails() {
1583 let (pager, tmp_dir) = temp_pager();
1584
1585 let kp = KeyPair::generate();
1587 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1588 let state = VaultState {
1589 users: vec![],
1590 api_keys: vec![],
1591 bootstrapped: true,
1592 master_secret: Some(kp.master_secret.clone()),
1593 kv: std::collections::HashMap::new(),
1594 };
1595 vault.save(&pager, &state).unwrap();
1596
1597 let kp2 = KeyPair::generate();
1599 let vault2 = Vault::with_certificate_bytes(&pager, &kp2.certificate).unwrap();
1600 let result = vault2.load(&pager);
1601 assert!(result.is_err());
1602
1603 drop(pager);
1604 let _ = std::fs::remove_dir_all(&tmp_dir);
1605 }
1606
1607 #[test]
1608 fn test_vault_state_master_secret_serialization() {
1609 let secret = vec![0xAA; 32];
1610 let state = VaultState {
1611 users: vec![],
1612 api_keys: vec![],
1613 bootstrapped: true,
1614 master_secret: Some(secret.clone()),
1615 kv: std::collections::HashMap::new(),
1616 };
1617 let serialized = state.serialize();
1618 let text = std::str::from_utf8(&serialized).unwrap();
1619 assert!(text.contains("MASTER_SECRET:"));
1620 assert!(text.contains(&hex::encode(&secret)));
1621
1622 let restored = VaultState::deserialize(&serialized).unwrap();
1623 assert_eq!(restored.master_secret, Some(secret));
1624 assert!(restored.bootstrapped);
1625 }
1626
1627 #[test]
1628 fn test_vault_state_no_master_secret_backward_compat() {
1629 let data = b"SEALED:true\n";
1631 let restored = VaultState::deserialize(data).unwrap();
1632 assert!(restored.master_secret.is_none());
1633 assert!(restored.bootstrapped);
1634 }
1635
1636 #[test]
1637 fn test_vault_state_scram_verifier_roundtrip() {
1638 use crate::auth::scram::ScramVerifier;
1639
1640 let verifier = ScramVerifier::from_password(
1641 "hunter2",
1642 b"reddb-vault-test-salt".to_vec(),
1643 crate::auth::scram::DEFAULT_ITER,
1644 );
1645
1646 let now = now_ms();
1647 let state = VaultState {
1648 users: vec![User {
1649 username: "carol".into(),
1650 tenant_id: None,
1651 password_hash: "argon2id$abc$def".into(),
1652 scram_verifier: Some(verifier.clone()),
1653 role: Role::Admin,
1654 api_keys: vec![],
1655 created_at: now,
1656 updated_at: now,
1657 enabled: true,
1658 }],
1659 api_keys: vec![],
1660 bootstrapped: true,
1661 master_secret: None,
1662 kv: std::collections::HashMap::new(),
1663 };
1664
1665 let bytes = state.serialize();
1666 let restored = VaultState::deserialize(&bytes).unwrap();
1667 let carol = restored
1668 .users
1669 .iter()
1670 .find(|u| u.username == "carol")
1671 .unwrap();
1672 let v = carol.scram_verifier.as_ref().expect("verifier round-trips");
1673 assert_eq!(v.salt, verifier.salt);
1674 assert_eq!(v.iter, verifier.iter);
1675 assert_eq!(v.stored_key, verifier.stored_key);
1676 assert_eq!(v.server_key, verifier.server_key);
1677 }
1678
1679 #[test]
1680 fn test_vault_state_pre_tenant_user_line_still_parses() {
1681 let now = now_ms();
1685 let line = format!(
1686 "USER:dave\targon2id$x$y\tread\ttrue\t{}\t{}\t\nSEALED:false\n",
1687 now, now
1688 );
1689 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1690 let dave = restored
1691 .users
1692 .iter()
1693 .find(|u| u.username == "dave")
1694 .unwrap();
1695 assert!(dave.scram_verifier.is_none());
1696 assert!(dave.tenant_id.is_none());
1697 }
1698
1699 #[test]
1700 fn test_vault_state_tenant_user_line_still_parses() {
1701 let now = now_ms();
1702 let line = format!(
1703 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\nSEALED:false\n",
1704 now, now
1705 );
1706 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1707 let erin = restored
1708 .users
1709 .iter()
1710 .find(|u| u.username == "erin")
1711 .unwrap();
1712 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1713 }
1714
1715 #[test]
1716 fn test_vault_state_legacy_user_line_with_extra_ownership_field_is_ignored() {
1717 let now = now_ms();
1718 let line = format!(
1719 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\ttrue\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 }
1730
1731 #[test]
1732 fn test_vault_state_user_line_with_tenant_roundtrip() {
1733 let now = now_ms();
1734 let state = VaultState {
1735 users: vec![User {
1736 username: "alice".into(),
1737 tenant_id: Some("acme".into()),
1738 password_hash: "argon2id$x$y".into(),
1739 scram_verifier: None,
1740 role: Role::Write,
1741 api_keys: vec![],
1742 created_at: now,
1743 updated_at: now,
1744 enabled: true,
1745 }],
1746 api_keys: vec![],
1747 bootstrapped: true,
1748 master_secret: None,
1749 kv: std::collections::HashMap::new(),
1750 };
1751 let bytes = state.serialize();
1752 let text = std::str::from_utf8(&bytes).unwrap();
1753 assert!(text.contains("\tacme\n"));
1755
1756 let restored = VaultState::deserialize(&bytes).unwrap();
1757 let alice = restored
1758 .users
1759 .iter()
1760 .find(|u| u.username == "alice")
1761 .unwrap();
1762 assert_eq!(alice.tenant_id.as_deref(), Some("acme"));
1763 }
1764
1765 #[test]
1766 fn test_vault_state_key_line_with_tenant_reattaches_correctly() {
1767 let now = now_ms();
1770 let state = VaultState {
1771 users: vec![
1772 User {
1773 username: "alice".into(),
1774 tenant_id: Some("acme".into()),
1775 password_hash: "argon2id$x$y".into(),
1776 scram_verifier: None,
1777 role: Role::Write,
1778 api_keys: vec![],
1779 created_at: now,
1780 updated_at: now,
1781 enabled: true,
1782 },
1783 User {
1784 username: "alice".into(),
1785 tenant_id: Some("globex".into()),
1786 password_hash: "argon2id$a$b".into(),
1787 scram_verifier: None,
1788 role: Role::Read,
1789 api_keys: vec![],
1790 created_at: now,
1791 updated_at: now,
1792 enabled: true,
1793 },
1794 ],
1795 api_keys: vec![
1796 (
1797 UserId::scoped("acme", "alice"),
1798 ApiKey {
1799 key: "rk_acme_key".into(),
1800 name: "deploy".into(),
1801 role: Role::Write,
1802 created_at: now,
1803 },
1804 ),
1805 (
1806 UserId::scoped("globex", "alice"),
1807 ApiKey {
1808 key: "rk_globex_key".into(),
1809 name: "ci".into(),
1810 role: Role::Read,
1811 created_at: now,
1812 },
1813 ),
1814 ],
1815 bootstrapped: true,
1816 master_secret: None,
1817 kv: std::collections::HashMap::new(),
1818 };
1819 let bytes = state.serialize();
1820 let restored = VaultState::deserialize(&bytes).unwrap();
1821 assert_eq!(restored.api_keys.len(), 2);
1824 let acme_key = restored
1825 .api_keys
1826 .iter()
1827 .find(|(o, _)| o.tenant.as_deref() == Some("acme"))
1828 .unwrap();
1829 assert_eq!(acme_key.1.key, "rk_acme_key");
1830 let globex_key = restored
1831 .api_keys
1832 .iter()
1833 .find(|(o, _)| o.tenant.as_deref() == Some("globex"))
1834 .unwrap();
1835 assert_eq!(globex_key.1.key, "rk_globex_key");
1836 }
1837
1838 #[test]
1839 fn test_vault_state_scram_iter_below_min_rejected() {
1840 let now = now_ms();
1841 let stored_hex = "00".repeat(32);
1845 let server_hex = "11".repeat(32);
1846 let line = format!(
1847 "USER:eve\targon2id$x$y\tread\ttrue\t{}\t{}\tdeadbeef:1024:{}:{}\n",
1848 now, now, stored_hex, server_hex
1849 );
1850 match VaultState::deserialize(line.as_bytes()) {
1851 Err(VaultError::Corrupt(msg)) => assert!(msg.contains("below minimum")),
1852 Err(other) => panic!("expected Corrupt iter-floor error, got {other:?}"),
1853 Ok(_) => panic!("expected Corrupt iter-floor error, got Ok"),
1854 }
1855 }
1856
1857 #[test]
1858 fn test_constant_time_eq_function() {
1859 assert!(constant_time_eq(b"hello", b"hello"));
1860 assert!(!constant_time_eq(b"hello", b"world"));
1861 assert!(!constant_time_eq(b"short", b"longer"));
1862 assert!(constant_time_eq(b"", b""));
1863 }
1864}