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