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 crate::crypto::constant_time_eq(a, b)
207}
208
209#[derive(Debug)]
215pub enum VaultError {
216 NoKey,
218 Encryption,
220 Decryption,
222 Io(std::io::Error),
224 Corrupt(String),
226 Pager(String),
228}
229
230impl std::fmt::Display for VaultError {
231 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232 match self {
233 Self::NoKey => write!(
234 f,
235 "no vault certificate: set REDDB_CERTIFICATE or REDDB_CERTIFICATE_FILE"
236 ),
237 Self::Encryption => write!(f, "vault encryption failed"),
238 Self::Decryption => write!(f, "vault decryption failed (wrong key or corrupt data)"),
239 Self::Io(err) => write!(f, "vault I/O error: {err}"),
240 Self::Corrupt(msg) => write!(f, "vault corrupt: {msg}"),
241 Self::Pager(msg) => write!(f, "vault pager error: {msg}"),
242 }
243 }
244}
245
246impl std::error::Error for VaultError {}
247
248impl From<VaultError> for AuthError {
249 fn from(err: VaultError) -> Self {
250 AuthError::Internal(err.to_string())
251 }
252}
253
254fn decode_certificate_hex(certificate_hex: &str) -> Result<Vec<u8>, VaultError> {
255 let certificate = hex::decode(certificate_hex.trim()).map_err(|_| VaultError::NoKey)?;
256 if certificate.len() != 32 {
257 return Err(VaultError::NoKey);
258 }
259 Ok(certificate)
260}
261
262#[derive(Default)]
270pub struct VaultState {
271 pub users: Vec<User>,
272 pub api_keys: Vec<(UserId, ApiKey)>,
276 pub bootstrapped: bool,
277 pub master_secret: Option<Vec<u8>>,
281 pub kv: std::collections::HashMap<String, String>,
285}
286
287impl std::fmt::Debug for VaultState {
288 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289 f.debug_struct("VaultState")
290 .field("users", &self.users.len())
291 .field("api_keys", &self.api_keys.len())
292 .field("bootstrapped", &self.bootstrapped)
293 .field(
294 "master_secret",
295 &self.master_secret.as_ref().map(|_| "<redacted>"),
296 )
297 .field("kv", &self.kv.len())
298 .finish()
299 }
300}
301
302impl VaultState {
303 pub fn serialize(&self) -> Vec<u8> {
305 let mut out = String::new();
306
307 if let Some(ref secret) = self.master_secret {
309 out.push_str(&format!("MASTER_SECRET:{}\n", hex::encode(secret)));
310 }
311
312 out.push_str(&format!("SEALED:{}\n", self.bootstrapped));
314
315 for user in &self.users {
328 let scram_field = match &user.scram_verifier {
329 Some(v) => format!(
330 "{}:{}:{}:{}",
331 hex::encode(&v.salt),
332 v.iter,
333 hex::encode(v.stored_key),
334 hex::encode(v.server_key),
335 ),
336 None => String::new(),
337 };
338 let tenant_field = user.tenant_id.clone().unwrap_or_default();
339 out.push_str(&format!(
340 "USER:{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
341 user.username,
342 user.password_hash,
343 user.role.as_str(),
344 user.enabled,
345 user.created_at,
346 user.updated_at,
347 scram_field,
348 tenant_field,
349 ));
350 }
351
352 for (owner, key) in &self.api_keys {
357 let tenant_field = owner.tenant.clone().unwrap_or_default();
358 let expires_field = key
359 .expires_at
360 .map(|deadline| deadline.to_string())
361 .unwrap_or_default();
362 out.push_str(&format!(
363 "KEY:{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
364 owner.username,
365 key.key,
366 key.name,
367 key.role.as_str(),
368 key.created_at,
369 tenant_field,
370 expires_field,
371 ));
372 }
373
374 for (k, v) in &self.kv {
376 out.push_str(&format!("KV:{}\t{}\n", k, hex::encode(v.as_bytes())));
377 }
378
379 out.into_bytes()
380 }
381
382 pub fn deserialize(data: &[u8]) -> Result<Self, VaultError> {
384 let text = std::str::from_utf8(data)
385 .map_err(|_| VaultError::Corrupt("payload is not valid UTF-8".into()))?;
386
387 let mut users = Vec::new();
388 let mut api_keys: Vec<(UserId, ApiKey)> = Vec::new();
389 let mut bootstrapped = false;
390 let mut master_secret: Option<Vec<u8>> = None;
391 let mut kv: std::collections::HashMap<String, String> = std::collections::HashMap::new();
392
393 for line in text.lines() {
394 if line.is_empty() {
395 continue;
396 }
397
398 if let Some(rest) = line.strip_prefix("MASTER_SECRET:") {
399 master_secret = Some(
400 hex::decode(rest)
401 .map_err(|_| VaultError::Corrupt("invalid MASTER_SECRET hex".into()))?,
402 );
403 } else if let Some(rest) = line.strip_prefix("SEALED:") {
404 bootstrapped = rest == "true";
405 } else if let Some(rest) = line.strip_prefix("USER:") {
406 let parts: Vec<&str> = rest.split('\t').collect();
407 if !(7..=9).contains(&parts.len()) {
411 return Err(VaultError::Corrupt(format!(
412 "USER line has {} fields, expected 7, 8, or 9",
413 parts.len()
414 )));
415 }
416 let role = Role::from_str(parts[2])
417 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[2])))?;
418 let enabled = parts[3] == "true";
419 let created_at: u128 = parts[4]
420 .parse()
421 .map_err(|_| VaultError::Corrupt("invalid created_at".into()))?;
422 let updated_at: u128 = parts[5]
423 .parse()
424 .map_err(|_| VaultError::Corrupt("invalid updated_at".into()))?;
425 let scram_verifier = parts
426 .get(6)
427 .map(|s| s.trim())
428 .filter(|s| !s.is_empty())
429 .map(parse_scram_field)
430 .transpose()?;
431 let tenant_id = parts
432 .get(7)
433 .map(|s| s.trim())
434 .filter(|s| !s.is_empty())
435 .map(|s| s.to_string());
436
437 users.push(User {
438 username: parts[0].to_string(),
439 tenant_id,
440 password_hash: parts[1].to_string(),
441 scram_verifier,
442 role,
443 api_keys: Vec::new(), created_at,
445 updated_at,
446 enabled,
447 });
448 } else if let Some(rest) = line.strip_prefix("KEY:") {
449 let parts: Vec<&str> = rest.split('\t').collect();
450 if !(5..=7).contains(&parts.len()) {
452 return Err(VaultError::Corrupt(format!(
453 "KEY line has {} fields, expected 5 to 7",
454 parts.len()
455 )));
456 }
457 let role = Role::from_str(parts[3])
458 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[3])))?;
459 let created_at: u128 = parts[4]
460 .parse()
461 .map_err(|_| VaultError::Corrupt("invalid key created_at".into()))?;
462 let tenant_id = parts
463 .get(5)
464 .map(|s| s.trim())
465 .filter(|s| !s.is_empty())
466 .map(|s| s.to_string());
467
468 api_keys.push((
469 UserId {
470 tenant: tenant_id,
471 username: parts[0].to_string(),
472 },
473 ApiKey {
474 key: parts[1].to_string(),
475 name: parts[2].to_string(),
476 role,
477 created_at,
478 expires_at: parts
479 .get(6)
480 .map(|s| s.trim())
481 .filter(|s| !s.is_empty())
482 .and_then(|s| s.parse::<u128>().ok()),
483 },
484 ));
485 } else if let Some(rest) = line.strip_prefix("KV:") {
486 let parts: Vec<&str> = rest.splitn(2, '\t').collect();
487 if parts.len() == 2 {
488 if let Ok(bytes) = hex::decode(parts[1]) {
489 if let Ok(value) = String::from_utf8(bytes) {
490 kv.insert(parts[0].to_string(), value);
491 }
492 }
493 }
494 } else {
495 }
497 }
498
499 for (owner, key) in &api_keys {
503 if let Some(user) = users
504 .iter_mut()
505 .find(|u| u.username == owner.username && u.tenant_id == owner.tenant)
506 {
507 user.api_keys.push(key.clone());
508 }
509 }
510
511 Ok(Self {
512 users,
513 api_keys,
514 bootstrapped,
515 master_secret,
516 kv,
517 })
518 }
519}
520
521pub struct Vault {
532 key: SecureKey,
533 salt: [u8; 16],
534}
535
536fn vault_argon2_params() -> Argon2Params {
539 Argon2Params {
540 m_cost: 16 * 1024, t_cost: 3,
542 p: 1,
543 tag_len: 32,
544 }
545}
546
547impl Vault {
548 pub fn has_saved_state(pager: &Pager) -> bool {
550 pager
551 .read_page_no_checksum(VAULT_HEADER_PAGE)
552 .ok()
553 .map(|page| {
554 let content = page.content();
555 content.len() >= VAULT_MAGIC_SIZE && &content[0..VAULT_MAGIC_SIZE] == VAULT_MAGIC
556 })
557 .unwrap_or(false)
558 }
559
560 pub fn open(pager: &Pager) -> Result<Self, VaultError> {
569 let cert_hex =
570 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE").ok_or(VaultError::NoKey)?;
571 Self::with_certificate(pager, &cert_hex)
572 }
573
574 fn salt_for_open(pager: &Pager) -> Result<[u8; 16], VaultError> {
575 match read_vault_salt_from_pager(pager) {
577 Ok(s) => Ok(s),
578 Err(_) => {
579 let mut salt = [0u8; 16];
581 let mut buf = [0u8; 16];
582 os_random::fill_bytes(&mut buf)
583 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
584 salt.copy_from_slice(&buf);
585 Ok(salt)
586 }
587 }
588 }
589
590 pub fn with_certificate(pager: &Pager, certificate_hex: &str) -> Result<Self, VaultError> {
596 let certificate = decode_certificate_hex(certificate_hex)?;
597
598 let key = KeyPair::vault_key_from_certificate(&certificate);
599
600 let salt = Self::salt_for_open(pager)?;
601
602 Ok(Self { key, salt })
603 }
604
605 pub fn from_env(pager: &Pager) -> Result<Self, VaultError> {
609 Self::open(pager)
610 }
611
612 pub fn with_certificate_bytes(pager: &Pager, certificate: &[u8]) -> Result<Self, VaultError> {
617 if certificate.len() != 32 {
618 return Err(VaultError::NoKey);
619 }
620 let key = KeyPair::vault_key_from_certificate(certificate);
621
622 let salt = match read_vault_salt_from_pager(pager) {
623 Ok(s) => s,
624 Err(_) => {
625 let mut s = [0u8; 16];
626 os_random::fill_bytes(&mut s)
627 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
628 s
629 }
630 };
631
632 Ok(Self { key, salt })
633 }
634
635 pub fn seal_logical_export(&self, state: &VaultState) -> Result<String, VaultError> {
641 let plaintext = state.serialize();
642 let mut nonce = [0u8; NONCE_SIZE];
643 os_random::fill_bytes(&mut nonce)
644 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
645
646 let key_bytes: &[u8] = self.key.as_bytes();
647 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
648 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_LOGICAL_EXPORT_AAD, &plaintext);
649
650 Ok(reddb_file::encode_vault_logical_export(
653 &self.salt,
654 &nonce,
655 &ciphertext,
656 ))
657 }
658
659 pub fn unseal_logical_export(blob_hex: &str) -> Result<VaultState, VaultError> {
662 let cert_hex =
663 crate::utils::env_with_file_fallback("REDDB_CERTIFICATE").ok_or(VaultError::NoKey)?;
664 Self::unseal_logical_export_with_certificate(blob_hex, &cert_hex)
665 }
666
667 pub fn unseal_logical_export_with_certificate(
669 blob_hex: &str,
670 certificate_hex: &str,
671 ) -> Result<VaultState, VaultError> {
672 let (_salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
673 let certificate = decode_certificate_hex(certificate_hex)?;
674 let key = KeyPair::vault_key_from_certificate(&certificate);
675 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
676 }
677
678 fn decode_logical_export(
679 blob_hex: &str,
680 ) -> Result<([u8; VAULT_SALT_SIZE], [u8; NONCE_SIZE], Vec<u8>), VaultError> {
681 let env = reddb_file::decode_vault_logical_export(blob_hex)
684 .map_err(|e| VaultError::Corrupt(e.to_string()))?;
685 Ok((env.salt, env.nonce, env.ciphertext))
686 }
687
688 fn decrypt_logical_export(
689 key: &SecureKey,
690 nonce: &[u8; NONCE_SIZE],
691 ciphertext: &[u8],
692 ) -> Result<VaultState, VaultError> {
693 let key_bytes: &[u8] = key.as_bytes();
694 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
695 let plaintext = aes256_gcm_decrypt(key_arr, nonce, VAULT_LOGICAL_EXPORT_AAD, ciphertext)
696 .map_err(|_| VaultError::Decryption)?;
697 VaultState::deserialize(&plaintext)
698 }
699
700 pub fn save(&self, pager: &Pager, state: &VaultState) -> Result<(), VaultError> {
714 let plaintext = state.serialize();
715
716 let mut nonce = [0u8; NONCE_SIZE];
718 os_random::fill_bytes(&mut nonce)
719 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
720
721 let key_bytes: &[u8] = self.key.as_bytes();
722 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
723 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_AAD, &plaintext);
724 let cipher_total = ciphertext.len();
728 let payload_len = (NONCE_SIZE + cipher_total) as u32; let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
736 let overflow = cipher_total.saturating_sub(header_chunk_len);
737 let chain_count = overflow.div_ceil(VAULT_DATA_CIPHER_CAPACITY);
738
739 while pager
753 .page_count()
754 .map_err(|e| VaultError::Pager(e.to_string()))?
755 <= VAULT_HEADER_PAGE
756 {
757 pager
758 .allocate_page(PageType::Vault)
759 .map_err(|e| VaultError::Pager(format!("reserve vault slot: {e}")))?;
760 }
761
762 let old_chain = self.read_existing_chain_ids(pager).unwrap_or_default();
772
773 let mut new_chain: Vec<u32> = Vec::with_capacity(chain_count);
780 for _ in 0..chain_count {
781 let page = pager
782 .allocate_page(PageType::Vault)
783 .map_err(|e| VaultError::Pager(format!("allocate vault data page: {e}")))?;
784 new_chain.push(page.page_id());
785 }
786
787 let mut cursor = header_chunk_len;
794 for i in 0..chain_count {
795 let next_id = if i + 1 < chain_count {
796 new_chain[i + 1]
797 } else {
798 0
799 };
800 let take = (cipher_total - cursor).min(VAULT_DATA_CIPHER_CAPACITY);
801 let frag = &ciphertext[cursor..cursor + take];
802 self.write_data_page(pager, new_chain[i], next_id, frag)?;
803 cursor += take;
804 }
805 debug_assert_eq!(cursor, cipher_total, "ciphertext spill accounting mismatch");
806
807 let first_data_page = new_chain.first().copied().unwrap_or(0);
815 self.write_header_page(
816 pager,
817 &nonce,
818 payload_len,
819 chain_count as u32,
820 first_data_page,
821 &ciphertext[..header_chunk_len],
822 )?;
823
824 pager
829 .flush()
830 .map_err(|e| VaultError::Pager(e.to_string()))?;
831
832 for &id in old_chain.iter() {
837 pager
838 .free_page(id)
839 .map_err(|e| VaultError::Pager(format!("free old vault page {id}: {e}")))?;
840 }
841
842 Ok(())
843 }
844
845 pub fn load(&self, pager: &Pager) -> Result<Option<VaultState>, VaultError> {
849 let page = match pager.read_page_no_checksum(VAULT_HEADER_PAGE) {
851 Ok(p) => p,
852 Err(_) => return Ok(None),
853 };
854
855 let page_content = page.content();
856
857 if page_content.len() < VAULT_HEADER_META_SIZE {
858 return Ok(None);
859 }
860 if &page_content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
861 return Ok(None); }
863
864 let version = page_content[4];
865 if version == VAULT_LEGACY_VERSION {
866 return Err(VaultError::Corrupt(
870 "vault was bootstrapped with the legacy 2-page format \
871 (pre-RedDB v0.3); re-bootstrap with `red bootstrap` to upgrade"
872 .to_string(),
873 ));
874 }
875 if version != VAULT_VERSION {
876 return Err(VaultError::Corrupt(format!(
877 "unsupported vault version: {} (expected {})",
878 version, VAULT_VERSION
879 )));
880 }
881
882 let payload_len = u32::from_le_bytes(
884 page_content[21..25]
885 .try_into()
886 .map_err(|_| VaultError::Corrupt("bad payload length bytes".into()))?,
887 ) as usize;
888
889 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
890 let nonce: [u8; NONCE_SIZE] = page_content[nonce_start..nonce_start + NONCE_SIZE]
891 .try_into()
892 .map_err(|_| VaultError::Corrupt("bad nonce".into()))?;
893
894 let chain_count_off = nonce_start + NONCE_SIZE;
895 let chain_count = u32::from_le_bytes(
896 page_content[chain_count_off..chain_count_off + 4]
897 .try_into()
898 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
899 ) as usize;
900 let first_id_off = chain_count_off + 4;
901 let mut next_id = u32::from_le_bytes(
902 page_content[first_id_off..first_id_off + 4]
903 .try_into()
904 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
905 );
906
907 if payload_len < NONCE_SIZE {
908 return Err(VaultError::Corrupt("payload too short for nonce".into()));
909 }
910 let cipher_total = payload_len - NONCE_SIZE;
911
912 let mut cipher = Vec::with_capacity(cipher_total);
914 let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
915 let header_cipher_start = VAULT_HEADER_META_SIZE;
916 cipher.extend_from_slice(
917 &page_content[header_cipher_start..header_cipher_start + header_chunk_len],
918 );
919
920 let mut hops = 0usize;
922 while cipher.len() < cipher_total {
926 if hops >= chain_count {
927 return Err(VaultError::Corrupt(format!(
928 "vault chain shorter than declared: {} hops, expected {}",
929 hops, chain_count
930 )));
931 }
932 if next_id == 0 {
933 return Err(VaultError::Corrupt(
934 "vault chain ends prematurely (next_id == 0)".to_string(),
935 ));
936 }
937
938 let dp = pager
939 .read_page_no_checksum(next_id)
940 .map_err(|e| VaultError::Pager(format!("vault data page {next_id}: {e}")))?;
941 let dp_content = dp.content();
942 if dp_content.len() < VAULT_DATA_PREFIX_SIZE {
943 return Err(VaultError::Corrupt(format!(
944 "vault data page {next_id} truncated"
945 )));
946 }
947 if &dp_content[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC {
948 return Err(VaultError::Corrupt(format!(
949 "vault data page {next_id} has bad magic"
950 )));
951 }
952 let np = u32::from_le_bytes(
953 dp_content[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
954 .try_into()
955 .map_err(|_| VaultError::Corrupt("bad next_page_id bytes".into()))?,
956 );
957 let take = (cipher_total - cipher.len()).min(VAULT_DATA_CIPHER_CAPACITY);
958 let frag_start = VAULT_DATA_PREFIX_SIZE;
959 cipher.extend_from_slice(&dp_content[frag_start..frag_start + take]);
960
961 next_id = np;
962 hops += 1;
963 }
964
965 if cipher.len() != cipher_total {
966 return Err(VaultError::Corrupt(format!(
967 "vault truncated: expected {} cipher bytes, got {}",
968 cipher_total,
969 cipher.len()
970 )));
971 }
972 if hops != chain_count {
973 return Err(VaultError::Corrupt(format!(
974 "vault chain length mismatch: walked {} pages, header says {}",
975 hops, chain_count
976 )));
977 }
978
979 let key_bytes: &[u8] = self.key.as_bytes();
981 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
982 let plaintext = aes256_gcm_decrypt(key_arr, &nonce, VAULT_AAD, &cipher)
983 .map_err(|_| VaultError::Decryption)?;
984
985 let state = VaultState::deserialize(&plaintext)?;
986 Ok(Some(state))
987 }
988
989 fn read_existing_chain_ids(&self, pager: &Pager) -> Result<Vec<u32>, VaultError> {
996 let header = pager
997 .read_page_no_checksum(VAULT_HEADER_PAGE)
998 .map_err(|e| VaultError::Pager(e.to_string()))?;
999 let content = header.content();
1000 if content.len() < VAULT_HEADER_META_SIZE {
1001 return Ok(Vec::new());
1002 }
1003 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1004 return Ok(Vec::new());
1005 }
1006 let version = content[4];
1007 if version != VAULT_VERSION {
1008 return Ok(Vec::new());
1012 }
1013 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
1014 let chain_count_off = nonce_start + NONCE_SIZE;
1015 let chain_count = u32::from_le_bytes(
1016 content[chain_count_off..chain_count_off + 4]
1017 .try_into()
1018 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1019 ) as usize;
1020 let first_id_off = chain_count_off + 4;
1021 let mut id = u32::from_le_bytes(
1022 content[first_id_off..first_id_off + 4]
1023 .try_into()
1024 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1025 );
1026
1027 let mut out = Vec::with_capacity(chain_count);
1028 let mut hops = 0usize;
1029 while id != 0 && hops < chain_count {
1030 out.push(id);
1031 match pager.read_page_no_checksum(id) {
1034 Ok(dp) => {
1035 let dc = dp.content();
1036 if dc.len() < VAULT_DATA_PREFIX_SIZE
1037 || &dc[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC
1038 {
1039 break;
1040 }
1041 id = u32::from_le_bytes(
1042 dc[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1043 .try_into()
1044 .map_err(|_| VaultError::Corrupt("bad next_id".into()))?,
1045 );
1046 }
1047 Err(_) => break,
1048 }
1049 hops += 1;
1050 }
1051 Ok(out)
1052 }
1053
1054 fn write_header_page(
1058 &self,
1059 pager: &Pager,
1060 nonce: &[u8; NONCE_SIZE],
1061 payload_len: u32,
1062 chain_count: u32,
1063 first_data_page_id: u32,
1064 cipher_fragment: &[u8],
1065 ) -> Result<(), VaultError> {
1066 debug_assert!(cipher_fragment.len() <= VAULT_HEADER_CIPHER_CAPACITY);
1067
1068 let mut page = Page::new(PageType::Vault, VAULT_HEADER_PAGE);
1069 let bytes = page.as_bytes_mut();
1070 let mut off = HEADER_SIZE;
1071
1072 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_MAGIC);
1073 off += VAULT_MAGIC_SIZE;
1074
1075 bytes[off] = VAULT_VERSION;
1076 off += VAULT_VERSION_SIZE;
1077
1078 bytes[off..off + VAULT_SALT_SIZE].copy_from_slice(&self.salt);
1079 off += VAULT_SALT_SIZE;
1080
1081 bytes[off..off + 4].copy_from_slice(&payload_len.to_le_bytes());
1082 off += VAULT_PAYLOAD_LEN_SIZE;
1083
1084 bytes[off..off + NONCE_SIZE].copy_from_slice(nonce);
1085 off += NONCE_SIZE;
1086
1087 bytes[off..off + 4].copy_from_slice(&chain_count.to_le_bytes());
1088 off += VAULT_CHAIN_COUNT_SIZE;
1089
1090 bytes[off..off + 4].copy_from_slice(&first_data_page_id.to_le_bytes());
1091 off += VAULT_FIRST_PAGE_ID_SIZE;
1092
1093 debug_assert_eq!(off, HEADER_SIZE + VAULT_HEADER_META_SIZE);
1094
1095 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1096
1097 pager
1098 .write_page_no_checksum(VAULT_HEADER_PAGE, page)
1099 .map_err(|e| VaultError::Pager(e.to_string()))?;
1100 Ok(())
1101 }
1102
1103 fn write_data_page(
1105 &self,
1106 pager: &Pager,
1107 page_id: u32,
1108 next_page_id: u32,
1109 cipher_fragment: &[u8],
1110 ) -> Result<(), VaultError> {
1111 debug_assert!(cipher_fragment.len() <= VAULT_DATA_CIPHER_CAPACITY);
1112
1113 let mut page = Page::new(PageType::Vault, page_id);
1114 let bytes = page.as_bytes_mut();
1115 let mut off = HEADER_SIZE;
1116
1117 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_DATA_MAGIC);
1118 off += VAULT_MAGIC_SIZE;
1119
1120 bytes[off..off + 4].copy_from_slice(&next_page_id.to_le_bytes());
1121 off += 4;
1122
1123 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1124
1125 pager
1126 .write_page_no_checksum(page_id, page)
1127 .map_err(|e| VaultError::Pager(e.to_string()))?;
1128 Ok(())
1129 }
1130}
1131
1132fn parse_scram_field(field: &str) -> Result<crate::auth::scram::ScramVerifier, VaultError> {
1139 let parts: Vec<&str> = field.split(':').collect();
1140 if parts.len() != 4 {
1141 return Err(VaultError::Corrupt(format!(
1142 "SCRAM verifier has {} segments, expected 4",
1143 parts.len()
1144 )));
1145 }
1146 let salt =
1147 hex::decode(parts[0]).map_err(|_| VaultError::Corrupt("invalid SCRAM salt hex".into()))?;
1148 let iter: u32 = parts[1]
1149 .parse()
1150 .map_err(|_| VaultError::Corrupt("invalid SCRAM iter".into()))?;
1151 if iter < crate::auth::scram::MIN_ITER {
1152 return Err(VaultError::Corrupt(format!(
1153 "SCRAM iter {} below minimum {}",
1154 iter,
1155 crate::auth::scram::MIN_ITER
1156 )));
1157 }
1158 let stored_vec = hex::decode(parts[2])
1159 .map_err(|_| VaultError::Corrupt("invalid SCRAM stored_key hex".into()))?;
1160 let server_vec = hex::decode(parts[3])
1161 .map_err(|_| VaultError::Corrupt("invalid SCRAM server_key hex".into()))?;
1162 let stored_key: [u8; 32] = stored_vec
1163 .try_into()
1164 .map_err(|_| VaultError::Corrupt("SCRAM stored_key must be 32 bytes".into()))?;
1165 let server_key: [u8; 32] = server_vec
1166 .try_into()
1167 .map_err(|_| VaultError::Corrupt("SCRAM server_key must be 32 bytes".into()))?;
1168 Ok(crate::auth::scram::ScramVerifier {
1169 salt,
1170 iter,
1171 stored_key,
1172 server_key,
1173 })
1174}
1175
1176fn read_vault_salt_from_pager(pager: &Pager) -> Result<[u8; 16], VaultError> {
1183 let page = pager
1184 .read_page_no_checksum(VAULT_HEADER_PAGE)
1185 .map_err(|e| VaultError::Pager(format!("vault page read: {e}")))?;
1186
1187 let content = page.content();
1188 if content.len() < VAULT_HEADER_PREAMBLE_SIZE {
1189 return Err(VaultError::Corrupt("vault page too short".into()));
1190 }
1191 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1192 return Err(VaultError::Corrupt("bad magic bytes".into()));
1193 }
1194
1195 let mut salt = [0u8; VAULT_SALT_SIZE];
1196 salt.copy_from_slice(&content[5..21]);
1197 Ok(salt)
1198}
1199
1200#[cfg(test)]
1205mod tests {
1206 use super::*;
1207 use crate::auth::{now_ms, ApiKey, Role, User};
1208 use crate::storage::engine::pager::PagerConfig;
1209
1210 fn sample_state() -> VaultState {
1211 let now = now_ms();
1212 VaultState {
1213 users: vec![
1214 User {
1215 username: "alice".into(),
1216 tenant_id: None,
1217 password_hash: "argon2id$aabbccdd$eeff0011".into(),
1218 scram_verifier: None,
1219 role: Role::Admin,
1220 api_keys: vec![ApiKey {
1221 key: "rk_abc123".into(),
1222 name: "ci-token".into(),
1223 role: Role::Write,
1224 created_at: now,
1225 expires_at: None,
1226 }],
1227 created_at: now,
1228 updated_at: now,
1229 enabled: true,
1230 },
1231 User {
1232 username: "bob".into(),
1233 tenant_id: None,
1234 password_hash: "argon2id$11223344$55667788".into(),
1235 scram_verifier: None,
1236 role: Role::Read,
1237 api_keys: vec![],
1238 created_at: now,
1239 updated_at: now,
1240 enabled: false,
1241 },
1242 ],
1243 api_keys: vec![(
1244 UserId::platform("alice"),
1245 ApiKey {
1246 key: "rk_abc123".into(),
1247 name: "ci-token".into(),
1248 role: Role::Write,
1249 created_at: now,
1250 expires_at: None,
1251 },
1252 )],
1253 bootstrapped: true,
1254 master_secret: None,
1255 kv: std::collections::HashMap::new(),
1256 }
1257 }
1258
1259 fn temp_pager() -> (Pager, std::path::PathBuf) {
1261 use std::sync::atomic::{AtomicU64, Ordering};
1262 static COUNTER: AtomicU64 = AtomicU64::new(0);
1263 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1264 let tmp_dir =
1265 std::env::temp_dir().join(format!("reddb_vault_test_{}_{}", std::process::id(), id));
1266 std::fs::create_dir_all(&tmp_dir).unwrap();
1267 let db_path = tmp_dir.join("test.rdb");
1268 let pager = Pager::open(&db_path, PagerConfig::default()).unwrap();
1269 (pager, tmp_dir)
1270 }
1271
1272 fn env_lock() -> &'static std::sync::Mutex<()> {
1273 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
1274 LOCK.get_or_init(|| std::sync::Mutex::new(()))
1275 }
1276
1277 fn restore_env_var(name: &str, value: Option<std::ffi::OsString>) {
1278 unsafe {
1279 match value {
1280 Some(value) => std::env::set_var(name, value),
1281 None => std::env::remove_var(name),
1282 }
1283 }
1284 }
1285
1286 #[test]
1287 fn test_vault_state_serialize_deserialize_roundtrip() {
1288 let state = sample_state();
1289 let serialized = state.serialize();
1290 let text = std::str::from_utf8(&serialized).unwrap();
1291
1292 assert!(text.contains("SEALED:true"));
1294 assert!(text.contains("USER:alice\t"));
1295 assert!(text.contains("USER:bob\t"));
1296 assert!(text.contains("KEY:alice\trk_abc123\t"));
1297
1298 let restored = VaultState::deserialize(&serialized).unwrap();
1300 assert!(restored.bootstrapped);
1301 assert_eq!(restored.users.len(), 2);
1302
1303 let alice = restored
1304 .users
1305 .iter()
1306 .find(|u| u.username == "alice")
1307 .unwrap();
1308 assert_eq!(alice.role, Role::Admin);
1309 assert!(alice.enabled);
1310 assert_eq!(alice.password_hash, "argon2id$aabbccdd$eeff0011");
1311 assert_eq!(alice.api_keys.len(), 1);
1312 assert_eq!(alice.api_keys[0].key, "rk_abc123");
1313 assert_eq!(alice.api_keys[0].name, "ci-token");
1314 assert_eq!(alice.api_keys[0].role, Role::Write);
1315
1316 let bob = restored.users.iter().find(|u| u.username == "bob").unwrap();
1317 assert_eq!(bob.role, Role::Read);
1318 assert!(!bob.enabled);
1319 assert!(bob.api_keys.is_empty());
1320
1321 assert_eq!(restored.api_keys.len(), 1);
1322 assert_eq!(restored.api_keys[0].0.username, "alice");
1323 assert!(restored.api_keys[0].0.tenant.is_none());
1324 }
1325
1326 #[test]
1327 fn test_vault_state_empty() {
1328 let state = VaultState {
1329 users: vec![],
1330 api_keys: vec![],
1331 bootstrapped: false,
1332 master_secret: None,
1333 kv: std::collections::HashMap::new(),
1334 };
1335 let serialized = state.serialize();
1336 let restored = VaultState::deserialize(&serialized).unwrap();
1337 assert!(!restored.bootstrapped);
1338 assert!(restored.users.is_empty());
1339 assert!(restored.api_keys.is_empty());
1340 }
1341
1342 #[test]
1343 fn test_vault_state_deserialize_invalid_utf8() {
1344 let bad_data = vec![0xFF, 0xFE, 0xFD];
1345 let result = VaultState::deserialize(&bad_data);
1346 assert!(result.is_err());
1347 }
1348
1349 #[test]
1350 fn test_vault_state_deserialize_bad_user_line() {
1351 let bad = b"USER:only_two\tfields\n";
1352 let result = VaultState::deserialize(bad);
1353 assert!(result.is_err());
1354 }
1355
1356 #[test]
1357 fn test_vault_state_deserialize_bad_key_line() {
1358 let bad = b"KEY:too\tfew\n";
1359 let result = VaultState::deserialize(bad);
1360 assert!(result.is_err());
1361 }
1362
1363 #[test]
1364 fn test_vault_state_deserialize_unknown_line_skipped() {
1365 let data = b"SEALED:false\nFUTURE:some_data\n";
1366 let result = VaultState::deserialize(data).unwrap();
1367 assert!(!result.bootstrapped);
1368 }
1369
1370 #[test]
1371 fn test_vault_pager_save_load_roundtrip() {
1372 let (pager, tmp_dir) = temp_pager();
1373
1374 let kp = KeyPair::generate();
1375 let certificate = kp.certificate_hex();
1376 let vault = Vault::with_certificate(&pager, &certificate).unwrap();
1377
1378 let loaded = vault.load(&pager).unwrap();
1380 assert!(loaded.is_none());
1381
1382 let state = sample_state();
1384 vault.save(&pager, &state).unwrap();
1385
1386 let restored = vault.load(&pager).unwrap().unwrap();
1388 assert!(restored.bootstrapped);
1389 assert_eq!(restored.users.len(), 2);
1390 assert_eq!(restored.api_keys.len(), 1);
1391
1392 let alice = restored
1393 .users
1394 .iter()
1395 .find(|u| u.username == "alice")
1396 .unwrap();
1397 assert_eq!(alice.role, Role::Admin);
1398 assert_eq!(alice.api_keys.len(), 1);
1399
1400 let vault2 = Vault::with_certificate(&pager, &certificate).unwrap();
1402 let restored2 = vault2.load(&pager).unwrap().unwrap();
1403 assert!(restored2.bootstrapped);
1404 assert_eq!(restored2.users.len(), 2);
1405
1406 drop(pager);
1408 let _ = std::fs::remove_dir_all(&tmp_dir);
1409 }
1410
1411 #[test]
1412 fn test_vault_wrong_key_fails_decryption() {
1413 let (pager, tmp_dir) = temp_pager();
1414
1415 let kp = KeyPair::generate();
1417 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1418 let state = VaultState {
1419 users: vec![],
1420 api_keys: vec![],
1421 bootstrapped: true,
1422 master_secret: None,
1423 kv: std::collections::HashMap::new(),
1424 };
1425 vault.save(&pager, &state).unwrap();
1426
1427 let wrong = KeyPair::generate();
1429 let vault2 = Vault::with_certificate_bytes(&pager, &wrong.certificate).unwrap();
1430 let result = vault2.load(&pager);
1431
1432 assert!(result.is_err());
1433
1434 drop(pager);
1436 let _ = std::fs::remove_dir_all(&tmp_dir);
1437 }
1438
1439 #[test]
1440 fn test_vault_no_key_error() {
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 unsafe {
1447 std::env::remove_var("REDDB_CERTIFICATE");
1448 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1449 }
1450 let result = Vault::open(&pager);
1451 restore_env_var("REDDB_CERTIFICATE", old);
1452 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1453 assert!(matches!(result, Err(VaultError::NoKey)));
1454
1455 drop(pager);
1457 let _ = std::fs::remove_dir_all(&tmp_dir);
1458 }
1459
1460 #[test]
1461 fn test_vault_open_uses_certificate_env() {
1462 let (pager, tmp_dir) = temp_pager();
1463
1464 let _guard = env_lock().lock().unwrap();
1465 let old = std::env::var_os("REDDB_CERTIFICATE");
1466 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1467 let kp = KeyPair::generate();
1468 unsafe {
1469 std::env::set_var("REDDB_CERTIFICATE", kp.certificate_hex());
1470 std::env::remove_var("REDDB_CERTIFICATE_FILE");
1471 }
1472
1473 let vault = Vault::open(&pager).unwrap();
1475 let state = VaultState {
1476 users: vec![],
1477 api_keys: vec![],
1478 bootstrapped: false,
1479 master_secret: None,
1480 kv: std::collections::HashMap::new(),
1481 };
1482 vault.save(&pager, &state).unwrap();
1483
1484 let vault2 = Vault::open(&pager).unwrap();
1486 let loaded = vault2.load(&pager).unwrap().unwrap();
1487 assert!(!loaded.bootstrapped);
1488
1489 restore_env_var("REDDB_CERTIFICATE", old);
1490 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1491 drop(pager);
1492 let _ = std::fs::remove_dir_all(&tmp_dir);
1493 }
1494
1495 #[test]
1496 fn test_vault_open_uses_certificate_file_env() {
1497 let (pager, tmp_dir) = temp_pager();
1498
1499 let _guard = env_lock().lock().unwrap();
1500 let old = std::env::var_os("REDDB_CERTIFICATE");
1501 let old_file = std::env::var_os("REDDB_CERTIFICATE_FILE");
1502 let kp = KeyPair::generate();
1503 let cert_path = tmp_dir.join("certificate");
1504 std::fs::write(&cert_path, format!("{}\n", kp.certificate_hex())).unwrap();
1505 unsafe {
1506 std::env::remove_var("REDDB_CERTIFICATE");
1507 std::env::set_var("REDDB_CERTIFICATE_FILE", &cert_path);
1508 }
1509
1510 let vault = Vault::open(&pager).unwrap();
1511 let state = VaultState {
1512 users: vec![],
1513 api_keys: vec![],
1514 bootstrapped: false,
1515 master_secret: None,
1516 kv: std::collections::HashMap::new(),
1517 };
1518 vault.save(&pager, &state).unwrap();
1519
1520 let vault2 = Vault::open(&pager).unwrap();
1521 let loaded = vault2.load(&pager).unwrap().unwrap();
1522 assert!(!loaded.bootstrapped);
1523
1524 restore_env_var("REDDB_CERTIFICATE", old);
1525 restore_env_var("REDDB_CERTIFICATE_FILE", old_file);
1526 drop(pager);
1527 let _ = std::fs::remove_dir_all(&tmp_dir);
1528 }
1529
1530 #[test]
1535 fn test_keypair_generate_deterministic_certificate() {
1536 let kp = KeyPair::generate();
1537 assert_eq!(kp.master_secret.len(), 32);
1538 assert_eq!(kp.certificate.len(), 32);
1539
1540 let kp2 = KeyPair::from_master_secret(kp.master_secret.clone());
1542 assert_eq!(kp.certificate, kp2.certificate);
1543 }
1544
1545 #[test]
1546 fn test_keypair_sign_verify() {
1547 let kp = KeyPair::generate();
1548 let data = b"session:abc123";
1549 let sig = kp.sign(data);
1550 assert!(kp.verify(data, &sig));
1551
1552 assert!(!kp.verify(b"session:wrong", &sig));
1554
1555 let mut bad_sig = sig.clone();
1557 bad_sig[0] ^= 0xFF;
1558 assert!(!kp.verify(data, &bad_sig));
1559 }
1560
1561 #[test]
1562 fn test_keypair_certificate_hex() {
1563 let kp = KeyPair::generate();
1564 let hex_str = kp.certificate_hex();
1565 assert_eq!(hex_str.len(), 64); let decoded = hex::decode(&hex_str).unwrap();
1567 assert_eq!(decoded, kp.certificate);
1568 }
1569
1570 #[test]
1571 fn test_vault_certificate_seal_roundtrip() {
1572 let (pager, tmp_dir) = temp_pager();
1573
1574 let kp = KeyPair::generate();
1576 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1577
1578 let state = VaultState {
1580 users: vec![],
1581 api_keys: vec![],
1582 bootstrapped: true,
1583 master_secret: Some(kp.master_secret.clone()),
1584 kv: std::collections::HashMap::new(),
1585 };
1586 vault.save(&pager, &state).unwrap();
1587
1588 let vault2 = Vault::with_certificate(&pager, &kp.certificate_hex()).unwrap();
1590 let loaded = vault2.load(&pager).unwrap().unwrap();
1591 assert!(loaded.bootstrapped);
1592 assert_eq!(loaded.master_secret, Some(kp.master_secret.clone()));
1593
1594 let kp2 = KeyPair::from_master_secret(loaded.master_secret.unwrap());
1596 assert_eq!(kp.certificate, kp2.certificate);
1597
1598 drop(pager);
1599 let _ = std::fs::remove_dir_all(&tmp_dir);
1600 }
1601
1602 #[test]
1603 fn test_vault_certificate_wrong_cert_fails() {
1604 let (pager, tmp_dir) = temp_pager();
1605
1606 let kp = KeyPair::generate();
1608 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1609 let state = VaultState {
1610 users: vec![],
1611 api_keys: vec![],
1612 bootstrapped: true,
1613 master_secret: Some(kp.master_secret.clone()),
1614 kv: std::collections::HashMap::new(),
1615 };
1616 vault.save(&pager, &state).unwrap();
1617
1618 let kp2 = KeyPair::generate();
1620 let vault2 = Vault::with_certificate_bytes(&pager, &kp2.certificate).unwrap();
1621 let result = vault2.load(&pager);
1622 assert!(result.is_err());
1623
1624 drop(pager);
1625 let _ = std::fs::remove_dir_all(&tmp_dir);
1626 }
1627
1628 #[test]
1629 fn test_vault_state_master_secret_serialization() {
1630 let secret = vec![0xAA; 32];
1631 let state = VaultState {
1632 users: vec![],
1633 api_keys: vec![],
1634 bootstrapped: true,
1635 master_secret: Some(secret.clone()),
1636 kv: std::collections::HashMap::new(),
1637 };
1638 let serialized = state.serialize();
1639 let text = std::str::from_utf8(&serialized).unwrap();
1640 assert!(text.contains("MASTER_SECRET:"));
1641 assert!(text.contains(&hex::encode(&secret)));
1642
1643 let restored = VaultState::deserialize(&serialized).unwrap();
1644 assert_eq!(restored.master_secret, Some(secret));
1645 assert!(restored.bootstrapped);
1646 }
1647
1648 #[test]
1649 fn test_vault_state_no_master_secret_backward_compat() {
1650 let data = b"SEALED:true\n";
1652 let restored = VaultState::deserialize(data).unwrap();
1653 assert!(restored.master_secret.is_none());
1654 assert!(restored.bootstrapped);
1655 }
1656
1657 #[test]
1658 fn test_vault_state_scram_verifier_roundtrip() {
1659 use crate::auth::scram::ScramVerifier;
1660
1661 let verifier = ScramVerifier::from_password(
1662 "hunter2",
1663 b"reddb-vault-test-salt".to_vec(),
1664 crate::auth::scram::DEFAULT_ITER,
1665 );
1666
1667 let now = now_ms();
1668 let state = VaultState {
1669 users: vec![User {
1670 username: "carol".into(),
1671 tenant_id: None,
1672 password_hash: "argon2id$abc$def".into(),
1673 scram_verifier: Some(verifier.clone()),
1674 role: Role::Admin,
1675 api_keys: vec![],
1676 created_at: now,
1677 updated_at: now,
1678 enabled: true,
1679 }],
1680 api_keys: vec![],
1681 bootstrapped: true,
1682 master_secret: None,
1683 kv: std::collections::HashMap::new(),
1684 };
1685
1686 let bytes = state.serialize();
1687 let restored = VaultState::deserialize(&bytes).unwrap();
1688 let carol = restored
1689 .users
1690 .iter()
1691 .find(|u| u.username == "carol")
1692 .unwrap();
1693 let v = carol.scram_verifier.as_ref().expect("verifier round-trips");
1694 assert_eq!(v.salt, verifier.salt);
1695 assert_eq!(v.iter, verifier.iter);
1696 assert_eq!(v.stored_key, verifier.stored_key);
1697 assert_eq!(v.server_key, verifier.server_key);
1698 }
1699
1700 #[test]
1701 fn test_vault_state_pre_tenant_user_line_still_parses() {
1702 let now = now_ms();
1706 let line = format!(
1707 "USER:dave\targon2id$x$y\tread\ttrue\t{}\t{}\t\nSEALED:false\n",
1708 now, now
1709 );
1710 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1711 let dave = restored
1712 .users
1713 .iter()
1714 .find(|u| u.username == "dave")
1715 .unwrap();
1716 assert!(dave.scram_verifier.is_none());
1717 assert!(dave.tenant_id.is_none());
1718 }
1719
1720 #[test]
1721 fn test_vault_state_tenant_user_line_still_parses() {
1722 let now = now_ms();
1723 let line = format!(
1724 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\nSEALED:false\n",
1725 now, now
1726 );
1727 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1728 let erin = restored
1729 .users
1730 .iter()
1731 .find(|u| u.username == "erin")
1732 .unwrap();
1733 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1734 }
1735
1736 #[test]
1737 fn test_vault_state_legacy_user_line_with_extra_ownership_field_is_ignored() {
1738 let now = now_ms();
1739 let line = format!(
1740 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\ttrue\nSEALED:false\n",
1741 now, now
1742 );
1743 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1744 let erin = restored
1745 .users
1746 .iter()
1747 .find(|u| u.username == "erin")
1748 .unwrap();
1749 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1750 }
1751
1752 #[test]
1753 fn test_vault_state_user_line_with_tenant_roundtrip() {
1754 let now = now_ms();
1755 let state = VaultState {
1756 users: vec![User {
1757 username: "alice".into(),
1758 tenant_id: Some("acme".into()),
1759 password_hash: "argon2id$x$y".into(),
1760 scram_verifier: None,
1761 role: Role::Write,
1762 api_keys: vec![],
1763 created_at: now,
1764 updated_at: now,
1765 enabled: true,
1766 }],
1767 api_keys: vec![],
1768 bootstrapped: true,
1769 master_secret: None,
1770 kv: std::collections::HashMap::new(),
1771 };
1772 let bytes = state.serialize();
1773 let text = std::str::from_utf8(&bytes).unwrap();
1774 assert!(text.contains("\tacme\n"));
1776
1777 let restored = VaultState::deserialize(&bytes).unwrap();
1778 let alice = restored
1779 .users
1780 .iter()
1781 .find(|u| u.username == "alice")
1782 .unwrap();
1783 assert_eq!(alice.tenant_id.as_deref(), Some("acme"));
1784 }
1785
1786 #[test]
1787 fn test_vault_state_key_line_with_tenant_reattaches_correctly() {
1788 let now = now_ms();
1791 let state = VaultState {
1792 users: vec![
1793 User {
1794 username: "alice".into(),
1795 tenant_id: Some("acme".into()),
1796 password_hash: "argon2id$x$y".into(),
1797 scram_verifier: None,
1798 role: Role::Write,
1799 api_keys: vec![],
1800 created_at: now,
1801 updated_at: now,
1802 enabled: true,
1803 },
1804 User {
1805 username: "alice".into(),
1806 tenant_id: Some("globex".into()),
1807 password_hash: "argon2id$a$b".into(),
1808 scram_verifier: None,
1809 role: Role::Read,
1810 api_keys: vec![],
1811 created_at: now,
1812 updated_at: now,
1813 enabled: true,
1814 },
1815 ],
1816 api_keys: vec![
1817 (
1818 UserId::scoped("acme", "alice"),
1819 ApiKey {
1820 key: "rk_acme_key".into(),
1821 name: "deploy".into(),
1822 role: Role::Write,
1823 created_at: now,
1824 expires_at: None,
1825 },
1826 ),
1827 (
1828 UserId::scoped("globex", "alice"),
1829 ApiKey {
1830 key: "rk_globex_key".into(),
1831 name: "ci".into(),
1832 role: Role::Read,
1833 created_at: now,
1834 expires_at: None,
1835 },
1836 ),
1837 ],
1838 bootstrapped: true,
1839 master_secret: None,
1840 kv: std::collections::HashMap::new(),
1841 };
1842 let bytes = state.serialize();
1843 let restored = VaultState::deserialize(&bytes).unwrap();
1844 assert_eq!(restored.api_keys.len(), 2);
1847 let acme_key = restored
1848 .api_keys
1849 .iter()
1850 .find(|(o, _)| o.tenant.as_deref() == Some("acme"))
1851 .unwrap();
1852 assert_eq!(acme_key.1.key, "rk_acme_key");
1853 let globex_key = restored
1854 .api_keys
1855 .iter()
1856 .find(|(o, _)| o.tenant.as_deref() == Some("globex"))
1857 .unwrap();
1858 assert_eq!(globex_key.1.key, "rk_globex_key");
1859 }
1860
1861 #[test]
1862 fn test_vault_state_scram_iter_below_min_rejected() {
1863 let now = now_ms();
1864 let stored_hex = "00".repeat(32);
1868 let server_hex = "11".repeat(32);
1869 let line = format!(
1870 "USER:eve\targon2id$x$y\tread\ttrue\t{}\t{}\tdeadbeef:1024:{}:{}\n",
1871 now, now, stored_hex, server_hex
1872 );
1873 match VaultState::deserialize(line.as_bytes()) {
1874 Err(VaultError::Corrupt(msg)) => assert!(msg.contains("below minimum")),
1875 Err(other) => panic!("expected Corrupt iter-floor error, got {other:?}"),
1876 Ok(_) => panic!("expected Corrupt iter-floor error, got Ok"),
1877 }
1878 }
1879
1880 #[test]
1881 fn test_constant_time_eq_function() {
1882 assert!(constant_time_eq(b"hello", b"hello"));
1883 assert!(!constant_time_eq(b"hello", b"world"));
1884 assert!(!constant_time_eq(b"short", b"longer"));
1885 assert!(constant_time_eq(b"", b""));
1886 }
1887}