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 key: set REDDB_CERTIFICATE (or REDDB_VAULT_KEY) or provide a certificate"
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
261#[derive(Debug, Default)]
269pub struct VaultState {
270 pub users: Vec<User>,
271 pub api_keys: Vec<(UserId, ApiKey)>,
275 pub bootstrapped: bool,
276 pub master_secret: Option<Vec<u8>>,
280 pub kv: std::collections::HashMap<String, String>,
284}
285
286impl VaultState {
287 pub fn serialize(&self) -> Vec<u8> {
289 let mut out = String::new();
290
291 if let Some(ref secret) = self.master_secret {
293 out.push_str(&format!("MASTER_SECRET:{}\n", hex::encode(secret)));
294 }
295
296 out.push_str(&format!("SEALED:{}\n", self.bootstrapped));
298
299 for user in &self.users {
312 let scram_field = match &user.scram_verifier {
313 Some(v) => format!(
314 "{}:{}:{}:{}",
315 hex::encode(&v.salt),
316 v.iter,
317 hex::encode(v.stored_key),
318 hex::encode(v.server_key),
319 ),
320 None => String::new(),
321 };
322 let tenant_field = user.tenant_id.clone().unwrap_or_default();
323 out.push_str(&format!(
324 "USER:{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
325 user.username,
326 user.password_hash,
327 user.role.as_str(),
328 user.enabled,
329 user.created_at,
330 user.updated_at,
331 scram_field,
332 tenant_field,
333 user.system_owned,
334 ));
335 }
336
337 for (owner, key) in &self.api_keys {
341 let tenant_field = owner.tenant.clone().unwrap_or_default();
342 out.push_str(&format!(
343 "KEY:{}\t{}\t{}\t{}\t{}\t{}\n",
344 owner.username,
345 key.key,
346 key.name,
347 key.role.as_str(),
348 key.created_at,
349 tenant_field,
350 ));
351 }
352
353 for (k, v) in &self.kv {
355 out.push_str(&format!("KV:{}\t{}\n", k, hex::encode(v.as_bytes())));
356 }
357
358 out.into_bytes()
359 }
360
361 pub fn deserialize(data: &[u8]) -> Result<Self, VaultError> {
363 let text = std::str::from_utf8(data)
364 .map_err(|_| VaultError::Corrupt("payload is not valid UTF-8".into()))?;
365
366 let mut users = Vec::new();
367 let mut api_keys: Vec<(UserId, ApiKey)> = Vec::new();
368 let mut bootstrapped = false;
369 let mut master_secret: Option<Vec<u8>> = None;
370 let mut kv: std::collections::HashMap<String, String> = std::collections::HashMap::new();
371
372 for line in text.lines() {
373 if line.is_empty() {
374 continue;
375 }
376
377 if let Some(rest) = line.strip_prefix("MASTER_SECRET:") {
378 master_secret = Some(
379 hex::decode(rest)
380 .map_err(|_| VaultError::Corrupt("invalid MASTER_SECRET hex".into()))?,
381 );
382 } else if let Some(rest) = line.strip_prefix("SEALED:") {
383 bootstrapped = rest == "true";
384 } else if let Some(rest) = line.strip_prefix("USER:") {
385 let parts: Vec<&str> = rest.split('\t').collect();
386 if !(7..=9).contains(&parts.len()) {
389 return Err(VaultError::Corrupt(format!(
390 "USER line has {} fields, expected 7, 8, or 9",
391 parts.len()
392 )));
393 }
394 let role = Role::from_str(parts[2])
395 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[2])))?;
396 let enabled = parts[3] == "true";
397 let created_at: u128 = parts[4]
398 .parse()
399 .map_err(|_| VaultError::Corrupt("invalid created_at".into()))?;
400 let updated_at: u128 = parts[5]
401 .parse()
402 .map_err(|_| VaultError::Corrupt("invalid updated_at".into()))?;
403 let scram_verifier = parts
404 .get(6)
405 .map(|s| s.trim())
406 .filter(|s| !s.is_empty())
407 .map(parse_scram_field)
408 .transpose()?;
409 let tenant_id = parts
410 .get(7)
411 .map(|s| s.trim())
412 .filter(|s| !s.is_empty())
413 .map(|s| s.to_string());
414 let system_owned = parts.get(8).map(|s| s.trim() == "true").unwrap_or(false);
415
416 users.push(User {
417 username: parts[0].to_string(),
418 tenant_id,
419 password_hash: parts[1].to_string(),
420 scram_verifier,
421 role,
422 api_keys: Vec::new(), created_at,
424 updated_at,
425 enabled,
426 system_owned,
427 });
428 } else if let Some(rest) = line.strip_prefix("KEY:") {
429 let parts: Vec<&str> = rest.split('\t').collect();
430 if parts.len() != 5 && parts.len() != 6 {
432 return Err(VaultError::Corrupt(format!(
433 "KEY line has {} fields, expected 5 or 6",
434 parts.len()
435 )));
436 }
437 let role = Role::from_str(parts[3])
438 .ok_or_else(|| VaultError::Corrupt(format!("unknown role: {}", parts[3])))?;
439 let created_at: u128 = parts[4]
440 .parse()
441 .map_err(|_| VaultError::Corrupt("invalid key created_at".into()))?;
442 let tenant_id = parts
443 .get(5)
444 .map(|s| s.trim())
445 .filter(|s| !s.is_empty())
446 .map(|s| s.to_string());
447
448 api_keys.push((
449 UserId {
450 tenant: tenant_id,
451 username: parts[0].to_string(),
452 },
453 ApiKey {
454 key: parts[1].to_string(),
455 name: parts[2].to_string(),
456 role,
457 created_at,
458 },
459 ));
460 } else if let Some(rest) = line.strip_prefix("KV:") {
461 let parts: Vec<&str> = rest.splitn(2, '\t').collect();
462 if parts.len() == 2 {
463 if let Ok(bytes) = hex::decode(parts[1]) {
464 if let Ok(value) = String::from_utf8(bytes) {
465 kv.insert(parts[0].to_string(), value);
466 }
467 }
468 }
469 } else {
470 }
472 }
473
474 for (owner, key) in &api_keys {
478 if let Some(user) = users
479 .iter_mut()
480 .find(|u| u.username == owner.username && u.tenant_id == owner.tenant)
481 {
482 user.api_keys.push(key.clone());
483 }
484 }
485
486 Ok(Self {
487 users,
488 api_keys,
489 bootstrapped,
490 master_secret,
491 kv,
492 })
493 }
494}
495
496pub struct Vault {
507 key: SecureKey,
508 salt: [u8; 16],
509}
510
511fn vault_argon2_params() -> Argon2Params {
514 Argon2Params {
515 m_cost: 16 * 1024, t_cost: 3,
517 p: 1,
518 tag_len: 32,
519 }
520}
521
522impl Vault {
523 pub fn has_saved_state(pager: &Pager) -> bool {
525 pager
526 .read_page_no_checksum(VAULT_HEADER_PAGE)
527 .ok()
528 .map(|page| {
529 let content = page.content();
530 content.len() >= VAULT_MAGIC_SIZE && &content[0..VAULT_MAGIC_SIZE] == VAULT_MAGIC
531 })
532 .unwrap_or(false)
533 }
534
535 pub fn open(pager: &Pager, passphrase: Option<&str>) -> Result<Self, VaultError> {
544 if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
546 return Self::with_certificate(pager, &cert_hex);
547 }
548
549 let passphrase_str = std::env::var("REDDB_VAULT_KEY")
551 .ok()
552 .or_else(|| passphrase.map(|s| s.to_string()))
553 .ok_or(VaultError::NoKey)?;
554
555 let salt = match read_vault_salt_from_pager(pager) {
557 Ok(s) => s,
558 Err(_) => {
559 let mut salt = [0u8; 16];
561 let mut buf = [0u8; 16];
562 os_random::fill_bytes(&mut buf)
563 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
564 salt.copy_from_slice(&buf);
565 salt
566 }
567 };
568
569 let key_bytes = derive_key(passphrase_str.as_bytes(), &salt, &vault_argon2_params());
570 let key = SecureKey::new(&key_bytes);
571
572 Ok(Self { key, salt })
573 }
574
575 pub fn with_certificate(pager: &Pager, certificate_hex: &str) -> Result<Self, VaultError> {
581 let certificate = hex::decode(certificate_hex).map_err(|_| VaultError::NoKey)?;
582
583 let key = KeyPair::vault_key_from_certificate(&certificate);
584
585 let salt = match read_vault_salt_from_pager(pager) {
587 Ok(s) => s,
588 Err(_) => {
589 let mut s = [0u8; 16];
591 os_random::fill_bytes(&mut s)
592 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
593 s
594 }
595 };
596
597 Ok(Self { key, salt })
598 }
599
600 pub fn from_env(pager: &Pager) -> Result<Self, VaultError> {
604 if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
605 return Self::with_certificate(pager, &cert_hex);
606 }
607 if let Ok(passphrase) = std::env::var("REDDB_VAULT_KEY") {
608 return Self::open_with_passphrase(pager, &passphrase);
609 }
610 Err(VaultError::NoKey)
611 }
612
613 fn open_with_passphrase(pager: &Pager, passphrase: &str) -> Result<Self, VaultError> {
615 let salt = match read_vault_salt_from_pager(pager) {
616 Ok(s) => s,
617 Err(_) => {
618 let mut s = [0u8; 16];
619 os_random::fill_bytes(&mut s)
620 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
621 s
622 }
623 };
624
625 let key_bytes = derive_key(passphrase.as_bytes(), &salt, &vault_argon2_params());
626 let key = SecureKey::new(&key_bytes);
627 Ok(Self { key, salt })
628 }
629
630 pub fn with_certificate_bytes(pager: &Pager, certificate: &[u8]) -> Result<Self, VaultError> {
635 let key = KeyPair::vault_key_from_certificate(certificate);
636
637 let salt = match read_vault_salt_from_pager(pager) {
638 Ok(s) => s,
639 Err(_) => {
640 let mut s = [0u8; 16];
641 os_random::fill_bytes(&mut s)
642 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
643 s
644 }
645 };
646
647 Ok(Self { key, salt })
648 }
649
650 pub fn seal_logical_export(&self, state: &VaultState) -> Result<String, VaultError> {
656 let plaintext = state.serialize();
657 let mut nonce = [0u8; NONCE_SIZE];
658 os_random::fill_bytes(&mut nonce)
659 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
660
661 let key_bytes: &[u8] = self.key.as_bytes();
662 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
663 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_LOGICAL_EXPORT_AAD, &plaintext);
664
665 Ok(reddb_file::encode_vault_logical_export(
668 &self.salt,
669 &nonce,
670 &ciphertext,
671 ))
672 }
673
674 pub fn unseal_logical_export(
678 blob_hex: &str,
679 passphrase: Option<&str>,
680 ) -> Result<VaultState, VaultError> {
681 let (salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
682
683 let key = if let Ok(cert_hex) = std::env::var("REDDB_CERTIFICATE") {
684 let certificate = hex::decode(cert_hex).map_err(|_| VaultError::NoKey)?;
685 KeyPair::vault_key_from_certificate(&certificate)
686 } else {
687 let passphrase_str = std::env::var("REDDB_VAULT_KEY")
688 .ok()
689 .or_else(|| passphrase.map(|s| s.to_string()))
690 .ok_or(VaultError::NoKey)?;
691 let key_bytes = derive_key(passphrase_str.as_bytes(), &salt, &vault_argon2_params());
692 SecureKey::new(&key_bytes)
693 };
694
695 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
696 }
697
698 pub fn unseal_logical_export_with_passphrase(
700 blob_hex: &str,
701 passphrase: &str,
702 ) -> Result<VaultState, VaultError> {
703 let (salt, nonce, ciphertext) = Self::decode_logical_export(blob_hex)?;
704 let key_bytes = derive_key(passphrase.as_bytes(), &salt, &vault_argon2_params());
705 let key = SecureKey::new(&key_bytes);
706 Self::decrypt_logical_export(&key, &nonce, &ciphertext)
707 }
708
709 fn decode_logical_export(
710 blob_hex: &str,
711 ) -> Result<([u8; VAULT_SALT_SIZE], [u8; NONCE_SIZE], Vec<u8>), VaultError> {
712 let env = reddb_file::decode_vault_logical_export(blob_hex)
715 .map_err(|e| VaultError::Corrupt(e.to_string()))?;
716 Ok((env.salt, env.nonce, env.ciphertext))
717 }
718
719 fn decrypt_logical_export(
720 key: &SecureKey,
721 nonce: &[u8; NONCE_SIZE],
722 ciphertext: &[u8],
723 ) -> Result<VaultState, VaultError> {
724 let key_bytes: &[u8] = key.as_bytes();
725 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
726 let plaintext = aes256_gcm_decrypt(key_arr, nonce, VAULT_LOGICAL_EXPORT_AAD, ciphertext)
727 .map_err(|_| VaultError::Decryption)?;
728 VaultState::deserialize(&plaintext)
729 }
730
731 pub fn save(&self, pager: &Pager, state: &VaultState) -> Result<(), VaultError> {
745 let plaintext = state.serialize();
746
747 let mut nonce = [0u8; NONCE_SIZE];
749 os_random::fill_bytes(&mut nonce)
750 .map_err(|e| VaultError::Corrupt(format!("CSPRNG failed: {e}")))?;
751
752 let key_bytes: &[u8] = self.key.as_bytes();
753 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Encryption)?;
754 let ciphertext = aes256_gcm_encrypt(key_arr, &nonce, VAULT_AAD, &plaintext);
755 let cipher_total = ciphertext.len();
759 let payload_len = (NONCE_SIZE + cipher_total) as u32; let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
767 let overflow = cipher_total.saturating_sub(header_chunk_len);
768 let chain_count = overflow.div_ceil(VAULT_DATA_CIPHER_CAPACITY);
769
770 while pager
784 .page_count()
785 .map_err(|e| VaultError::Pager(e.to_string()))?
786 <= VAULT_HEADER_PAGE
787 {
788 pager
789 .allocate_page(PageType::Vault)
790 .map_err(|e| VaultError::Pager(format!("reserve vault slot: {e}")))?;
791 }
792
793 let old_chain = self.read_existing_chain_ids(pager).unwrap_or_default();
803
804 let mut new_chain: Vec<u32> = Vec::with_capacity(chain_count);
811 for _ in 0..chain_count {
812 let page = pager
813 .allocate_page(PageType::Vault)
814 .map_err(|e| VaultError::Pager(format!("allocate vault data page: {e}")))?;
815 new_chain.push(page.page_id());
816 }
817
818 let mut cursor = header_chunk_len;
825 for i in 0..chain_count {
826 let next_id = if i + 1 < chain_count {
827 new_chain[i + 1]
828 } else {
829 0
830 };
831 let take = (cipher_total - cursor).min(VAULT_DATA_CIPHER_CAPACITY);
832 let frag = &ciphertext[cursor..cursor + take];
833 self.write_data_page(pager, new_chain[i], next_id, frag)?;
834 cursor += take;
835 }
836 debug_assert_eq!(cursor, cipher_total, "ciphertext spill accounting mismatch");
837
838 let first_data_page = new_chain.first().copied().unwrap_or(0);
846 self.write_header_page(
847 pager,
848 &nonce,
849 payload_len,
850 chain_count as u32,
851 first_data_page,
852 &ciphertext[..header_chunk_len],
853 )?;
854
855 pager
860 .flush()
861 .map_err(|e| VaultError::Pager(e.to_string()))?;
862
863 for &id in old_chain.iter() {
868 pager
869 .free_page(id)
870 .map_err(|e| VaultError::Pager(format!("free old vault page {id}: {e}")))?;
871 }
872
873 Ok(())
874 }
875
876 pub fn load(&self, pager: &Pager) -> Result<Option<VaultState>, VaultError> {
880 let page = match pager.read_page_no_checksum(VAULT_HEADER_PAGE) {
882 Ok(p) => p,
883 Err(_) => return Ok(None),
884 };
885
886 let page_content = page.content();
887
888 if page_content.len() < VAULT_HEADER_META_SIZE {
889 return Ok(None);
890 }
891 if &page_content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
892 return Ok(None); }
894
895 let version = page_content[4];
896 if version == VAULT_LEGACY_VERSION {
897 return Err(VaultError::Corrupt(
901 "vault was bootstrapped with the legacy 2-page format \
902 (pre-RedDB v0.3); re-bootstrap with `red bootstrap` to upgrade"
903 .to_string(),
904 ));
905 }
906 if version != VAULT_VERSION {
907 return Err(VaultError::Corrupt(format!(
908 "unsupported vault version: {} (expected {})",
909 version, VAULT_VERSION
910 )));
911 }
912
913 let payload_len = u32::from_le_bytes(
915 page_content[21..25]
916 .try_into()
917 .map_err(|_| VaultError::Corrupt("bad payload length bytes".into()))?,
918 ) as usize;
919
920 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
921 let nonce: [u8; NONCE_SIZE] = page_content[nonce_start..nonce_start + NONCE_SIZE]
922 .try_into()
923 .map_err(|_| VaultError::Corrupt("bad nonce".into()))?;
924
925 let chain_count_off = nonce_start + NONCE_SIZE;
926 let chain_count = u32::from_le_bytes(
927 page_content[chain_count_off..chain_count_off + 4]
928 .try_into()
929 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
930 ) as usize;
931 let first_id_off = chain_count_off + 4;
932 let mut next_id = u32::from_le_bytes(
933 page_content[first_id_off..first_id_off + 4]
934 .try_into()
935 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
936 );
937
938 if payload_len < NONCE_SIZE {
939 return Err(VaultError::Corrupt("payload too short for nonce".into()));
940 }
941 let cipher_total = payload_len - NONCE_SIZE;
942
943 let mut cipher = Vec::with_capacity(cipher_total);
945 let header_chunk_len = cipher_total.min(VAULT_HEADER_CIPHER_CAPACITY);
946 let header_cipher_start = VAULT_HEADER_META_SIZE;
947 cipher.extend_from_slice(
948 &page_content[header_cipher_start..header_cipher_start + header_chunk_len],
949 );
950
951 let mut hops = 0usize;
953 while cipher.len() < cipher_total {
957 if hops >= chain_count {
958 return Err(VaultError::Corrupt(format!(
959 "vault chain shorter than declared: {} hops, expected {}",
960 hops, chain_count
961 )));
962 }
963 if next_id == 0 {
964 return Err(VaultError::Corrupt(
965 "vault chain ends prematurely (next_id == 0)".to_string(),
966 ));
967 }
968
969 let dp = pager
970 .read_page_no_checksum(next_id)
971 .map_err(|e| VaultError::Pager(format!("vault data page {next_id}: {e}")))?;
972 let dp_content = dp.content();
973 if dp_content.len() < VAULT_DATA_PREFIX_SIZE {
974 return Err(VaultError::Corrupt(format!(
975 "vault data page {next_id} truncated"
976 )));
977 }
978 if &dp_content[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC {
979 return Err(VaultError::Corrupt(format!(
980 "vault data page {next_id} has bad magic"
981 )));
982 }
983 let np = u32::from_le_bytes(
984 dp_content[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
985 .try_into()
986 .map_err(|_| VaultError::Corrupt("bad next_page_id bytes".into()))?,
987 );
988 let take = (cipher_total - cipher.len()).min(VAULT_DATA_CIPHER_CAPACITY);
989 let frag_start = VAULT_DATA_PREFIX_SIZE;
990 cipher.extend_from_slice(&dp_content[frag_start..frag_start + take]);
991
992 next_id = np;
993 hops += 1;
994 }
995
996 if cipher.len() != cipher_total {
997 return Err(VaultError::Corrupt(format!(
998 "vault truncated: expected {} cipher bytes, got {}",
999 cipher_total,
1000 cipher.len()
1001 )));
1002 }
1003 if hops != chain_count {
1004 return Err(VaultError::Corrupt(format!(
1005 "vault chain length mismatch: walked {} pages, header says {}",
1006 hops, chain_count
1007 )));
1008 }
1009
1010 let key_bytes: &[u8] = self.key.as_bytes();
1012 let key_arr: &[u8; 32] = key_bytes.try_into().map_err(|_| VaultError::Decryption)?;
1013 let plaintext = aes256_gcm_decrypt(key_arr, &nonce, VAULT_AAD, &cipher)
1014 .map_err(|_| VaultError::Decryption)?;
1015
1016 let state = VaultState::deserialize(&plaintext)?;
1017 Ok(Some(state))
1018 }
1019
1020 fn read_existing_chain_ids(&self, pager: &Pager) -> Result<Vec<u32>, VaultError> {
1027 let header = pager
1028 .read_page_no_checksum(VAULT_HEADER_PAGE)
1029 .map_err(|e| VaultError::Pager(e.to_string()))?;
1030 let content = header.content();
1031 if content.len() < VAULT_HEADER_META_SIZE {
1032 return Ok(Vec::new());
1033 }
1034 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1035 return Ok(Vec::new());
1036 }
1037 let version = content[4];
1038 if version != VAULT_VERSION {
1039 return Ok(Vec::new());
1043 }
1044 let nonce_start = VAULT_HEADER_PREAMBLE_SIZE;
1045 let chain_count_off = nonce_start + NONCE_SIZE;
1046 let chain_count = u32::from_le_bytes(
1047 content[chain_count_off..chain_count_off + 4]
1048 .try_into()
1049 .map_err(|_| VaultError::Corrupt("bad chain_count bytes".into()))?,
1050 ) as usize;
1051 let first_id_off = chain_count_off + 4;
1052 let mut id = u32::from_le_bytes(
1053 content[first_id_off..first_id_off + 4]
1054 .try_into()
1055 .map_err(|_| VaultError::Corrupt("bad first_data_page_id bytes".into()))?,
1056 );
1057
1058 let mut out = Vec::with_capacity(chain_count);
1059 let mut hops = 0usize;
1060 while id != 0 && hops < chain_count {
1061 out.push(id);
1062 match pager.read_page_no_checksum(id) {
1065 Ok(dp) => {
1066 let dc = dp.content();
1067 if dc.len() < VAULT_DATA_PREFIX_SIZE
1068 || &dc[0..VAULT_MAGIC_SIZE] != VAULT_DATA_MAGIC
1069 {
1070 break;
1071 }
1072 id = u32::from_le_bytes(
1073 dc[VAULT_MAGIC_SIZE..VAULT_MAGIC_SIZE + 4]
1074 .try_into()
1075 .map_err(|_| VaultError::Corrupt("bad next_id".into()))?,
1076 );
1077 }
1078 Err(_) => break,
1079 }
1080 hops += 1;
1081 }
1082 Ok(out)
1083 }
1084
1085 fn write_header_page(
1089 &self,
1090 pager: &Pager,
1091 nonce: &[u8; NONCE_SIZE],
1092 payload_len: u32,
1093 chain_count: u32,
1094 first_data_page_id: u32,
1095 cipher_fragment: &[u8],
1096 ) -> Result<(), VaultError> {
1097 debug_assert!(cipher_fragment.len() <= VAULT_HEADER_CIPHER_CAPACITY);
1098
1099 let mut page = Page::new(PageType::Vault, VAULT_HEADER_PAGE);
1100 let bytes = page.as_bytes_mut();
1101 let mut off = HEADER_SIZE;
1102
1103 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_MAGIC);
1104 off += VAULT_MAGIC_SIZE;
1105
1106 bytes[off] = VAULT_VERSION;
1107 off += VAULT_VERSION_SIZE;
1108
1109 bytes[off..off + VAULT_SALT_SIZE].copy_from_slice(&self.salt);
1110 off += VAULT_SALT_SIZE;
1111
1112 bytes[off..off + 4].copy_from_slice(&payload_len.to_le_bytes());
1113 off += VAULT_PAYLOAD_LEN_SIZE;
1114
1115 bytes[off..off + NONCE_SIZE].copy_from_slice(nonce);
1116 off += NONCE_SIZE;
1117
1118 bytes[off..off + 4].copy_from_slice(&chain_count.to_le_bytes());
1119 off += VAULT_CHAIN_COUNT_SIZE;
1120
1121 bytes[off..off + 4].copy_from_slice(&first_data_page_id.to_le_bytes());
1122 off += VAULT_FIRST_PAGE_ID_SIZE;
1123
1124 debug_assert_eq!(off, HEADER_SIZE + VAULT_HEADER_META_SIZE);
1125
1126 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1127
1128 pager
1129 .write_page_no_checksum(VAULT_HEADER_PAGE, page)
1130 .map_err(|e| VaultError::Pager(e.to_string()))?;
1131 Ok(())
1132 }
1133
1134 fn write_data_page(
1136 &self,
1137 pager: &Pager,
1138 page_id: u32,
1139 next_page_id: u32,
1140 cipher_fragment: &[u8],
1141 ) -> Result<(), VaultError> {
1142 debug_assert!(cipher_fragment.len() <= VAULT_DATA_CIPHER_CAPACITY);
1143
1144 let mut page = Page::new(PageType::Vault, page_id);
1145 let bytes = page.as_bytes_mut();
1146 let mut off = HEADER_SIZE;
1147
1148 bytes[off..off + VAULT_MAGIC_SIZE].copy_from_slice(VAULT_DATA_MAGIC);
1149 off += VAULT_MAGIC_SIZE;
1150
1151 bytes[off..off + 4].copy_from_slice(&next_page_id.to_le_bytes());
1152 off += 4;
1153
1154 bytes[off..off + cipher_fragment.len()].copy_from_slice(cipher_fragment);
1155
1156 pager
1157 .write_page_no_checksum(page_id, page)
1158 .map_err(|e| VaultError::Pager(e.to_string()))?;
1159 Ok(())
1160 }
1161}
1162
1163fn parse_scram_field(field: &str) -> Result<crate::auth::scram::ScramVerifier, VaultError> {
1170 let parts: Vec<&str> = field.split(':').collect();
1171 if parts.len() != 4 {
1172 return Err(VaultError::Corrupt(format!(
1173 "SCRAM verifier has {} segments, expected 4",
1174 parts.len()
1175 )));
1176 }
1177 let salt =
1178 hex::decode(parts[0]).map_err(|_| VaultError::Corrupt("invalid SCRAM salt hex".into()))?;
1179 let iter: u32 = parts[1]
1180 .parse()
1181 .map_err(|_| VaultError::Corrupt("invalid SCRAM iter".into()))?;
1182 if iter < crate::auth::scram::MIN_ITER {
1183 return Err(VaultError::Corrupt(format!(
1184 "SCRAM iter {} below minimum {}",
1185 iter,
1186 crate::auth::scram::MIN_ITER
1187 )));
1188 }
1189 let stored_vec = hex::decode(parts[2])
1190 .map_err(|_| VaultError::Corrupt("invalid SCRAM stored_key hex".into()))?;
1191 let server_vec = hex::decode(parts[3])
1192 .map_err(|_| VaultError::Corrupt("invalid SCRAM server_key hex".into()))?;
1193 let stored_key: [u8; 32] = stored_vec
1194 .try_into()
1195 .map_err(|_| VaultError::Corrupt("SCRAM stored_key must be 32 bytes".into()))?;
1196 let server_key: [u8; 32] = server_vec
1197 .try_into()
1198 .map_err(|_| VaultError::Corrupt("SCRAM server_key must be 32 bytes".into()))?;
1199 Ok(crate::auth::scram::ScramVerifier {
1200 salt,
1201 iter,
1202 stored_key,
1203 server_key,
1204 })
1205}
1206
1207fn read_vault_salt_from_pager(pager: &Pager) -> Result<[u8; 16], VaultError> {
1214 let page = pager
1215 .read_page_no_checksum(VAULT_HEADER_PAGE)
1216 .map_err(|e| VaultError::Pager(format!("vault page read: {e}")))?;
1217
1218 let content = page.content();
1219 if content.len() < VAULT_HEADER_PREAMBLE_SIZE {
1220 return Err(VaultError::Corrupt("vault page too short".into()));
1221 }
1222 if &content[0..VAULT_MAGIC_SIZE] != VAULT_MAGIC {
1223 return Err(VaultError::Corrupt("bad magic bytes".into()));
1224 }
1225
1226 let mut salt = [0u8; VAULT_SALT_SIZE];
1227 salt.copy_from_slice(&content[5..21]);
1228 Ok(salt)
1229}
1230
1231#[cfg(test)]
1236mod tests {
1237 use super::*;
1238 use crate::auth::{now_ms, ApiKey, Role, User};
1239 use crate::storage::engine::pager::PagerConfig;
1240
1241 fn sample_state() -> VaultState {
1242 let now = now_ms();
1243 VaultState {
1244 users: vec![
1245 User {
1246 username: "alice".into(),
1247 tenant_id: None,
1248 password_hash: "argon2id$aabbccdd$eeff0011".into(),
1249 scram_verifier: None,
1250 role: Role::Admin,
1251 api_keys: vec![ApiKey {
1252 key: "rk_abc123".into(),
1253 name: "ci-token".into(),
1254 role: Role::Write,
1255 created_at: now,
1256 }],
1257 created_at: now,
1258 updated_at: now,
1259 enabled: true,
1260 system_owned: false,
1261 },
1262 User {
1263 username: "bob".into(),
1264 tenant_id: None,
1265 password_hash: "argon2id$11223344$55667788".into(),
1266 scram_verifier: None,
1267 role: Role::Read,
1268 api_keys: vec![],
1269 created_at: now,
1270 updated_at: now,
1271 enabled: false,
1272 system_owned: false,
1273 },
1274 ],
1275 api_keys: vec![(
1276 UserId::platform("alice"),
1277 ApiKey {
1278 key: "rk_abc123".into(),
1279 name: "ci-token".into(),
1280 role: Role::Write,
1281 created_at: now,
1282 },
1283 )],
1284 bootstrapped: true,
1285 master_secret: None,
1286 kv: std::collections::HashMap::new(),
1287 }
1288 }
1289
1290 fn temp_pager() -> (Pager, std::path::PathBuf) {
1292 use std::sync::atomic::{AtomicU64, Ordering};
1293 static COUNTER: AtomicU64 = AtomicU64::new(0);
1294 let id = COUNTER.fetch_add(1, Ordering::Relaxed);
1295 let tmp_dir =
1296 std::env::temp_dir().join(format!("reddb_vault_test_{}_{}", std::process::id(), id));
1297 std::fs::create_dir_all(&tmp_dir).unwrap();
1298 let db_path = tmp_dir.join("test.rdb");
1299 let pager = Pager::open(&db_path, PagerConfig::default()).unwrap();
1300 (pager, tmp_dir)
1301 }
1302
1303 #[test]
1304 fn test_vault_state_serialize_deserialize_roundtrip() {
1305 let state = sample_state();
1306 let serialized = state.serialize();
1307 let text = std::str::from_utf8(&serialized).unwrap();
1308
1309 assert!(text.contains("SEALED:true"));
1311 assert!(text.contains("USER:alice\t"));
1312 assert!(text.contains("USER:bob\t"));
1313 assert!(text.contains("KEY:alice\trk_abc123\t"));
1314
1315 let restored = VaultState::deserialize(&serialized).unwrap();
1317 assert!(restored.bootstrapped);
1318 assert_eq!(restored.users.len(), 2);
1319
1320 let alice = restored
1321 .users
1322 .iter()
1323 .find(|u| u.username == "alice")
1324 .unwrap();
1325 assert_eq!(alice.role, Role::Admin);
1326 assert!(alice.enabled);
1327 assert_eq!(alice.password_hash, "argon2id$aabbccdd$eeff0011");
1328 assert_eq!(alice.api_keys.len(), 1);
1329 assert_eq!(alice.api_keys[0].key, "rk_abc123");
1330 assert_eq!(alice.api_keys[0].name, "ci-token");
1331 assert_eq!(alice.api_keys[0].role, Role::Write);
1332
1333 let bob = restored.users.iter().find(|u| u.username == "bob").unwrap();
1334 assert_eq!(bob.role, Role::Read);
1335 assert!(!bob.enabled);
1336 assert!(bob.api_keys.is_empty());
1337
1338 assert_eq!(restored.api_keys.len(), 1);
1339 assert_eq!(restored.api_keys[0].0.username, "alice");
1340 assert!(restored.api_keys[0].0.tenant.is_none());
1341 }
1342
1343 #[test]
1344 fn test_vault_state_empty() {
1345 let state = VaultState {
1346 users: vec![],
1347 api_keys: vec![],
1348 bootstrapped: false,
1349 master_secret: None,
1350 kv: std::collections::HashMap::new(),
1351 };
1352 let serialized = state.serialize();
1353 let restored = VaultState::deserialize(&serialized).unwrap();
1354 assert!(!restored.bootstrapped);
1355 assert!(restored.users.is_empty());
1356 assert!(restored.api_keys.is_empty());
1357 }
1358
1359 #[test]
1360 fn test_vault_state_deserialize_invalid_utf8() {
1361 let bad_data = vec![0xFF, 0xFE, 0xFD];
1362 let result = VaultState::deserialize(&bad_data);
1363 assert!(result.is_err());
1364 }
1365
1366 #[test]
1367 fn test_vault_state_deserialize_bad_user_line() {
1368 let bad = b"USER:only_two\tfields\n";
1369 let result = VaultState::deserialize(bad);
1370 assert!(result.is_err());
1371 }
1372
1373 #[test]
1374 fn test_vault_state_deserialize_bad_key_line() {
1375 let bad = b"KEY:too\tfew\n";
1376 let result = VaultState::deserialize(bad);
1377 assert!(result.is_err());
1378 }
1379
1380 #[test]
1381 fn test_vault_state_deserialize_unknown_line_skipped() {
1382 let data = b"SEALED:false\nFUTURE:some_data\n";
1383 let result = VaultState::deserialize(data).unwrap();
1384 assert!(!result.bootstrapped);
1385 }
1386
1387 #[test]
1388 fn test_vault_pager_save_load_roundtrip() {
1389 let (pager, tmp_dir) = temp_pager();
1390
1391 let vault = Vault::open(&pager, Some("test-passphrase-42")).unwrap();
1392
1393 let loaded = vault.load(&pager).unwrap();
1395 assert!(loaded.is_none());
1396
1397 let state = sample_state();
1399 vault.save(&pager, &state).unwrap();
1400
1401 let restored = vault.load(&pager).unwrap().unwrap();
1403 assert!(restored.bootstrapped);
1404 assert_eq!(restored.users.len(), 2);
1405 assert_eq!(restored.api_keys.len(), 1);
1406
1407 let alice = restored
1408 .users
1409 .iter()
1410 .find(|u| u.username == "alice")
1411 .unwrap();
1412 assert_eq!(alice.role, Role::Admin);
1413 assert_eq!(alice.api_keys.len(), 1);
1414
1415 let vault2 = Vault::open(&pager, Some("test-passphrase-42")).unwrap();
1417 let restored2 = vault2.load(&pager).unwrap().unwrap();
1418 assert!(restored2.bootstrapped);
1419 assert_eq!(restored2.users.len(), 2);
1420
1421 drop(pager);
1423 let _ = std::fs::remove_dir_all(&tmp_dir);
1424 }
1425
1426 #[test]
1427 fn test_vault_wrong_key_fails_decryption() {
1428 let (pager, tmp_dir) = temp_pager();
1429
1430 let vault = Vault::open(&pager, Some("correct-key")).unwrap();
1432 let state = VaultState {
1433 users: vec![],
1434 api_keys: vec![],
1435 bootstrapped: true,
1436 master_secret: None,
1437 kv: std::collections::HashMap::new(),
1438 };
1439 vault.save(&pager, &state).unwrap();
1440
1441 let vault2 = Vault::open(&pager, Some("wrong-key")).unwrap();
1443 let result = vault2.load(&pager);
1444
1445 assert!(result.is_err());
1446
1447 drop(pager);
1449 let _ = std::fs::remove_dir_all(&tmp_dir);
1450 }
1451
1452 #[test]
1453 fn test_vault_no_key_error() {
1454 let (pager, tmp_dir) = temp_pager();
1455
1456 let result = Vault::open(&pager, None);
1457 let has_env_key =
1461 std::env::var("REDDB_VAULT_KEY").is_ok() || std::env::var("REDDB_CERTIFICATE").is_ok();
1462 match has_env_key {
1463 true => {
1464 assert!(result.is_ok());
1466 }
1467 false => {
1468 assert!(matches!(result, Err(VaultError::NoKey)));
1469 }
1470 }
1471
1472 drop(pager);
1474 let _ = std::fs::remove_dir_all(&tmp_dir);
1475 }
1476
1477 #[test]
1478 fn test_vault_passphrase_argument() {
1479 let (pager, tmp_dir) = temp_pager();
1480
1481 let vault = Vault::open(&pager, Some("my-passphrase")).unwrap();
1483 let state = VaultState {
1484 users: vec![],
1485 api_keys: vec![],
1486 bootstrapped: false,
1487 master_secret: None,
1488 kv: std::collections::HashMap::new(),
1489 };
1490 vault.save(&pager, &state).unwrap();
1491
1492 let vault2 = Vault::open(&pager, Some("my-passphrase")).unwrap();
1494 let loaded = vault2.load(&pager).unwrap().unwrap();
1495 assert!(!loaded.bootstrapped);
1496
1497 drop(pager);
1498 let _ = std::fs::remove_dir_all(&tmp_dir);
1499 }
1500
1501 #[test]
1506 fn test_keypair_generate_deterministic_certificate() {
1507 let kp = KeyPair::generate();
1508 assert_eq!(kp.master_secret.len(), 32);
1509 assert_eq!(kp.certificate.len(), 32);
1510
1511 let kp2 = KeyPair::from_master_secret(kp.master_secret.clone());
1513 assert_eq!(kp.certificate, kp2.certificate);
1514 }
1515
1516 #[test]
1517 fn test_keypair_sign_verify() {
1518 let kp = KeyPair::generate();
1519 let data = b"session:abc123";
1520 let sig = kp.sign(data);
1521 assert!(kp.verify(data, &sig));
1522
1523 assert!(!kp.verify(b"session:wrong", &sig));
1525
1526 let mut bad_sig = sig.clone();
1528 bad_sig[0] ^= 0xFF;
1529 assert!(!kp.verify(data, &bad_sig));
1530 }
1531
1532 #[test]
1533 fn test_keypair_certificate_hex() {
1534 let kp = KeyPair::generate();
1535 let hex_str = kp.certificate_hex();
1536 assert_eq!(hex_str.len(), 64); let decoded = hex::decode(&hex_str).unwrap();
1538 assert_eq!(decoded, kp.certificate);
1539 }
1540
1541 #[test]
1542 fn test_vault_certificate_seal_roundtrip() {
1543 let (pager, tmp_dir) = temp_pager();
1544
1545 let kp = KeyPair::generate();
1547 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1548
1549 let state = VaultState {
1551 users: vec![],
1552 api_keys: vec![],
1553 bootstrapped: true,
1554 master_secret: Some(kp.master_secret.clone()),
1555 kv: std::collections::HashMap::new(),
1556 };
1557 vault.save(&pager, &state).unwrap();
1558
1559 let vault2 = Vault::with_certificate(&pager, &kp.certificate_hex()).unwrap();
1561 let loaded = vault2.load(&pager).unwrap().unwrap();
1562 assert!(loaded.bootstrapped);
1563 assert_eq!(loaded.master_secret, Some(kp.master_secret.clone()));
1564
1565 let kp2 = KeyPair::from_master_secret(loaded.master_secret.unwrap());
1567 assert_eq!(kp.certificate, kp2.certificate);
1568
1569 drop(pager);
1570 let _ = std::fs::remove_dir_all(&tmp_dir);
1571 }
1572
1573 #[test]
1574 fn test_vault_certificate_wrong_cert_fails() {
1575 let (pager, tmp_dir) = temp_pager();
1576
1577 let kp = KeyPair::generate();
1579 let vault = Vault::with_certificate_bytes(&pager, &kp.certificate).unwrap();
1580 let state = VaultState {
1581 users: vec![],
1582 api_keys: vec![],
1583 bootstrapped: true,
1584 master_secret: Some(kp.master_secret.clone()),
1585 kv: std::collections::HashMap::new(),
1586 };
1587 vault.save(&pager, &state).unwrap();
1588
1589 let kp2 = KeyPair::generate();
1591 let vault2 = Vault::with_certificate_bytes(&pager, &kp2.certificate).unwrap();
1592 let result = vault2.load(&pager);
1593 assert!(result.is_err());
1594
1595 drop(pager);
1596 let _ = std::fs::remove_dir_all(&tmp_dir);
1597 }
1598
1599 #[test]
1600 fn test_vault_state_master_secret_serialization() {
1601 let secret = vec![0xAA; 32];
1602 let state = VaultState {
1603 users: vec![],
1604 api_keys: vec![],
1605 bootstrapped: true,
1606 master_secret: Some(secret.clone()),
1607 kv: std::collections::HashMap::new(),
1608 };
1609 let serialized = state.serialize();
1610 let text = std::str::from_utf8(&serialized).unwrap();
1611 assert!(text.contains("MASTER_SECRET:"));
1612 assert!(text.contains(&hex::encode(&secret)));
1613
1614 let restored = VaultState::deserialize(&serialized).unwrap();
1615 assert_eq!(restored.master_secret, Some(secret));
1616 assert!(restored.bootstrapped);
1617 }
1618
1619 #[test]
1620 fn test_vault_state_no_master_secret_backward_compat() {
1621 let data = b"SEALED:true\n";
1623 let restored = VaultState::deserialize(data).unwrap();
1624 assert!(restored.master_secret.is_none());
1625 assert!(restored.bootstrapped);
1626 }
1627
1628 #[test]
1629 fn test_vault_state_scram_verifier_roundtrip() {
1630 use crate::auth::scram::ScramVerifier;
1631
1632 let verifier = ScramVerifier::from_password(
1633 "hunter2",
1634 b"reddb-vault-test-salt".to_vec(),
1635 crate::auth::scram::DEFAULT_ITER,
1636 );
1637
1638 let now = now_ms();
1639 let state = VaultState {
1640 users: vec![User {
1641 username: "carol".into(),
1642 tenant_id: None,
1643 password_hash: "argon2id$abc$def".into(),
1644 scram_verifier: Some(verifier.clone()),
1645 role: Role::Admin,
1646 api_keys: vec![],
1647 created_at: now,
1648 updated_at: now,
1649 enabled: true,
1650 system_owned: true,
1651 }],
1652 api_keys: vec![],
1653 bootstrapped: true,
1654 master_secret: None,
1655 kv: std::collections::HashMap::new(),
1656 };
1657
1658 let bytes = state.serialize();
1659 let restored = VaultState::deserialize(&bytes).unwrap();
1660 let carol = restored
1661 .users
1662 .iter()
1663 .find(|u| u.username == "carol")
1664 .unwrap();
1665 let v = carol.scram_verifier.as_ref().expect("verifier round-trips");
1666 assert_eq!(v.salt, verifier.salt);
1667 assert_eq!(v.iter, verifier.iter);
1668 assert_eq!(v.stored_key, verifier.stored_key);
1669 assert_eq!(v.server_key, verifier.server_key);
1670 }
1671
1672 #[test]
1673 fn test_vault_state_pre_tenant_user_line_still_parses() {
1674 let now = now_ms();
1678 let line = format!(
1679 "USER:dave\targon2id$x$y\tread\ttrue\t{}\t{}\t\nSEALED:false\n",
1680 now, now
1681 );
1682 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1683 let dave = restored
1684 .users
1685 .iter()
1686 .find(|u| u.username == "dave")
1687 .unwrap();
1688 assert!(dave.scram_verifier.is_none());
1689 assert!(dave.tenant_id.is_none());
1690 assert!(!dave.system_owned);
1691 }
1692
1693 #[test]
1694 fn test_vault_state_legacy_tenant_user_line_defaults_system_owned_false() {
1695 let now = now_ms();
1696 let line = format!(
1697 "USER:erin\targon2id$x$y\twrite\ttrue\t{}\t{}\t\tacme\nSEALED:false\n",
1698 now, now
1699 );
1700 let restored = VaultState::deserialize(line.as_bytes()).unwrap();
1701 let erin = restored
1702 .users
1703 .iter()
1704 .find(|u| u.username == "erin")
1705 .unwrap();
1706 assert_eq!(erin.tenant_id.as_deref(), Some("acme"));
1707 assert!(!erin.system_owned);
1708 }
1709
1710 #[test]
1711 fn test_vault_state_user_line_with_tenant_roundtrip() {
1712 let now = now_ms();
1713 let state = VaultState {
1714 users: vec![User {
1715 username: "alice".into(),
1716 tenant_id: Some("acme".into()),
1717 password_hash: "argon2id$x$y".into(),
1718 scram_verifier: None,
1719 role: Role::Write,
1720 api_keys: vec![],
1721 created_at: now,
1722 updated_at: now,
1723 enabled: true,
1724 system_owned: true,
1725 }],
1726 api_keys: vec![],
1727 bootstrapped: true,
1728 master_secret: None,
1729 kv: std::collections::HashMap::new(),
1730 };
1731 let bytes = state.serialize();
1732 let text = std::str::from_utf8(&bytes).unwrap();
1733 assert!(text.contains("\tacme\ttrue\n"));
1735
1736 let restored = VaultState::deserialize(&bytes).unwrap();
1737 let alice = restored
1738 .users
1739 .iter()
1740 .find(|u| u.username == "alice")
1741 .unwrap();
1742 assert_eq!(alice.tenant_id.as_deref(), Some("acme"));
1743 assert!(alice.system_owned);
1744 }
1745
1746 #[test]
1747 fn test_vault_state_key_line_with_tenant_reattaches_correctly() {
1748 let now = now_ms();
1751 let state = VaultState {
1752 users: vec![
1753 User {
1754 username: "alice".into(),
1755 tenant_id: Some("acme".into()),
1756 password_hash: "argon2id$x$y".into(),
1757 scram_verifier: None,
1758 role: Role::Write,
1759 api_keys: vec![],
1760 created_at: now,
1761 updated_at: now,
1762 enabled: true,
1763 system_owned: false,
1764 },
1765 User {
1766 username: "alice".into(),
1767 tenant_id: Some("globex".into()),
1768 password_hash: "argon2id$a$b".into(),
1769 scram_verifier: None,
1770 role: Role::Read,
1771 api_keys: vec![],
1772 created_at: now,
1773 updated_at: now,
1774 enabled: true,
1775 system_owned: false,
1776 },
1777 ],
1778 api_keys: vec![
1779 (
1780 UserId::scoped("acme", "alice"),
1781 ApiKey {
1782 key: "rk_acme_key".into(),
1783 name: "deploy".into(),
1784 role: Role::Write,
1785 created_at: now,
1786 },
1787 ),
1788 (
1789 UserId::scoped("globex", "alice"),
1790 ApiKey {
1791 key: "rk_globex_key".into(),
1792 name: "ci".into(),
1793 role: Role::Read,
1794 created_at: now,
1795 },
1796 ),
1797 ],
1798 bootstrapped: true,
1799 master_secret: None,
1800 kv: std::collections::HashMap::new(),
1801 };
1802 let bytes = state.serialize();
1803 let restored = VaultState::deserialize(&bytes).unwrap();
1804 assert_eq!(restored.api_keys.len(), 2);
1807 let acme_key = restored
1808 .api_keys
1809 .iter()
1810 .find(|(o, _)| o.tenant.as_deref() == Some("acme"))
1811 .unwrap();
1812 assert_eq!(acme_key.1.key, "rk_acme_key");
1813 let globex_key = restored
1814 .api_keys
1815 .iter()
1816 .find(|(o, _)| o.tenant.as_deref() == Some("globex"))
1817 .unwrap();
1818 assert_eq!(globex_key.1.key, "rk_globex_key");
1819 }
1820
1821 #[test]
1822 fn test_vault_state_scram_iter_below_min_rejected() {
1823 let now = now_ms();
1824 let stored_hex = "00".repeat(32);
1828 let server_hex = "11".repeat(32);
1829 let line = format!(
1830 "USER:eve\targon2id$x$y\tread\ttrue\t{}\t{}\tdeadbeef:1024:{}:{}\n",
1831 now, now, stored_hex, server_hex
1832 );
1833 match VaultState::deserialize(line.as_bytes()) {
1834 Err(VaultError::Corrupt(msg)) => assert!(msg.contains("below minimum")),
1835 Err(other) => panic!("expected Corrupt iter-floor error, got {other:?}"),
1836 Ok(_) => panic!("expected Corrupt iter-floor error, got Ok"),
1837 }
1838 }
1839
1840 #[test]
1841 fn test_constant_time_eq_function() {
1842 assert!(constant_time_eq(b"hello", b"hello"));
1843 assert!(!constant_time_eq(b"hello", b"world"));
1844 assert!(!constant_time_eq(b"short", b"longer"));
1845 assert!(constant_time_eq(b"", b""));
1846 }
1847}