Skip to main content

revault_vault_api/
vault_directory.rs

1use revault_lockbox_api::{
2    ArtifactKind, ContactKeyPair, ContactPublicKey, Error, FileLockScope, FormDefinition,
3    FormFieldDefinition, FormFieldKind, FormTypeId, ListOptions, Lockbox, LockboxEntryKind,
4    LockboxId, LockboxOpen, LockboxPath, LockboxProtection, OwnerSigningKeyPair,
5    OwnerSigningPublicKey, ReadOnly, Result, ScopedFileLock, SecretString, SecretVec, VariableName,
6};
7use sha2::{Digest, Sha256};
8use std::cell::{Cell, RefCell};
9use std::env;
10use std::fs::{self, File, OpenOptions};
11use std::io::{Read, Write};
12use std::path::{Path, PathBuf};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use crate::key_format::{export_private_key, import_private_key, KeyFormat};
16
17const VAULT_FILE_NAME: &str = "local-vault.lbox";
18const VAULT_BACKUP_MAGIC: &[u8; 8] = b"LBVBK001";
19const VAULT_STRUCTURE_VERSION_PATH: &str = "/vault/structure-version";
20const KNOWN_LOCKBOX_MAGIC: &[u8; 4] = b"LBKL";
21const KNOWN_LOCKBOX_VERSION: u16 = 1;
22const PROFILE_HISTORY_MAGIC: &[u8; 4] = b"LBPH";
23const PROFILE_HISTORY_VERSION: u16 = 1;
24const PROFILE_EMAIL_MAGIC: &[u8; 4] = b"LBPE";
25const PROFILE_EMAIL_VERSION: u16 = 1;
26const GENERATION_ACTIVE: u16 = 1;
27const GENERATION_RETIRED: u16 = 2;
28const GENERATION_COMPROMISED: u16 = 3;
29
30thread_local! {
31    static VAULT_LOCK_DEPTH: Cell<usize> = const { Cell::new(0) };
32}
33
34/// Current on-disk structure version for records stored inside the local vault.
35pub const CURRENT_VAULT_STRUCTURE_VERSION: u32 = 2;
36
37/// Validates a profile or contact name used by the native vault.
38///
39/// # Errors
40///
41/// Returns [`Error::InvalidInput`] when `name` is empty, too long, contains
42/// unsupported characters, or is not in its normalized form.
43pub fn validate_vault_record_name(name: &str) -> Result<()> {
44    validate_record_name(name).map(|_| ())
45}
46
47/// Contact entry stored in a `VaultDirectory`.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct StoredContact {
50    /// User-assigned contact name.
51    pub name: String,
52
53    /// Contact public key associated with `name`.
54    pub key: ContactPublicKey,
55}
56
57/// Lockbox path remembered by the local vault for diagnostics and bulk access
58/// refresh operations.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct KnownLockbox {
61    /// Stable id embedded in the lockbox.
62    pub lockbox_id: LockboxId,
63
64    /// Path used when this lockbox was last seen.
65    pub path: String,
66
67    /// Last time this record was updated.
68    pub last_seen_unix_ms: u64,
69}
70
71/// Local-only label for one access slot in a remembered lockbox.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct AccessSlotLabel {
74    /// Lockbox containing the labelled access slot.
75    pub lockbox_id: LockboxId,
76    /// Stable identifier of the access slot within the lockbox.
77    pub slot_id: u64,
78    /// User-assigned, local-only label.
79    pub name: String,
80    /// Time at which the label was last changed, in Unix milliseconds.
81    pub updated_at_unix_ms: u64,
82}
83
84/// One generation of a vault profile.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ProfileGeneration {
87    /// Monotonically increasing generation number within the profile.
88    pub index: u16,
89    /// Current lifecycle state of this generation.
90    pub status: ProfileGenerationStatus,
91    /// Fingerprint of the contact key belonging to this generation.
92    pub contact_fingerprint: Vec<u8>,
93    /// Creation time in Unix milliseconds.
94    pub created_at_unix_ms: u64,
95    /// Retirement time in Unix milliseconds, when the generation was retired.
96    pub retired_at_unix_ms: Option<u64>,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100/// Lifecycle state of one profile key generation.
101pub enum ProfileGenerationStatus {
102    /// The generation currently used for new operations.
103    Active,
104    /// The generation was replaced normally and remains part of history.
105    Retired,
106    /// The generation must no longer be trusted because its key was exposed.
107    Compromised,
108}
109
110/// Versioned profile history for one vault profile.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct ProfileHistory {
113    /// User-assigned profile name.
114    pub name: String,
115    /// Index of the generation currently used by the profile.
116    pub active_generation: u16,
117    /// All known generations, including retired or compromised entries.
118    pub generations: Vec<ProfileGeneration>,
119}
120
121/// Metadata stored in an encrypted vault backup archive.
122#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
123pub struct VaultBackupManifest {
124    /// Backup archive format version.
125    pub format_version: u16,
126
127    /// Backup creation time.
128    pub created_at_unix_ms: u64,
129
130    /// Name of the encrypted vault file contained in the archive.
131    pub vault_file_name: String,
132
133    /// Number of bytes in the encrypted vault file.
134    pub vault_size: u64,
135
136    /// SHA-256 checksum of the encrypted vault file, encoded as lowercase hex.
137    pub vault_sha256: String,
138}
139
140/// Password-protected vault file for native reVault metadata.
141///
142/// The default layout stores `local-vault.lbox` under a private directory;
143/// explicitly created vaults may use any file path. A vault can hold profile
144/// private keys, contact public keys, and key-directory recovery records.
145#[derive(Debug)]
146pub struct VaultDirectory {
147    root: PathBuf,
148    path: PathBuf,
149    lockbox: RefCell<Lockbox>,
150}
151
152/// Read-only view of encrypted vault metadata.
153///
154/// This type deliberately opens the vault with [`Lockbox::open`] and never
155/// attaches or loads an owner-signing key. It is intended for completion,
156/// diagnostics, and other metadata-only consumers.
157#[derive(Debug)]
158pub struct ReadOnlyVaultDirectory {
159    lockbox: RefCell<Lockbox<ReadOnly>>,
160}
161
162impl ReadOnlyVaultDirectory {
163    /// Opens the default vault without loading owner-signing material.
164    pub fn open_default(password: &SecretString) -> Result<Self> {
165        Self::open(default_vault_dir()?, password)
166    }
167
168    /// Opens a vault at `root` without loading owner-signing material.
169    pub fn open(root: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
170        let path = root.as_ref().join(VAULT_FILE_NAME);
171        if !path.exists() {
172            return Err(Error::VaultUnavailable(
173                "local vault is not initialized; run `lockbox vault init` first".to_string(),
174            ));
175        }
176        Self::open_file(path, password)
177    }
178
179    /// Opens a vault file without loading owner-signing material.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error when the file does not exist, cannot be read, is not a
184    /// supported vault, or cannot be decrypted with `password`.
185    pub fn open_file(path: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
186        let path = path.as_ref();
187        if !path.exists() {
188            return Err(Error::VaultUnavailable(format!(
189                "vault file does not exist: {}",
190                path.display()
191            )));
192        }
193        Ok(Self {
194            lockbox: RefCell::new(Lockbox::open(path, LockboxOpen::Password(password))?),
195        })
196    }
197
198    /// Lists profile names without reading their private key records.
199    pub fn list_private_key_names(&self) -> Result<Vec<String>> {
200        let mut names = Vec::new();
201        for (variable_name, _) in self.lockbox.borrow().list_variables()? {
202            let Some(name) = private_key_name_from_variable(&variable_name) else {
203                continue;
204            };
205            names.push(name?);
206        }
207        names.sort();
208        names.dedup();
209        Ok(names)
210    }
211
212    /// Lists saved contact names without loading contact key material.
213    pub fn list_contact_names(&self) -> Result<Vec<String>> {
214        list_read_only_record_names(&self.lockbox, "/contacts", ".pub")
215    }
216
217    /// Lists reusable form aliases. Form definitions contain no private key
218    /// material and are read directly from the encrypted metadata view.
219    pub fn list_form_aliases(&self) -> Result<Vec<String>> {
220        let mut aliases = self
221            .lockbox
222            .borrow()
223            .list_form_definitions()?
224            .into_iter()
225            .map(|definition| definition.alias)
226            .collect::<Vec<_>>();
227        aliases.sort();
228        aliases.dedup();
229        Ok(aliases)
230    }
231
232    /// Lists remembered lockbox paths without opening any lockbox or key.
233    pub fn list_known_lockboxes(&self) -> Result<Vec<KnownLockbox>> {
234        let mut out = Vec::new();
235        for name in list_read_only_record_names(&self.lockbox, "/known_lockboxes", ".lkl")? {
236            let path = LockboxPath::new(format!("/known_lockboxes/{name}.lkl"))?;
237            out.push(decode_known_lockbox(
238                &self.lockbox.borrow().get_file(&path)?,
239            )?);
240        }
241        out.sort_by(|left, right| left.path.cmp(&right.path));
242        Ok(out)
243    }
244}
245
246impl VaultDirectory {
247    /// Default name used for the primary local contact key.
248    pub const DEFAULT_KEY_NAME: &'static str = "default";
249
250    /// Reads the stable vault structure discriminator without interpreting
251    /// version-specific records. Migration orchestration uses this to choose a
252    /// historical exporter before opening the source through `VaultDirectory`.
253    pub fn probe_structure_version(root: impl AsRef<Path>, password: &SecretString) -> Result<u32> {
254        let path = root.as_ref().join(VAULT_FILE_NAME);
255        let lockbox = Lockbox::open(&path, LockboxOpen::Password(password))?;
256        let record = lockbox.get_file(&vault_structure_version_record_path()?)?;
257        decode_structure_version(&record)
258    }
259
260    /// Opens or creates the default vault directory using `password`.
261    ///
262    /// The directory is chosen by `default_vault_dir`.
263    pub fn open_or_create_default(password: &SecretString) -> Result<Self> {
264        Self::open_or_create(default_vault_dir()?, password)
265    }
266
267    /// Creates a new vault at an explicit file path.
268    ///
269    /// Unlike [`Self::open_or_create`], this never opens an existing vault and
270    /// does not require the conventional `local-vault.lbox` filename.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error when `path` already exists, its parent cannot be
275    /// created, the vault cannot be locked or written, or key generation fails.
276    pub fn create_file(path: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
277        let path = path.as_ref().to_path_buf();
278        if path.exists() {
279            return Err(Error::AlreadyExists(path.display().to_string()));
280        }
281        let root = path
282            .parent()
283            .filter(|parent| !parent.as_os_str().is_empty())
284            .unwrap_or_else(|| Path::new("."))
285            .to_path_buf();
286        fs::create_dir_all(&root).map_err(|err| Error::Io(err.to_string()))?;
287        let _guard = VaultFileLock::acquire(&path)?;
288        if path.exists() {
289            return Err(Error::AlreadyExists(path.display().to_string()));
290        }
291        let signing_key = OwnerSigningKeyPair::generate()?;
292        let lockbox = Lockbox::create_file_assuming_locked(
293            &path,
294            LockboxProtection::Password(password),
295            &signing_key,
296        )?;
297        set_private_file_permissions(&path)?;
298        let vault = Self {
299            root,
300            path,
301            lockbox: RefCell::new(lockbox),
302        };
303        vault.store_owner_signing_key_current_only(Self::DEFAULT_KEY_NAME, &signing_key)?;
304        vault.ensure_structure_version(true)?;
305        Ok(vault)
306    }
307
308    /// Opens an existing vault at an explicit file path.
309    ///
310    /// # Errors
311    ///
312    /// Returns an error when the file does not exist, cannot be locked or
313    /// opened, is not a supported vault, or cannot be decrypted with `password`.
314    pub fn open_file(path: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
315        let path = path.as_ref().to_path_buf();
316        if !path.exists() {
317            return Err(Error::VaultUnavailable(format!(
318                "vault file does not exist: {}",
319                path.display()
320            )));
321        }
322        let root = path
323            .parent()
324            .filter(|parent| !parent.as_os_str().is_empty())
325            .unwrap_or_else(|| Path::new("."))
326            .to_path_buf();
327        let _guard = VaultFileLock::acquire(&path)?;
328        let lockbox = open_vault_lockbox_for_write(&path, password)?;
329        let vault = Self {
330            root,
331            path,
332            lockbox: RefCell::new(lockbox),
333        };
334        vault.attach_or_create_default_owner_signing_key()?;
335        vault.ensure_structure_version(false)?;
336        Ok(vault)
337    }
338
339    /// Replaces the default vault directory using `password`.
340    ///
341    /// The replacement is coordinated with the same interprocess lock used for
342    /// vault backups and record writes.
343    pub fn replace_default(password: &SecretString) -> Result<Self> {
344        Self::replace(default_vault_dir()?, password)
345    }
346
347    /// Changes the pass phrase for the default vault directory.
348    pub fn change_default_password(
349        old_password: &SecretString,
350        new_password: &SecretString,
351    ) -> Result<()> {
352        Self::change_password(default_vault_dir()?, old_password, new_password)
353    }
354
355    /// Changes the pass phrase for a vault directory.
356    pub fn change_password(
357        root: impl AsRef<Path>,
358        old_password: &SecretString,
359        new_password: &SecretString,
360    ) -> Result<()> {
361        let root = root.as_ref().to_path_buf();
362        let path = root.join(VAULT_FILE_NAME);
363        if !path.exists() {
364            return Err(Error::VaultUnavailable(
365                "local vault is not initialized; run `lockbox vault init` first".to_string(),
366            ));
367        }
368        let _guard = VaultFileLock::acquire(&path)?;
369        let lockbox = open_vault_lockbox_for_write(&path, old_password)?;
370        let vault = Self {
371            root,
372            path,
373            lockbox: RefCell::new(lockbox),
374        };
375        vault.attach_or_create_default_owner_signing_key()?;
376        vault
377            .lockbox
378            .borrow_mut()
379            .replace_password(old_password, new_password)?;
380        set_private_file_permissions(&vault.path)?;
381        let vault_id = vault.path.to_string_lossy().into_owned();
382        let _ = crate::forget_vault_unlock_key(&vault_id);
383        let _ = crate::forget_owner_signing_key(&vault_id, Self::DEFAULT_KEY_NAME);
384        Ok(())
385    }
386
387    /// Replaces the vault directory at `root` using `password`.
388    pub fn replace(root: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
389        let root = root.as_ref().to_path_buf();
390        create_private_dir(&root)?;
391        let path = root.join(VAULT_FILE_NAME);
392        let _guard = VaultFileLock::acquire(&path)?;
393        let vault_id = path.to_string_lossy().into_owned();
394        let _ = crate::forget_vault_unlock_key(&vault_id);
395        let _ = crate::forget_owner_signing_key(&vault_id, Self::DEFAULT_KEY_NAME);
396        if path.exists() {
397            fs::remove_file(&path).map_err(|err| Error::Io(err.to_string()))?;
398        }
399        let signing_key = OwnerSigningKeyPair::generate()?;
400        let lockbox = Lockbox::create_file_assuming_locked(
401            &path,
402            LockboxProtection::Password(password),
403            &signing_key,
404        )?;
405        set_private_file_permissions(&path)?;
406        let vault = Self {
407            root,
408            path,
409            lockbox: RefCell::new(lockbox),
410        };
411        vault.store_owner_signing_key_current_only(Self::DEFAULT_KEY_NAME, &signing_key)?;
412        vault.ensure_structure_version(true)?;
413        Ok(vault)
414    }
415
416    /// Creates an empty current-format vault for migration import.
417    ///
418    /// The container still has an ephemeral owner signer, but unlike `replace`
419    /// this does not seed that signer as a user-visible `default` vault key.
420    #[doc(hidden)]
421    pub fn replace_for_migration(root: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
422        let root = root.as_ref().to_path_buf();
423        create_private_dir(&root)?;
424        let path = root.join(VAULT_FILE_NAME);
425        let _guard = VaultFileLock::acquire(&path)?;
426        if path.exists() {
427            return Err(Error::AlreadyExists(path.display().to_string()));
428        }
429        let signing_key = OwnerSigningKeyPair::generate()?;
430        let lockbox = Lockbox::create_file_assuming_locked(
431            &path,
432            LockboxProtection::Password(password),
433            &signing_key,
434        )?;
435        set_private_file_permissions(&path)?;
436        let vault = Self {
437            root,
438            path,
439            lockbox: RefCell::new(lockbox),
440        };
441        vault.ensure_structure_version(true)?;
442        Ok(vault)
443    }
444
445    /// Opens or creates a vault directory at `root`.
446    ///
447    /// The vault file is protected with `password`. When a new vault file is
448    /// created, private file permissions are applied on supported platforms.
449    pub fn open_or_create(root: impl AsRef<Path>, password: &SecretString) -> Result<Self> {
450        let root = root.as_ref().to_path_buf();
451        create_private_dir(&root)?;
452        let path = root.join(VAULT_FILE_NAME);
453        let _guard = VaultFileLock::acquire(&path)?;
454        let existed = path.exists();
455        let lockbox = if existed {
456            open_vault_lockbox_for_write(&path, password)?
457        } else {
458            let signing_key = OwnerSigningKeyPair::generate()?;
459            let lockbox = Lockbox::create_file_assuming_locked(
460                &path,
461                LockboxProtection::Password(password),
462                &signing_key,
463            )?;
464            set_private_file_permissions(&path)?;
465            let vault = Self {
466                root,
467                path,
468                lockbox: RefCell::new(lockbox),
469            };
470            vault.store_owner_signing_key_current_only(Self::DEFAULT_KEY_NAME, &signing_key)?;
471            vault.ensure_structure_version(true)?;
472            return Ok(vault);
473        };
474        let vault = Self {
475            root,
476            path,
477            lockbox: RefCell::new(lockbox),
478        };
479        vault.attach_or_create_default_owner_signing_key()?;
480        vault.ensure_structure_version(!existed)?;
481        Ok(vault)
482    }
483
484    /// Returns the directory containing this vault file.
485    pub fn root(&self) -> &Path {
486        &self.root
487    }
488
489    /// Returns the vault file path.
490    pub fn path(&self) -> &Path {
491        &self.path
492    }
493
494    fn attach_or_create_default_owner_signing_key(&self) -> Result<()> {
495        let signing_key = match self.load_owner_signing_key_existing(Self::DEFAULT_KEY_NAME) {
496            Ok(signing_key) => signing_key,
497            Err(Error::NotFound(_)) => {
498                let signing_key = OwnerSigningKeyPair::generate()?;
499                self.lockbox
500                    .borrow_mut()
501                    .set_owner_signing_key(signing_key.try_clone()?);
502                self.store_owner_signing_key_current_only(Self::DEFAULT_KEY_NAME, &signing_key)?;
503                return Ok(());
504            }
505            Err(err) => return Err(err),
506        };
507        self.lockbox.borrow_mut().set_owner_signing_key(signing_key);
508        Ok(())
509    }
510
511    /// Returns the structure version recorded inside this vault.
512    pub fn structure_version(&self) -> Result<u32> {
513        self.read_structure_version()?.ok_or_else(|| {
514            Error::CorruptVaultRecord("vault structure version record is missing".to_string())
515        })
516    }
517
518    /// Stores a contact private key under `name`.
519    ///
520    /// Names must contain only ASCII letters, digits, `-`, or `_`.
521    pub fn store_private_key(&self, name: &str, keypair: &ContactKeyPair) -> Result<()> {
522        let variable_name = private_key_variable_name(name)?;
523        let private_record = export_private_key(keypair, KeyFormat::RawHex)?;
524        let value = SecretString::from_secure_vec(private_record);
525        self.put_secret_variable_record(&variable_name, &value)?;
526        if !self.owner_signing_key_exists(name)? {
527            self.store_owner_signing_key_current_only(name, &OwnerSigningKeyPair::generate()?)?;
528        }
529        if self.read_profile_history(name)?.is_none() {
530            self.store_private_key_generation(name, 1, keypair)?;
531            let signing_key = self.load_owner_signing_key(name)?;
532            self.store_owner_signing_key_generation(name, 1, &signing_key)?;
533            let now = unix_ms(SystemTime::now());
534            self.write_profile_history(&ProfileHistory {
535                name: name.to_string(),
536                active_generation: 1,
537                generations: vec![ProfileGeneration {
538                    index: 1,
539                    status: ProfileGenerationStatus::Active,
540                    contact_fingerprint: contact_fingerprint(&keypair.public_key()),
541                    created_at_unix_ms: now,
542                    retired_at_unix_ms: None,
543                }],
544            })?;
545        }
546        Ok(())
547    }
548
549    /// Restores a profile private key and optional owner signing key.
550    ///
551    /// When `overwrite` is true, any existing profile with the same name is
552    /// removed first so current keys, signing keys, and generation history stay
553    /// consistent with the restored material.
554    pub fn restore_private_key(
555        &self,
556        name: &str,
557        keypair: &ContactKeyPair,
558        signing_key: Option<&OwnerSigningKeyPair>,
559        overwrite: bool,
560    ) -> Result<()> {
561        if self.private_key_exists(name)? {
562            if !overwrite {
563                return Err(Error::AlreadyExists(format!("vault profile {name}")));
564            }
565            self.delete_private_key(name)?;
566        }
567        if let Some(signing_key) = signing_key {
568            self.store_owner_signing_key_current_only(name, signing_key)?;
569        }
570        self.store_private_key(name, keypair)
571    }
572
573    /// Restores an exact profile generation history during a format migration.
574    ///
575    /// This is intentionally a logical import operation: native vault pages
576    /// and record offsets are never copied from the source vault.
577    #[doc(hidden)]
578    pub fn restore_profile_generations(
579        &self,
580        history: ProfileHistory,
581        generations: Vec<(u16, ContactKeyPair, OwnerSigningKeyPair)>,
582        email: Option<&str>,
583        overwrite: bool,
584    ) -> Result<()> {
585        if generations.is_empty() {
586            return Err(Error::InvalidInput(
587                "a migrated profile must contain at least one generation".to_string(),
588            ));
589        }
590        if history.generations.len() != generations.len()
591            || !history
592                .generations
593                .iter()
594                .all(|item| generations.iter().any(|(index, _, _)| *index == item.index))
595        {
596            return Err(Error::InvalidInput(
597                "migrated profile generation keys do not match its history".to_string(),
598            ));
599        }
600        let Some((_, active_key, active_signing)) = generations
601            .iter()
602            .find(|(index, _, _)| *index == history.active_generation)
603        else {
604            return Err(Error::InvalidInput(
605                "migrated profile active generation is missing".to_string(),
606            ));
607        };
608        if self.private_key_exists(&history.name)? {
609            if !overwrite {
610                return Err(Error::AlreadyExists(format!(
611                    "vault profile {}",
612                    history.name
613                )));
614            }
615            self.delete_private_key(&history.name)?;
616        }
617        self.store_private_key_current_only(&history.name, active_key)?;
618        self.store_owner_signing_key_current_only(&history.name, active_signing)?;
619        for (index, key, signing) in generations {
620            self.store_private_key_generation(&history.name, index, &key)?;
621            self.store_owner_signing_key_generation(&history.name, index, &signing)?;
622        }
623        self.write_profile_history(&history)?;
624        if let Some(email) = email {
625            self.store_profile_email(&history.name, email)?;
626        }
627        Ok(())
628    }
629
630    /// Loads a contact private key previously stored under `name`.
631    pub fn load_private_key(&self, name: &str) -> Result<ContactKeyPair> {
632        let variable_name = private_key_variable_name(name)?;
633        let secret = self
634            .lockbox
635            .borrow()
636            .with_secret_variable(&variable_name, SecretString::try_clone)?
637            .transpose()?
638            .ok_or_else(|| Error::NotFound(format!("vault private key {name}")))?;
639        let mut bytes = SecretVec::new();
640        secret.append_to_secure_vec(&mut bytes)?;
641        import_private_key(bytes)
642    }
643
644    /// Loads the owner signing key associated with a vault profile.
645    ///
646    /// Older vault profiles did not have a separate signing key. The first
647    /// load lazily creates one so future lockbox commits can be signed without
648    /// deriving signing material from the lockbox content key.
649    pub fn load_owner_signing_key(&self, name: &str) -> Result<OwnerSigningKeyPair> {
650        let vault_id = self.path.to_string_lossy().into_owned();
651        if let Ok(Some(key)) = crate::get_owner_signing_key(&vault_id, name) {
652            return Ok(key);
653        }
654        if !self.owner_signing_key_exists(name)? {
655            self.store_owner_signing_key_current_only(name, &OwnerSigningKeyPair::generate()?)?;
656        }
657        let key = self.load_owner_signing_key_existing(name)?;
658        let _ = crate::put_owner_signing_key(&vault_id, name, key.try_clone()?, None);
659        Ok(key)
660    }
661
662    /// Loads one owner-signing key using the enabled session-agent cache when
663    /// available, then refreshes that cache after the normal vault load.
664    pub fn load_owner_signing_key_cached(&self, name: &str) -> Result<OwnerSigningKeyPair> {
665        self.load_owner_signing_key(name)
666    }
667
668    /// Returns whether a private key exists under `name`.
669    pub fn private_key_exists(&self, name: &str) -> Result<bool> {
670        let lockbox = self.lockbox.borrow();
671        Ok(lockbox
672            .variable_sensitivity(&private_key_variable_name(name)?)?
673            .is_some())
674    }
675
676    /// Lists private-key names stored in this vault.
677    pub fn list_private_keys(&self) -> Result<Vec<String>> {
678        let mut names = Vec::new();
679        let lockbox = self.lockbox.borrow();
680        for (variable_name, _) in lockbox.list_variables()? {
681            let Some(name) = private_key_name_from_variable(&variable_name) else {
682                continue;
683            };
684            names.push(name?);
685        }
686        names.sort();
687        names.dedup();
688        Ok(names)
689    }
690
691    /// Deletes the private key stored under `name`, if present.
692    pub fn delete_private_key(&self, name: &str) -> Result<()> {
693        if let Some(history) = self.read_profile_history(name)? {
694            for generation in history.generations {
695                self.delete_secret_variable_record_if_exists(
696                    &private_key_generation_variable_name(name, generation.index)?,
697                )?;
698                self.delete_secret_variable_record_if_exists(
699                    &owner_signing_key_generation_variable_name(name, generation.index)?,
700                )?;
701            }
702            self.delete_record_if_exists(&profile_history_record_path(name)?)?;
703        }
704        self.delete_record_if_exists(&profile_email_record_path(name)?)?;
705        self.delete_secret_variable_record_if_exists(&owner_signing_key_variable_name(name)?)?;
706        self.delete_secret_variable_record_if_exists(&private_key_variable_name(name)?)?;
707        let vault_id = self.path.to_string_lossy().into_owned();
708        let _ = crate::forget_owner_signing_key(&vault_id, name);
709        Ok(())
710    }
711
712    /// Stores the public email address associated with a vault profile.
713    pub fn store_profile_email(&self, name: &str, email: &str) -> Result<()> {
714        if !self.private_key_exists(name)? {
715            return Err(Error::NotFound(format!("vault private key {name}")));
716        }
717        self.put_record_replace(
718            &profile_email_record_path(name)?,
719            &encode_profile_email(email),
720        )
721    }
722
723    /// Loads the public email address associated with a vault profile.
724    pub fn profile_email(&self, name: &str) -> Result<Option<String>> {
725        let path = profile_email_record_path(name)?;
726        {
727            let lockbox = self.lockbox.borrow();
728            if lockbox.stat(&path).is_none() {
729                return Ok(None);
730            }
731        }
732        decode_profile_email(&self.get_record(&path)?).map(Some)
733    }
734
735    /// Lists profile generations for a private key, creating generation one
736    /// for existing pre-history profiles.
737    pub fn list_profile_generations(&self, name: &str) -> Result<ProfileHistory> {
738        self.ensure_profile_history(name)
739    }
740
741    /// Rotates a vault profile to a new active key generation.
742    pub fn rotate_private_key(&self, name: &str) -> Result<ProfileHistory> {
743        let mut history = self.ensure_profile_history(name)?;
744        let now = unix_ms(SystemTime::now());
745        for generation in &mut history.generations {
746            if generation.status == ProfileGenerationStatus::Active {
747                generation.status = ProfileGenerationStatus::Retired;
748                generation.retired_at_unix_ms = Some(now);
749            }
750        }
751        let new_index = history
752            .generations
753            .iter()
754            .map(|generation| generation.index)
755            .max()
756            .unwrap_or(0)
757            .saturating_add(1);
758        let keypair = ContactKeyPair::generate()?;
759        let signing_key = OwnerSigningKeyPair::generate()?;
760        self.store_private_key_current_only(name, &keypair)?;
761        self.store_owner_signing_key_current_only(name, &signing_key)?;
762        self.store_private_key_generation(name, new_index, &keypair)?;
763        self.store_owner_signing_key_generation(name, new_index, &signing_key)?;
764        history.active_generation = new_index;
765        history.generations.push(ProfileGeneration {
766            index: new_index,
767            status: ProfileGenerationStatus::Active,
768            contact_fingerprint: contact_fingerprint(&keypair.public_key()),
769            created_at_unix_ms: now,
770            retired_at_unix_ms: None,
771        });
772        self.write_profile_history(&history)?;
773        let vault_id = self.path.to_string_lossy().into_owned();
774        let _ = crate::forget_owner_signing_key(&vault_id, name);
775        Ok(history)
776    }
777
778    /// Loads one profile generation by index.
779    pub fn load_private_key_generation(&self, name: &str, index: u16) -> Result<ContactKeyPair> {
780        let variable_name = private_key_generation_variable_name(name, index)?;
781        let secret = self
782            .lockbox
783            .borrow()
784            .with_secret_variable(&variable_name, SecretString::try_clone)?
785            .transpose()?
786            .ok_or_else(|| {
787                Error::NotFound(format!("vault private key {name} generation {index}"))
788            })?;
789        let mut bytes = SecretVec::new();
790        secret.append_to_secure_vec(&mut bytes)?;
791        import_private_key(bytes)
792    }
793
794    /// Loads one owner signing-key generation by index.
795    pub fn load_owner_signing_key_generation(
796        &self,
797        name: &str,
798        index: u16,
799    ) -> Result<OwnerSigningKeyPair> {
800        let variable_name = owner_signing_key_generation_variable_name(name, index)?;
801        let secret = self
802            .lockbox
803            .borrow()
804            .with_secret_variable(&variable_name, SecretString::try_clone)?
805            .transpose()?
806            .ok_or_else(|| {
807                Error::NotFound(format!("vault owner signing key {name} generation {index}"))
808            })?;
809        let mut bytes = SecretVec::new();
810        secret.append_to_secure_vec(&mut bytes)?;
811        decode_hex_secret_in_place(&mut bytes)?;
812        OwnerSigningKeyPair::from_private_key_record(bytes)
813    }
814
815    /// Stores a contact public key under `name`.
816    ///
817    /// Names must contain only ASCII letters, digits, `-`, or `_`.
818    pub fn store_contact(&self, name: &str, key: &ContactPublicKey) -> Result<()> {
819        self.put_record(&contact_record_path(name)?, &key.to_bytes())
820    }
821
822    /// Stores the contact signing public key associated with a contact.
823    pub fn store_contact_signing_key(&self, name: &str, key: &OwnerSigningPublicKey) -> Result<()> {
824        self.put_record_replace(&contact_signing_record_path(name)?, &key.to_bytes())
825    }
826
827    /// Loads a contact public key by name.
828    pub fn load_contact(&self, name: &str) -> Result<ContactPublicKey> {
829        ContactPublicKey::from_bytes(&self.get_record(&contact_record_path(name)?)?)
830    }
831
832    /// Loads the contact signing public key associated with a contact.
833    pub fn load_contact_signing_key(&self, name: &str) -> Result<OwnerSigningPublicKey> {
834        OwnerSigningPublicKey::from_bytes(&self.get_record(&contact_signing_record_path(name)?)?)
835    }
836
837    /// Returns whether a contact exists under `name`.
838    pub fn contact_exists(&self, name: &str) -> Result<bool> {
839        Ok(self
840            .lockbox
841            .borrow()
842            .stat(&contact_record_path(name)?)
843            .is_some())
844    }
845
846    /// Deletes the contact stored under `name`, if present.
847    pub fn delete_contact(&self, name: &str) -> Result<()> {
848        self.delete_record_if_exists(&contact_signing_record_path(name)?)?;
849        self.delete_record_if_exists(&contact_record_path(name)?)
850    }
851
852    /// Lists contacts stored in this vault.
853    pub fn list_contacts(&self) -> Result<Vec<StoredContact>> {
854        let mut out = Vec::new();
855        for name in self.list_record_names("/contacts", ".pub")? {
856            if name.ends_with(".signing") {
857                continue;
858            }
859            out.push(StoredContact {
860                key: self.load_contact(&name)?,
861                name,
862            });
863        }
864        Ok(out)
865    }
866
867    /// Stores an exported key-directory backup for `lockbox_id`.
868    ///
869    /// Backups can be used by `Vault` to recover openability when the
870    /// embedded key directory in a lockbox file is damaged.
871    pub fn store_key_directory_backup(
872        &self,
873        lockbox_id: LockboxId,
874        key_directory: &[u8],
875    ) -> Result<()> {
876        self.put_record_replace(
877            &key_directory_backup_record_path(lockbox_id)?,
878            key_directory,
879        )
880    }
881
882    /// Loads the key-directory backup for `lockbox_id`.
883    pub fn load_key_directory_backup(&self, lockbox_id: LockboxId) -> Result<Vec<u8>> {
884        self.get_record(&key_directory_backup_record_path(lockbox_id)?)
885    }
886
887    /// Counts key-directory backups stored in this vault.
888    pub fn key_directory_backup_count(&self) -> Result<usize> {
889        Ok(self
890            .lockbox
891            .borrow()
892            .list(recursive_list("/key_directories")?)?
893            .filter_map(Result::ok)
894            .filter(|entry| entry.kind == LockboxEntryKind::File)
895            .count())
896    }
897
898    /// Remembers a lockbox path for diagnostics and future bulk access refresh.
899    pub fn remember_known_lockbox(
900        &self,
901        lockbox_id: LockboxId,
902        path: impl AsRef<Path>,
903    ) -> Result<()> {
904        let path = path.as_ref().to_string_lossy().to_string();
905        let record = KnownLockbox {
906            lockbox_id,
907            path,
908            last_seen_unix_ms: unix_ms(SystemTime::now()),
909        };
910        self.put_record_replace(
911            &known_lockbox_record_path(record.path.as_str())?,
912            &encode_known_lockbox(&record),
913        )
914    }
915
916    /// Restores a known-lockbox record without replacing its source timestamp.
917    #[doc(hidden)]
918    pub fn restore_known_lockbox(&self, record: KnownLockbox) -> Result<()> {
919        self.put_record_replace(
920            &known_lockbox_record_path(record.path.as_str())?,
921            &encode_known_lockbox(&record),
922        )
923    }
924
925    /// Lists lockboxes remembered by the local vault.
926    pub fn list_known_lockboxes(&self) -> Result<Vec<KnownLockbox>> {
927        let mut out = Vec::new();
928        for name in self.list_record_names("/known_lockboxes", ".lkl")? {
929            let path = LockboxPath::new(format!("/known_lockboxes/{name}.lkl"))?;
930            out.push(decode_known_lockbox(&self.get_record(&path)?)?);
931        }
932        out.sort_by(|left, right| left.path.cmp(&right.path));
933        Ok(out)
934    }
935
936    /// Removes one remembered lockbox path. The lockbox file itself is not
937    /// deleted or modified.
938    pub fn forget_known_lockbox(&self, path: impl AsRef<Path>) -> Result<()> {
939        self.delete_record_if_exists(&known_lockbox_record_path(path.as_ref())?)
940    }
941
942    /// Remember a local name for one lockbox access slot.
943    ///
944    /// This mapping is stored only inside the encrypted local vault. It is not
945    /// written to the shared lockbox, so it does not disclose contacts to
946    /// third parties who inspect the lockbox file.
947    pub fn remember_access_slot_label(
948        &self,
949        lockbox_id: LockboxId,
950        slot_id: u64,
951        name: impl Into<String>,
952    ) -> Result<()> {
953        let label = AccessSlotLabel {
954            lockbox_id,
955            slot_id,
956            name: name.into(),
957            updated_at_unix_ms: unix_ms(SystemTime::now()),
958        };
959        self.put_record_replace(
960            &access_slot_label_record_path(lockbox_id, slot_id)?,
961            &encode_access_slot_label(&label),
962        )
963    }
964
965    /// Restores an access-slot label without replacing its source timestamp.
966    #[doc(hidden)]
967    pub fn restore_access_slot_label(&self, label: AccessSlotLabel) -> Result<()> {
968        self.put_record_replace(
969            &access_slot_label_record_path(label.lockbox_id, label.slot_id)?,
970            &encode_access_slot_label(&label),
971        )
972    }
973
974    /// Lists local access-slot labels remembered for one lockbox.
975    pub fn list_access_slot_labels(&self, lockbox_id: LockboxId) -> Result<Vec<AccessSlotLabel>> {
976        let root = access_slot_label_root(lockbox_id);
977        let mut out = Vec::new();
978        for slot_id in self.list_record_names(&root, ".lbas")? {
979            let slot_id = slot_id.parse::<u64>().map_err(|_| {
980                Error::CorruptVaultRecord(format!("access slot label id {slot_id} is not numeric"))
981            })?;
982            out.push(decode_access_slot_label(&self.get_record(
983                &access_slot_label_record_path(lockbox_id, slot_id)?,
984            )?)?);
985        }
986        out.sort_by(|left, right| {
987            left.name
988                .cmp(&right.name)
989                .then_with(|| left.slot_id.cmp(&right.slot_id))
990        });
991        Ok(out)
992    }
993
994    /// Finds local slot labels for `name` in one lockbox.
995    pub fn find_access_slot_labels(
996        &self,
997        lockbox_id: LockboxId,
998        name: &str,
999    ) -> Result<Vec<AccessSlotLabel>> {
1000        Ok(self
1001            .list_access_slot_labels(lockbox_id)?
1002            .into_iter()
1003            .filter(|label| label.name == name)
1004            .collect())
1005    }
1006
1007    /// Forget one local access-slot label.
1008    pub fn forget_access_slot_label(&self, lockbox_id: LockboxId, slot_id: u64) -> Result<()> {
1009        self.delete_record_if_exists(&access_slot_label_record_path(lockbox_id, slot_id)?)
1010    }
1011
1012    /// Creates or revises a reusable form definition stored in the vault.
1013    pub fn define_form(
1014        &self,
1015        alias: &str,
1016        name: &str,
1017        fields: Vec<FormFieldDefinition>,
1018    ) -> Result<FormDefinition> {
1019        let _guard = VaultFileLock::acquire(&self.path)?;
1020        let mut lockbox = self.lockbox.borrow_mut();
1021        let definition = lockbox.define_form(alias, name, fields)?;
1022        lockbox.commit()?;
1023        set_private_file_permissions(&self.path)?;
1024        Ok(definition)
1025    }
1026
1027    /// Creates or revises a reusable form definition stored in the vault.
1028    pub fn define_form_with_description(
1029        &self,
1030        alias: &str,
1031        name: &str,
1032        description: &str,
1033        fields: Vec<FormFieldDefinition>,
1034    ) -> Result<FormDefinition> {
1035        let _guard = VaultFileLock::acquire(&self.path)?;
1036        let mut lockbox = self.lockbox.borrow_mut();
1037        let definition = lockbox.define_form_with_description(alias, name, description, fields)?;
1038        lockbox.commit()?;
1039        set_private_file_permissions(&self.path)?;
1040        Ok(definition)
1041    }
1042
1043    /// Creates or revises a reusable form definition with a stable definition id.
1044    pub fn define_form_with_type_id(
1045        &self,
1046        type_id: FormTypeId,
1047        alias: &str,
1048        name: &str,
1049        fields: Vec<FormFieldDefinition>,
1050    ) -> Result<FormDefinition> {
1051        let _guard = VaultFileLock::acquire(&self.path)?;
1052        let mut lockbox = self.lockbox.borrow_mut();
1053        let definition = lockbox.define_form_with_type_id(type_id, alias, name, fields)?;
1054        lockbox.commit()?;
1055        set_private_file_permissions(&self.path)?;
1056        Ok(definition)
1057    }
1058
1059    /// Creates or revises a reusable form definition with a stable definition id.
1060    pub fn define_form_with_type_id_and_description(
1061        &self,
1062        type_id: FormTypeId,
1063        alias: &str,
1064        name: &str,
1065        description: &str,
1066        fields: Vec<FormFieldDefinition>,
1067    ) -> Result<FormDefinition> {
1068        let _guard = VaultFileLock::acquire(&self.path)?;
1069        let mut lockbox = self.lockbox.borrow_mut();
1070        let definition = lockbox.define_form_with_type_id_and_description(
1071            type_id,
1072            alias,
1073            name,
1074            description,
1075            fields,
1076        )?;
1077        lockbox.commit()?;
1078        set_private_file_permissions(&self.path)?;
1079        Ok(definition)
1080    }
1081
1082    /// Imports an exact reusable form definition into the vault.
1083    pub fn import_form_definition(&self, definition: FormDefinition) -> Result<FormDefinition> {
1084        let _guard = VaultFileLock::acquire(&self.path)?;
1085        let mut lockbox = self.lockbox.borrow_mut();
1086        let definition = lockbox.import_form_definition(definition)?;
1087        lockbox.commit()?;
1088        set_private_file_permissions(&self.path)?;
1089        Ok(definition)
1090    }
1091
1092    /// Resolves a reusable vault form definition by alias or definition id.
1093    pub fn resolve_form_definition(&self, reference: &str) -> Result<FormDefinition> {
1094        self.lockbox.borrow().resolve_form_definition(reference)
1095    }
1096
1097    /// Lists reusable form definitions stored in the vault.
1098    pub fn list_form_definitions(&self) -> Result<Vec<FormDefinition>> {
1099        self.lockbox.borrow().list_form_definitions()
1100    }
1101
1102    /// Lists every stored revision of one reusable form definition.
1103    pub fn list_form_definition_revisions(
1104        &self,
1105        type_id: &FormTypeId,
1106    ) -> Result<Vec<FormDefinition>> {
1107        self.lockbox
1108            .borrow()
1109            .list_form_definition_revisions(type_id)
1110    }
1111
1112    /// Adds built-in form definitions that are not already present.
1113    pub fn seed_default_form_definitions(&self) -> Result<usize> {
1114        let existing = self
1115            .list_form_definitions()?
1116            .into_iter()
1117            .map(|definition| definition.alias)
1118            .collect::<Vec<_>>();
1119        let mut seeded = 0usize;
1120        for template in default_form_templates() {
1121            if existing.iter().any(|alias| alias == template.alias) {
1122                continue;
1123            }
1124            self.define_form_with_type_id(
1125                FormTypeId::new(template.type_id)?,
1126                template.alias,
1127                template.name,
1128                template.fields,
1129            )?;
1130            seeded += 1;
1131        }
1132        Ok(seeded)
1133    }
1134
1135    /// Stores a lockbox pass phrase in the vault, keyed by lockbox id.
1136    pub fn remember_lockbox_password(
1137        &self,
1138        lockbox_id: LockboxId,
1139        password: &SecretString,
1140    ) -> Result<()> {
1141        self.put_secret_variable_record(&lockbox_password_variable_name(lockbox_id)?, password)
1142    }
1143
1144    /// Loads a remembered lockbox pass phrase, if one exists for this lockbox id.
1145    pub fn remembered_lockbox_password(
1146        &self,
1147        lockbox_id: LockboxId,
1148    ) -> Result<Option<SecretString>> {
1149        Ok(self
1150            .lockbox
1151            .borrow()
1152            .with_secret_variable(
1153                &lockbox_password_variable_name(lockbox_id)?,
1154                SecretString::try_clone,
1155            )?
1156            .transpose()?)
1157    }
1158
1159    fn put_record(&self, path: &LockboxPath, bytes: &[u8]) -> Result<()> {
1160        self.put_record_with_replace(path, bytes, false)
1161    }
1162
1163    fn put_record_replace(&self, path: &LockboxPath, bytes: &[u8]) -> Result<()> {
1164        self.put_record_with_replace(path, bytes, true)
1165    }
1166
1167    fn put_record_with_replace(
1168        &self,
1169        path: &LockboxPath,
1170        bytes: &[u8],
1171        replace: bool,
1172    ) -> Result<()> {
1173        let _guard = VaultFileLock::acquire(&self.path)?;
1174        let mut lockbox = self.lockbox.borrow_mut();
1175        let replace = replace && lockbox.stat(path).is_some();
1176        lockbox.create_parent_dirs_for(path)?;
1177        lockbox.add_file(path, bytes, replace)?;
1178        lockbox.commit()?;
1179        set_private_file_permissions(&self.path)?;
1180        Ok(())
1181    }
1182
1183    fn put_secret_variable_record(&self, name: &VariableName, value: &SecretString) -> Result<()> {
1184        let _guard = VaultFileLock::acquire(&self.path)?;
1185        let mut lockbox = self.lockbox.borrow_mut();
1186        lockbox.set_secret_variable(name, value)?;
1187        lockbox.commit()?;
1188        set_private_file_permissions(&self.path)?;
1189        Ok(())
1190    }
1191
1192    fn get_record(&self, path: &LockboxPath) -> Result<Vec<u8>> {
1193        self.lockbox.borrow().get_file(path)
1194    }
1195
1196    fn delete_record_if_exists(&self, path: &LockboxPath) -> Result<()> {
1197        let _guard = VaultFileLock::acquire(&self.path)?;
1198        let mut lockbox = self.lockbox.borrow_mut();
1199        if lockbox.stat(path).is_some() {
1200            lockbox.delete(path)?;
1201            lockbox.commit()?;
1202            set_private_file_permissions(&self.path)?;
1203        }
1204        Ok(())
1205    }
1206
1207    fn delete_secret_variable_record_if_exists(&self, name: &VariableName) -> Result<()> {
1208        let _guard = VaultFileLock::acquire(&self.path)?;
1209        let mut lockbox = self.lockbox.borrow_mut();
1210        if lockbox.variable_sensitivity(name)?.is_some() {
1211            lockbox.delete_variable(name)?;
1212            lockbox.commit()?;
1213            set_private_file_permissions(&self.path)?;
1214        }
1215        Ok(())
1216    }
1217
1218    fn list_record_names(&self, root: &str, extension: &str) -> Result<Vec<String>> {
1219        let mut out = Vec::new();
1220        for entry in self.lockbox.borrow().list(recursive_list(root)?)? {
1221            let entry = entry?;
1222            if entry.kind != LockboxEntryKind::File || !entry.path.ends_with(extension) {
1223                continue;
1224            }
1225            let name = entry
1226                .path
1227                .rsplit('/')
1228                .next()
1229                .and_then(|file| file.strip_suffix(extension))
1230                .ok_or_else(|| {
1231                    Error::CorruptVaultRecord(format!(
1232                        "record path {} does not end with expected extension {extension}",
1233                        entry.path
1234                    ))
1235                })?;
1236            out.push(name.to_string());
1237        }
1238        out.sort();
1239        Ok(out)
1240    }
1241
1242    fn ensure_structure_version(&self, initialize_missing: bool) -> Result<()> {
1243        match self.read_structure_version()? {
1244            Some(CURRENT_VAULT_STRUCTURE_VERSION) => Ok(()),
1245            Some(version) => Err(Error::UnsupportedFormatVersion {
1246                artifact: ArtifactKind::Vault,
1247                found: version,
1248                supported: CURRENT_VAULT_STRUCTURE_VERSION,
1249            }),
1250            None if initialize_missing => self.write_structure_version(CURRENT_VAULT_STRUCTURE_VERSION),
1251            None => Err(Error::Configuration(
1252                "local vault structure version is missing; recreate the vault with this reVault build"
1253                    .to_string(),
1254            )),
1255        }
1256    }
1257
1258    fn read_structure_version(&self) -> Result<Option<u32>> {
1259        let path = vault_structure_version_record_path()?;
1260        {
1261            let lockbox = self.lockbox.borrow();
1262            if lockbox.stat(&path).is_none() {
1263                return Ok(None);
1264            }
1265        }
1266        decode_structure_version(&self.get_record(&path)?).map(Some)
1267    }
1268
1269    fn write_structure_version(&self, version: u32) -> Result<()> {
1270        let bytes = format!("{version}\n");
1271        self.put_record_replace(&vault_structure_version_record_path()?, bytes.as_bytes())
1272    }
1273
1274    fn store_private_key_current_only(&self, name: &str, keypair: &ContactKeyPair) -> Result<()> {
1275        let private_record = export_private_key(keypair, KeyFormat::RawHex)?;
1276        let value = SecretString::from_secure_vec(private_record);
1277        self.put_secret_variable_record(&private_key_variable_name(name)?, &value)
1278    }
1279
1280    fn owner_signing_key_exists(&self, name: &str) -> Result<bool> {
1281        let lockbox = self.lockbox.borrow();
1282        Ok(lockbox
1283            .variable_sensitivity(&owner_signing_key_variable_name(name)?)?
1284            .is_some())
1285    }
1286
1287    fn load_owner_signing_key_existing(&self, name: &str) -> Result<OwnerSigningKeyPair> {
1288        load_owner_signing_key_existing_from_lockbox(&self.lockbox.borrow(), name)
1289    }
1290
1291    fn store_owner_signing_key_current_only(
1292        &self,
1293        name: &str,
1294        keypair: &OwnerSigningKeyPair,
1295    ) -> Result<()> {
1296        let value =
1297            SecretString::from_secure_vec(hex_encode_secret(keypair.private_key_record()?)?);
1298        self.put_secret_variable_record(&owner_signing_key_variable_name(name)?, &value)
1299    }
1300
1301    fn store_private_key_generation(
1302        &self,
1303        name: &str,
1304        index: u16,
1305        keypair: &ContactKeyPair,
1306    ) -> Result<()> {
1307        let private_record = export_private_key(keypair, KeyFormat::RawHex)?;
1308        let value = SecretString::from_secure_vec(private_record);
1309        self.put_secret_variable_record(&private_key_generation_variable_name(name, index)?, &value)
1310    }
1311
1312    fn store_owner_signing_key_generation(
1313        &self,
1314        name: &str,
1315        index: u16,
1316        keypair: &OwnerSigningKeyPair,
1317    ) -> Result<()> {
1318        let value =
1319            SecretString::from_secure_vec(hex_encode_secret(keypair.private_key_record()?)?);
1320        self.put_secret_variable_record(
1321            &owner_signing_key_generation_variable_name(name, index)?,
1322            &value,
1323        )
1324    }
1325
1326    fn ensure_profile_history(&self, name: &str) -> Result<ProfileHistory> {
1327        if let Some(history) = self.read_profile_history(name)? {
1328            return Ok(history);
1329        }
1330        let keypair = self.load_private_key(name)?;
1331        self.store_private_key_generation(name, 1, &keypair)?;
1332        let signing_key = self.load_owner_signing_key(name)?;
1333        self.store_owner_signing_key_generation(name, 1, &signing_key)?;
1334        let history = ProfileHistory {
1335            name: name.to_string(),
1336            active_generation: 1,
1337            generations: vec![ProfileGeneration {
1338                index: 1,
1339                status: ProfileGenerationStatus::Active,
1340                contact_fingerprint: contact_fingerprint(&keypair.public_key()),
1341                created_at_unix_ms: unix_ms(SystemTime::now()),
1342                retired_at_unix_ms: None,
1343            }],
1344        };
1345        self.write_profile_history(&history)?;
1346        Ok(history)
1347    }
1348
1349    fn read_profile_history(&self, name: &str) -> Result<Option<ProfileHistory>> {
1350        let path = profile_history_record_path(name)?;
1351        {
1352            let lockbox = self.lockbox.borrow();
1353            if lockbox.stat(&path).is_none() {
1354                return Ok(None);
1355            }
1356        }
1357        decode_profile_history(name, &self.get_record(&path)?).map(Some)
1358    }
1359
1360    fn write_profile_history(&self, history: &ProfileHistory) -> Result<()> {
1361        self.put_record_replace(
1362            &profile_history_record_path(&history.name)?,
1363            &encode_profile_history(history),
1364        )
1365    }
1366}
1367
1368/// Writes a consistent encrypted backup archive for the default local vault.
1369///
1370/// The archive contains the raw encrypted `local-vault.lbox` bytes plus a JSON
1371/// manifest and checksum. It does not decrypt or export vault records.
1372pub fn backup_default_vault(
1373    output: impl AsRef<Path>,
1374    overwrite: bool,
1375) -> Result<VaultBackupManifest> {
1376    let root = default_vault_dir()?;
1377    let path = root.join(VAULT_FILE_NAME);
1378    if !path.exists() {
1379        return Err(Error::VaultUnavailable(
1380            "local vault is not initialized; run `lockbox vault init` first".to_string(),
1381        ));
1382    }
1383    let _guard = VaultFileLock::acquire(&path)?;
1384    let vault_bytes = fs::read(&path).map_err(|err| Error::Io(err.to_string()))?;
1385    let digest: [u8; 32] = Sha256::digest(&vault_bytes).into();
1386    let manifest = VaultBackupManifest {
1387        format_version: 1,
1388        created_at_unix_ms: unix_ms(SystemTime::now()),
1389        vault_file_name: VAULT_FILE_NAME.to_string(),
1390        vault_size: vault_bytes.len() as u64,
1391        vault_sha256: crate::encode_hex(&digest),
1392    };
1393    write_vault_backup_archive(output.as_ref(), overwrite, &manifest, &vault_bytes)?;
1394    Ok(manifest)
1395}
1396
1397/// Restores the default local vault from an encrypted backup archive.
1398///
1399/// The archive checksum is verified before the existing vault file is replaced.
1400pub fn restore_default_vault(
1401    input: impl AsRef<Path>,
1402    overwrite: bool,
1403) -> Result<VaultBackupManifest> {
1404    let (manifest, vault_bytes) = read_vault_backup_archive(input.as_ref())?;
1405    let root = default_vault_dir()?;
1406    create_private_dir(&root)?;
1407    let path = root.join(VAULT_FILE_NAME);
1408    let _guard = VaultFileLock::acquire(&path)?;
1409    if path.exists() && !overwrite {
1410        return Err(Error::AlreadyExists(format!(
1411            "{}; pass --overwrite to replace it",
1412            path.display()
1413        )));
1414    }
1415    let tmp = root.join("local-vault.lbox.restore.tmp");
1416    fs::write(&tmp, vault_bytes).map_err(|err| Error::Io(err.to_string()))?;
1417    set_private_file_permissions(&tmp)?;
1418    if path.exists() {
1419        fs::remove_file(&path).map_err(|err| Error::Io(err.to_string()))?;
1420    }
1421    fs::rename(&tmp, &path).map_err(|err| Error::Io(err.to_string()))?;
1422    set_private_file_permissions(&path)?;
1423    Ok(manifest)
1424}
1425
1426fn write_vault_backup_archive(
1427    output: &Path,
1428    overwrite: bool,
1429    manifest: &VaultBackupManifest,
1430    vault_bytes: &[u8],
1431) -> Result<()> {
1432    if output.exists() && !overwrite {
1433        return Err(Error::AlreadyExists(format!(
1434            "{}; pass --overwrite to replace it",
1435            output.display()
1436        )));
1437    }
1438    let manifest_bytes = serde_json::to_vec(manifest).map_err(|err| Error::Io(err.to_string()))?;
1439    let mut options = OpenOptions::new();
1440    options.write(true).create(true).truncate(true);
1441    if !overwrite {
1442        options.create_new(true);
1443    }
1444    let mut file = options
1445        .open(output)
1446        .map_err(|err| Error::Io(err.to_string()))?;
1447    file.write_all(VAULT_BACKUP_MAGIC)
1448        .map_err(|err| Error::Io(err.to_string()))?;
1449    file.write_all(&(manifest_bytes.len() as u64).to_be_bytes())
1450        .map_err(|err| Error::Io(err.to_string()))?;
1451    file.write_all(&manifest_bytes)
1452        .map_err(|err| Error::Io(err.to_string()))?;
1453    file.write_all(vault_bytes)
1454        .map_err(|err| Error::Io(err.to_string()))?;
1455    file.sync_all().map_err(|err| Error::Io(err.to_string()))
1456}
1457
1458fn read_vault_backup_archive(input: &Path) -> Result<(VaultBackupManifest, Vec<u8>)> {
1459    let mut file = File::open(input).map_err(|err| Error::Io(err.to_string()))?;
1460    let mut magic = [0u8; 8];
1461    file.read_exact(&mut magic)
1462        .map_err(|err| Error::Io(err.to_string()))?;
1463    if &magic != VAULT_BACKUP_MAGIC {
1464        return Err(Error::InvalidInput(
1465            "backup file is not a reVault vault backup archive".to_string(),
1466        ));
1467    }
1468    let mut len = [0u8; 8];
1469    file.read_exact(&mut len)
1470        .map_err(|err| Error::Io(err.to_string()))?;
1471    let manifest_len = u64::from_be_bytes(len);
1472    if manifest_len > 1024 * 1024 {
1473        return Err(Error::SecurityLimitExceeded(
1474            "vault backup manifest is too large".to_string(),
1475        ));
1476    }
1477    let mut manifest_bytes = vec![0u8; manifest_len as usize];
1478    file.read_exact(&mut manifest_bytes)
1479        .map_err(|err| Error::Io(err.to_string()))?;
1480    let manifest: VaultBackupManifest = serde_json::from_slice(&manifest_bytes)
1481        .map_err(|err| Error::InvalidInput(err.to_string()))?;
1482    if manifest.format_version != 1 {
1483        return Err(Error::InvalidInput(format!(
1484            "vault backup format version {} is not supported",
1485            manifest.format_version
1486        )));
1487    }
1488    if manifest.vault_file_name != VAULT_FILE_NAME {
1489        return Err(Error::InvalidInput(format!(
1490            "vault backup contains unexpected file {}",
1491            manifest.vault_file_name
1492        )));
1493    }
1494    let mut vault_bytes = Vec::new();
1495    file.read_to_end(&mut vault_bytes)
1496        .map_err(|err| Error::Io(err.to_string()))?;
1497    if vault_bytes.len() as u64 != manifest.vault_size {
1498        return Err(Error::InvalidInput(
1499            "vault backup size does not match manifest".to_string(),
1500        ));
1501    }
1502    let digest: [u8; 32] = Sha256::digest(&vault_bytes).into();
1503    if crate::encode_hex(&digest) != manifest.vault_sha256 {
1504        return Err(Error::InvalidInput(
1505            "vault backup checksum does not match manifest".to_string(),
1506        ));
1507    }
1508    Ok((manifest, vault_bytes))
1509}
1510
1511struct VaultFileLock {
1512    lock: Option<ScopedFileLock>,
1513    active: bool,
1514}
1515
1516impl VaultFileLock {
1517    fn acquire(path: &Path) -> Result<Self> {
1518        let nested = VAULT_LOCK_DEPTH.with(|depth| {
1519            let value = depth.get();
1520            depth.set(value.saturating_add(1));
1521            value > 0
1522        });
1523        if nested {
1524            return Ok(Self {
1525                lock: None,
1526                active: true,
1527            });
1528        }
1529        let lock = ScopedFileLock::acquire(path, FileLockScope::Vault).inspect_err(|_| {
1530            VAULT_LOCK_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
1531        })?;
1532        Ok(Self {
1533            lock: Some(lock),
1534            active: true,
1535        })
1536    }
1537}
1538
1539impl Drop for VaultFileLock {
1540    fn drop(&mut self) {
1541        if !self.active {
1542            return;
1543        }
1544        self.lock.take();
1545        VAULT_LOCK_DEPTH.with(|depth| depth.set(depth.get().saturating_sub(1)));
1546    }
1547}
1548
1549fn recursive_list(path: &str) -> Result<ListOptions> {
1550    let path = LockboxPath::new(path)?;
1551    let mut options = ListOptions::new(&path);
1552    options.recursive = true;
1553    Ok(options)
1554}
1555
1556fn list_read_only_record_names(
1557    lockbox: &RefCell<Lockbox<ReadOnly>>,
1558    root: &str,
1559    extension: &str,
1560) -> Result<Vec<String>> {
1561    let mut out = Vec::new();
1562    for entry in lockbox.borrow().list(recursive_list(root)?)? {
1563        let entry = entry?;
1564        if entry.kind != LockboxEntryKind::File || !entry.path.ends_with(extension) {
1565            continue;
1566        }
1567        let name = entry
1568            .path
1569            .rsplit('/')
1570            .next()
1571            .and_then(|file| file.strip_suffix(extension))
1572            .ok_or_else(|| {
1573                Error::CorruptVaultRecord(format!(
1574                    "record path {} does not end with expected extension {extension}",
1575                    entry.path
1576                ))
1577            })?;
1578        out.push(name.to_string());
1579    }
1580    out.sort();
1581    Ok(out)
1582}
1583
1584fn private_key_variable_name(name: &str) -> Result<VariableName> {
1585    let name = validate_record_name(name)?;
1586    VariableName::new(format!(
1587        "LOCKBOX_VAULT_PRIVATE_KEY_{}",
1588        encode_name_hex(name)
1589    ))
1590}
1591
1592fn private_key_generation_variable_name(name: &str, index: u16) -> Result<VariableName> {
1593    let name = validate_record_name(name)?;
1594    VariableName::new(format!(
1595        "LOCKBOX_VAULT_PRIVATE_KEY_{}_GEN_{index:04}",
1596        encode_name_hex(name)
1597    ))
1598}
1599
1600fn open_vault_lockbox_for_write(path: &Path, password: &SecretString) -> Result<Lockbox> {
1601    Lockbox::open_for_write_with_signing_key_assuming_locked(
1602        path,
1603        LockboxOpen::Password(password),
1604        |lockbox| {
1605            load_owner_signing_key_existing_from_lockbox(lockbox, VaultDirectory::DEFAULT_KEY_NAME)
1606                .or_else(|err| match err {
1607                    Error::NotFound(_) => OwnerSigningKeyPair::generate(),
1608                    other => Err(other),
1609                })
1610        },
1611    )
1612}
1613
1614fn load_owner_signing_key_existing_from_lockbox<State>(
1615    lockbox: &Lockbox<State>,
1616    name: &str,
1617) -> Result<OwnerSigningKeyPair> {
1618    let secret = lockbox
1619        .with_secret_variable(
1620            &owner_signing_key_variable_name(name)?,
1621            SecretString::try_clone,
1622        )?
1623        .transpose()?
1624        .ok_or_else(|| Error::NotFound(format!("vault owner signing key {name}")))?;
1625    let mut bytes = SecretVec::new();
1626    secret.append_to_secure_vec(&mut bytes)?;
1627    decode_hex_secret_in_place(&mut bytes)?;
1628    OwnerSigningKeyPair::from_private_key_record(bytes)
1629}
1630
1631fn owner_signing_key_variable_name(name: &str) -> Result<VariableName> {
1632    let name = validate_record_name(name)?;
1633    VariableName::new(format!(
1634        "LOCKBOX_VAULT_SIGNING_KEY_{}",
1635        encode_name_hex(name)
1636    ))
1637}
1638
1639fn owner_signing_key_generation_variable_name(name: &str, index: u16) -> Result<VariableName> {
1640    let name = validate_record_name(name)?;
1641    VariableName::new(format!(
1642        "LOCKBOX_VAULT_SIGNING_KEY_{}_GEN_{index:04}",
1643        encode_name_hex(name)
1644    ))
1645}
1646
1647fn private_key_name_from_variable(name: &str) -> Option<Result<String>> {
1648    let name = name.strip_prefix('/').unwrap_or(name);
1649    let hex = name.strip_prefix("LOCKBOX_VAULT_PRIVATE_KEY_")?;
1650    if hex.contains("_GEN_") {
1651        return None;
1652    }
1653    Some(decode_name_hex(hex).ok_or_else(|| {
1654        Error::CorruptVaultRecord(format!("private key record name is not valid hex: {name}"))
1655    }))
1656}
1657
1658struct DefaultFormTemplate {
1659    type_id: &'static str,
1660    alias: &'static str,
1661    name: &'static str,
1662    fields: Vec<FormFieldDefinition>,
1663}
1664
1665fn default_form_templates() -> Vec<DefaultFormTemplate> {
1666    vec![
1667        DefaultFormTemplate {
1668            type_id: "00000000-0000-4000-8000-000000000001",
1669            alias: "login",
1670            name: "Login",
1671            fields: vec![
1672                form_field("username", "Username", FormFieldKind::Text, false),
1673                form_field("password", "Password", FormFieldKind::Secret, true),
1674                form_field("url", "Website", FormFieldKind::Url, false),
1675                form_field("notes", "Notes", FormFieldKind::Notes, false),
1676            ],
1677        },
1678        DefaultFormTemplate {
1679            type_id: "00000000-0000-4000-8000-000000000002",
1680            alias: "payment-card",
1681            name: "Payment Card",
1682            fields: vec![
1683                form_field("cardholder", "Cardholder", FormFieldKind::Text, true),
1684                form_field("number", "Card number", FormFieldKind::Secret, true),
1685                form_field("expiry", "Expiry", FormFieldKind::Month, false),
1686                form_field("cvv", "CVV", FormFieldKind::Secret, false),
1687                form_field("pin", "PIN", FormFieldKind::Secret, false),
1688                form_field("notes", "Notes", FormFieldKind::Notes, false),
1689            ],
1690        },
1691        DefaultFormTemplate {
1692            type_id: "00000000-0000-4000-8000-000000000003",
1693            alias: "bank-account",
1694            name: "Bank Account",
1695            fields: vec![
1696                form_field("bank", "Bank", FormFieldKind::Text, false),
1697                form_field("account_name", "Account name", FormFieldKind::Text, false),
1698                form_field("bsb", "BSB / routing", FormFieldKind::Text, false),
1699                form_field(
1700                    "account_number",
1701                    "Account number",
1702                    FormFieldKind::Secret,
1703                    true,
1704                ),
1705                form_field("iban", "IBAN", FormFieldKind::Secret, false),
1706                form_field("swift", "SWIFT / BIC", FormFieldKind::Text, false),
1707                form_field("notes", "Notes", FormFieldKind::Notes, false),
1708            ],
1709        },
1710        DefaultFormTemplate {
1711            type_id: "00000000-0000-4000-8000-000000000004",
1712            alias: "profile",
1713            name: "Profile Document",
1714            fields: vec![
1715                form_field("full_name", "Full name", FormFieldKind::Text, true),
1716                form_field("date_of_birth", "Date of birth", FormFieldKind::Date, false),
1717                form_field("email", "Email", FormFieldKind::Email, false),
1718                form_field("phone", "Phone", FormFieldKind::Text, false),
1719                form_field(
1720                    "document_number",
1721                    "Document number",
1722                    FormFieldKind::Secret,
1723                    false,
1724                ),
1725                form_field("expiry", "Expiry", FormFieldKind::Date, false),
1726                form_field("address", "Address", FormFieldKind::Notes, false),
1727                form_field("notes", "Notes", FormFieldKind::Notes, false),
1728            ],
1729        },
1730        DefaultFormTemplate {
1731            type_id: "00000000-0000-4000-8000-000000000005",
1732            alias: "server",
1733            name: "Server",
1734            fields: vec![
1735                form_field("host", "Host", FormFieldKind::Text, true),
1736                form_field("port", "Port", FormFieldKind::Number, false),
1737                form_field("username", "Username", FormFieldKind::Text, false),
1738                form_field("password", "Password", FormFieldKind::Secret, false),
1739                form_field("url", "URL", FormFieldKind::Url, false),
1740                form_field("ssh_key", "SSH key", FormFieldKind::Secret, false),
1741                form_field("notes", "Notes", FormFieldKind::Notes, false),
1742            ],
1743        },
1744        DefaultFormTemplate {
1745            type_id: "00000000-0000-4000-8000-000000000006",
1746            alias: "wifi",
1747            name: "Wi-Fi Network",
1748            fields: vec![
1749                form_field("ssid", "SSID", FormFieldKind::Text, true),
1750                form_field("password", "Password", FormFieldKind::Secret, false),
1751                form_field("security", "Security", FormFieldKind::Text, false),
1752                form_field("notes", "Notes", FormFieldKind::Notes, false),
1753            ],
1754        },
1755        DefaultFormTemplate {
1756            type_id: "00000000-0000-4000-8000-000000000007",
1757            alias: "secure-note",
1758            name: "Secure Note",
1759            fields: vec![
1760                form_field("title", "Title", FormFieldKind::Text, true),
1761                form_field("note", "Note", FormFieldKind::Notes, true),
1762            ],
1763        },
1764    ]
1765}
1766
1767fn form_field(
1768    id: &'static str,
1769    label: &'static str,
1770    kind: FormFieldKind,
1771    required: bool,
1772) -> FormFieldDefinition {
1773    FormFieldDefinition {
1774        id: id.to_string(),
1775        label: label.to_string(),
1776        kind,
1777        required,
1778    }
1779}
1780
1781fn contact_record_path(name: &str) -> Result<LockboxPath> {
1782    LockboxPath::new(format!("/contacts/{}.pub", validate_record_name(name)?))
1783}
1784
1785fn contact_signing_record_path(name: &str) -> Result<LockboxPath> {
1786    LockboxPath::new(format!(
1787        "/contacts/{}.signing.pub",
1788        validate_record_name(name)?
1789    ))
1790}
1791
1792fn profile_history_record_path(name: &str) -> Result<LockboxPath> {
1793    LockboxPath::new(format!(
1794        "/profile_histories/{}.lbih",
1795        validate_record_name(name)?
1796    ))
1797}
1798
1799fn profile_email_record_path(name: &str) -> Result<LockboxPath> {
1800    LockboxPath::new(format!(
1801        "/profile_emails/{}.lbie",
1802        validate_record_name(name)?
1803    ))
1804}
1805
1806fn key_directory_backup_record_path(lockbox_id: LockboxId) -> Result<LockboxPath> {
1807    LockboxPath::new(format!("/key_directories/{lockbox_id}.keydir"))
1808}
1809
1810fn lockbox_password_variable_name(lockbox_id: LockboxId) -> Result<VariableName> {
1811    VariableName::new(format!(
1812        "LOCKBOX_VAULT_LOCKBOX_PASSWORD_{}",
1813        crate::encode_hex(lockbox_id.as_bytes())
1814    ))
1815}
1816
1817fn known_lockbox_record_path(path: impl AsRef<Path>) -> Result<LockboxPath> {
1818    let mut hasher = Sha256::new();
1819    hasher.update(path.as_ref().to_string_lossy().as_bytes());
1820    let digest: [u8; 32] = hasher.finalize().into();
1821    let encoded = crate::encode_hex(&digest);
1822    LockboxPath::new(format!("/known_lockboxes/{encoded}.lkl"))
1823}
1824
1825fn access_slot_label_root(lockbox_id: LockboxId) -> String {
1826    format!("/access_slots/{}", crate::encode_hex(lockbox_id.as_bytes()))
1827}
1828
1829fn access_slot_label_record_path(lockbox_id: LockboxId, slot_id: u64) -> Result<LockboxPath> {
1830    LockboxPath::new(format!(
1831        "{}/{slot_id}.lbas",
1832        access_slot_label_root(lockbox_id)
1833    ))
1834}
1835
1836fn vault_structure_version_record_path() -> Result<LockboxPath> {
1837    LockboxPath::new(VAULT_STRUCTURE_VERSION_PATH)
1838}
1839
1840fn decode_structure_version(bytes: &[u8]) -> Result<u32> {
1841    let text = std::str::from_utf8(bytes).map_err(|_| {
1842        Error::CorruptVaultRecord("vault structure version is not valid UTF-8".to_string())
1843    })?;
1844    let text = text.strip_suffix('\n').unwrap_or(text);
1845    if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
1846        return Err(Error::CorruptVaultRecord(
1847            "vault structure version is not a decimal integer".to_string(),
1848        ));
1849    }
1850    text.parse::<u32>()
1851        .map_err(|_| Error::CorruptVaultRecord("vault structure version is too large".to_string()))
1852}
1853
1854fn hex_encode_secret(mut bytes: SecretVec) -> Result<SecretVec> {
1855    let original_len = bytes.len();
1856    bytes.resize_zeroed(original_len * 2)?;
1857    bytes.with_mut_bytes(|bytes| {
1858        for index in (0..original_len).rev() {
1859            let byte = bytes[index];
1860            bytes[index * 2] = secret_hex_char(byte >> 4);
1861            bytes[index * 2 + 1] = secret_hex_char(byte & 0x0f);
1862        }
1863    })?;
1864    Ok(bytes)
1865}
1866
1867fn decode_hex_secret_in_place(bytes: &mut SecretVec) -> Result<()> {
1868    bytes.with_mut_bytes(|bytes| {
1869        let len = bytes.len();
1870        if len % 2 != 0 {
1871            return Err(Error::InvalidKeyMaterial(
1872                "owner signing key hex has odd length".to_string(),
1873            ));
1874        }
1875        let mut write = 0usize;
1876        let mut read = 0usize;
1877        while read < len {
1878            let high = secret_hex_digit(bytes[read])?;
1879            let low = secret_hex_digit(bytes[read + 1])?;
1880            bytes[write] = (high << 4) | low;
1881            write += 1;
1882            read += 2;
1883        }
1884        for byte in &mut bytes[write..] {
1885            *byte = 0;
1886        }
1887        Ok::<_, Error>(write)
1888    })??;
1889    bytes.truncate(bytes.len() / 2)?;
1890    Ok(())
1891}
1892
1893fn secret_hex_digit(byte: u8) -> Result<u8> {
1894    match byte {
1895        b'0'..=b'9' => Ok(byte - b'0'),
1896        b'a'..=b'f' => Ok(byte - b'a' + 10),
1897        b'A'..=b'F' => Ok(byte - b'A' + 10),
1898        _ => Err(Error::InvalidKeyMaterial(
1899            "owner signing key hex contains non-hex digits".to_string(),
1900        )),
1901    }
1902}
1903
1904fn secret_hex_char(value: u8) -> u8 {
1905    b"0123456789abcdef"[value as usize]
1906}
1907
1908fn encode_known_lockbox(record: &KnownLockbox) -> Vec<u8> {
1909    let mut out = Vec::new();
1910    out.extend_from_slice(KNOWN_LOCKBOX_MAGIC);
1911    put_u16(&mut out, KNOWN_LOCKBOX_VERSION);
1912    out.extend_from_slice(record.lockbox_id.as_bytes());
1913    put_string(&mut out, &record.path);
1914    put_u64(&mut out, record.last_seen_unix_ms);
1915    out
1916}
1917
1918fn decode_known_lockbox(bytes: &[u8]) -> Result<KnownLockbox> {
1919    let mut reader = BinaryReader::new(bytes);
1920    if reader.bytes(4)? != KNOWN_LOCKBOX_MAGIC {
1921        return Err(Error::CorruptVaultRecord(
1922            "known lockbox record has invalid magic".to_string(),
1923        ));
1924    }
1925    let version = reader.u16()?;
1926    if version != KNOWN_LOCKBOX_VERSION {
1927        return Err(Error::CorruptVaultRecord(format!(
1928            "known lockbox record version {version} is not supported"
1929        )));
1930    }
1931    let id = reader.bytes(16)?;
1932    let lockbox_id = LockboxId::from_bytes(id.try_into().map_err(|_| {
1933        Error::CorruptVaultRecord("known lockbox id has invalid length".to_string())
1934    })?);
1935    let path = reader.string()?;
1936    let last_seen_unix_ms = reader.u64()?;
1937    reader.finish()?;
1938    Ok(KnownLockbox {
1939        lockbox_id,
1940        path,
1941        last_seen_unix_ms,
1942    })
1943}
1944
1945fn encode_access_slot_label(label: &AccessSlotLabel) -> Vec<u8> {
1946    let mut out = Vec::new();
1947    out.extend_from_slice(b"LBAS");
1948    put_u16(&mut out, 1);
1949    out.extend_from_slice(label.lockbox_id.as_bytes());
1950    put_u64(&mut out, label.slot_id);
1951    put_string(&mut out, &label.name);
1952    put_u64(&mut out, label.updated_at_unix_ms);
1953    out
1954}
1955
1956fn decode_access_slot_label(bytes: &[u8]) -> Result<AccessSlotLabel> {
1957    let mut reader = BinaryReader::new(bytes);
1958    if reader.bytes(4)? != b"LBAS" {
1959        return Err(Error::CorruptVaultRecord(
1960            "access slot label record has invalid magic".to_string(),
1961        ));
1962    }
1963    let version = reader.u16()?;
1964    if version != 1 {
1965        return Err(Error::CorruptVaultRecord(format!(
1966            "access slot label version {version} is not supported"
1967        )));
1968    }
1969    let id = reader.bytes(16)?;
1970    let lockbox_id = LockboxId::from_bytes(id.try_into().map_err(|_| {
1971        Error::CorruptVaultRecord("access slot label lockbox id has invalid length".to_string())
1972    })?);
1973    let slot_id = reader.u64()?;
1974    let name = reader.string()?;
1975    let updated_at_unix_ms = reader.u64()?;
1976    reader.finish()?;
1977    Ok(AccessSlotLabel {
1978        lockbox_id,
1979        slot_id,
1980        name,
1981        updated_at_unix_ms,
1982    })
1983}
1984
1985fn encode_profile_email(email: &str) -> Vec<u8> {
1986    let mut out = Vec::new();
1987    out.extend_from_slice(PROFILE_EMAIL_MAGIC);
1988    put_u16(&mut out, PROFILE_EMAIL_VERSION);
1989    put_string(&mut out, email);
1990    out
1991}
1992
1993fn decode_profile_email(bytes: &[u8]) -> Result<String> {
1994    let mut reader = BinaryReader::new(bytes);
1995    if reader.bytes(4)? != PROFILE_EMAIL_MAGIC {
1996        return Err(Error::CorruptVaultRecord(
1997            "profile email record has invalid magic".to_string(),
1998        ));
1999    }
2000    let version = reader.u16()?;
2001    if version != PROFILE_EMAIL_VERSION {
2002        return Err(Error::CorruptVaultRecord(format!(
2003            "profile email record version {version} is not supported"
2004        )));
2005    }
2006    let email = reader.string()?;
2007    reader.finish()?;
2008    Ok(email)
2009}
2010
2011fn encode_profile_history(history: &ProfileHistory) -> Vec<u8> {
2012    let mut out = Vec::new();
2013    out.extend_from_slice(PROFILE_HISTORY_MAGIC);
2014    put_u16(&mut out, PROFILE_HISTORY_VERSION);
2015    put_u16(&mut out, history.active_generation);
2016    put_u16(&mut out, history.generations.len() as u16);
2017    for generation in &history.generations {
2018        put_u16(&mut out, generation.index);
2019        put_u16(&mut out, generation_status_to_u16(generation.status));
2020        put_u64(&mut out, generation.created_at_unix_ms);
2021        match generation.retired_at_unix_ms {
2022            Some(retired_at) => {
2023                out.push(1);
2024                put_u64(&mut out, retired_at);
2025            }
2026            None => {
2027                out.push(0);
2028                put_u64(&mut out, 0);
2029            }
2030        }
2031        put_bytes(&mut out, &generation.contact_fingerprint);
2032    }
2033    out
2034}
2035
2036fn decode_profile_history(name: &str, bytes: &[u8]) -> Result<ProfileHistory> {
2037    let mut reader = BinaryReader::new(bytes);
2038    if reader.bytes(4)? != PROFILE_HISTORY_MAGIC {
2039        return Err(Error::CorruptVaultRecord(
2040            "profile history record has invalid magic".to_string(),
2041        ));
2042    }
2043    let version = reader.u16()?;
2044    if version != PROFILE_HISTORY_VERSION {
2045        return Err(Error::CorruptVaultRecord(format!(
2046            "profile history version {version} is not supported"
2047        )));
2048    }
2049    let active_generation = reader.u16()?;
2050    let count = reader.u16()? as usize;
2051    let mut generations = Vec::with_capacity(count);
2052    for _ in 0..count {
2053        let index = reader.u16()?;
2054        let status = generation_status_from_u16(reader.u16()?)?;
2055        let created_at_unix_ms = reader.u64()?;
2056        let retired_present = reader.u8()? != 0;
2057        let retired_at = reader.u64()?;
2058        let contact_fingerprint = reader.length_prefixed_bytes()?.to_vec();
2059        generations.push(ProfileGeneration {
2060            index,
2061            status,
2062            contact_fingerprint,
2063            created_at_unix_ms,
2064            retired_at_unix_ms: retired_present.then_some(retired_at),
2065        });
2066    }
2067    reader.finish()?;
2068    Ok(ProfileHistory {
2069        name: name.to_string(),
2070        active_generation,
2071        generations,
2072    })
2073}
2074
2075struct BinaryReader<'a> {
2076    bytes: &'a [u8],
2077    offset: usize,
2078}
2079
2080impl<'a> BinaryReader<'a> {
2081    fn new(bytes: &'a [u8]) -> Self {
2082        Self { bytes, offset: 0 }
2083    }
2084
2085    fn bytes(&mut self, len: usize) -> Result<&'a [u8]> {
2086        if self.offset + len > self.bytes.len() {
2087            return Err(Error::CorruptVaultRecord(
2088                "binary vault record is truncated".to_string(),
2089            ));
2090        }
2091        let out = &self.bytes[self.offset..self.offset + len];
2092        self.offset += len;
2093        Ok(out)
2094    }
2095
2096    fn u16(&mut self) -> Result<u16> {
2097        let bytes = self.bytes(2)?;
2098        Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
2099    }
2100
2101    fn u8(&mut self) -> Result<u8> {
2102        Ok(self.bytes(1)?[0])
2103    }
2104
2105    fn u32(&mut self) -> Result<u32> {
2106        let bytes = self.bytes(4)?;
2107        Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
2108    }
2109
2110    fn u64(&mut self) -> Result<u64> {
2111        let bytes = self.bytes(8)?;
2112        Ok(u64::from_be_bytes([
2113            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
2114        ]))
2115    }
2116
2117    fn string(&mut self) -> Result<String> {
2118        let len = self.u32()? as usize;
2119        let bytes = self.bytes(len)?;
2120        String::from_utf8(bytes.to_vec()).map_err(|_| {
2121            Error::CorruptVaultRecord("binary vault string is not valid UTF-8".to_string())
2122        })
2123    }
2124
2125    fn length_prefixed_bytes(&mut self) -> Result<&'a [u8]> {
2126        let len = self.u32()? as usize;
2127        self.bytes(len)
2128    }
2129
2130    fn finish(&self) -> Result<()> {
2131        if self.offset != self.bytes.len() {
2132            return Err(Error::CorruptVaultRecord(
2133                "binary vault record has trailing bytes".to_string(),
2134            ));
2135        }
2136        Ok(())
2137    }
2138}
2139
2140fn put_u16(out: &mut Vec<u8>, value: u16) {
2141    out.extend_from_slice(&value.to_be_bytes());
2142}
2143
2144fn put_u32(out: &mut Vec<u8>, value: u32) {
2145    out.extend_from_slice(&value.to_be_bytes());
2146}
2147
2148fn put_u64(out: &mut Vec<u8>, value: u64) {
2149    out.extend_from_slice(&value.to_be_bytes());
2150}
2151
2152fn put_string(out: &mut Vec<u8>, value: &str) {
2153    put_u32(out, value.len() as u32);
2154    out.extend_from_slice(value.as_bytes());
2155}
2156
2157fn put_bytes(out: &mut Vec<u8>, value: &[u8]) {
2158    put_u32(out, value.len() as u32);
2159    out.extend_from_slice(value);
2160}
2161
2162fn generation_status_to_u16(status: ProfileGenerationStatus) -> u16 {
2163    match status {
2164        ProfileGenerationStatus::Active => GENERATION_ACTIVE,
2165        ProfileGenerationStatus::Retired => GENERATION_RETIRED,
2166        ProfileGenerationStatus::Compromised => GENERATION_COMPROMISED,
2167    }
2168}
2169
2170fn generation_status_from_u16(value: u16) -> Result<ProfileGenerationStatus> {
2171    match value {
2172        GENERATION_ACTIVE => Ok(ProfileGenerationStatus::Active),
2173        GENERATION_RETIRED => Ok(ProfileGenerationStatus::Retired),
2174        GENERATION_COMPROMISED => Ok(ProfileGenerationStatus::Compromised),
2175        _ => Err(Error::CorruptVaultRecord(format!(
2176            "unknown profile generation status {value}"
2177        ))),
2178    }
2179}
2180
2181fn contact_fingerprint(public_key: &ContactPublicKey) -> Vec<u8> {
2182    let mut hasher = Sha256::new();
2183    hasher.update(public_key.to_bytes());
2184    hasher.finalize()[..16].to_vec()
2185}
2186
2187fn unix_ms(time: SystemTime) -> u64 {
2188    time.duration_since(UNIX_EPOCH)
2189        .map(|duration| duration.as_millis() as u64)
2190        .unwrap_or(0)
2191}
2192
2193/// Returns the default directory for the local vault.
2194///
2195/// `LOCKBOX_VAULT_DIR` overrides the platform default. Without an override,
2196/// the path follows the operating system's application-data conventions.
2197pub fn default_vault_dir() -> Result<PathBuf> {
2198    if let Ok(path) = env::var("LOCKBOX_VAULT_DIR") {
2199        return Ok(PathBuf::from(path));
2200    }
2201    default_vault_dir_for_os()
2202}
2203
2204/// Returns the default path to the local vault file.
2205pub fn default_vault_path() -> Result<PathBuf> {
2206    Ok(default_vault_dir()?.join(VAULT_FILE_NAME))
2207}
2208
2209#[cfg(target_os = "windows")]
2210fn default_vault_dir_for_os() -> Result<PathBuf> {
2211    let base = env::var_os("LOCALAPPDATA")
2212        .map(PathBuf::from)
2213        .ok_or_else(|| Error::Configuration("LOCALAPPDATA is not set".to_string()))?;
2214    Ok(base.join("reVault").join("vault"))
2215}
2216
2217#[cfg(target_os = "macos")]
2218fn default_vault_dir_for_os() -> Result<PathBuf> {
2219    let home = home_dir()?;
2220    Ok(home
2221        .join("Library")
2222        .join("Application Support")
2223        .join("reVault")
2224        .join("vault"))
2225}
2226
2227#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
2228fn default_vault_dir_for_os() -> Result<PathBuf> {
2229    if let Ok(path) = env::var("XDG_DATA_HOME") {
2230        return Ok(PathBuf::from(path).join("lockbox").join("vault"));
2231    }
2232    Ok(home_dir()?
2233        .join(".local")
2234        .join("share")
2235        .join("lockbox")
2236        .join("vault"))
2237}
2238
2239#[cfg(not(target_os = "windows"))]
2240fn home_dir() -> Result<PathBuf> {
2241    env::var_os("HOME")
2242        .map(PathBuf::from)
2243        .ok_or_else(|| Error::Configuration("HOME is not set".to_string()))
2244}
2245
2246fn validate_record_name(name: &str) -> Result<&str> {
2247    let valid = !name.is_empty()
2248        && name
2249            .bytes()
2250            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'));
2251    if valid {
2252        Ok(name)
2253    } else {
2254        Err(Error::InvalidInput(format!(
2255            "vault record name must contain only ASCII letters, digits, '-' or '_': {name}"
2256        )))
2257    }
2258}
2259
2260fn encode_name_hex(name: &str) -> String {
2261    crate::encode_hex(name.as_bytes()).to_ascii_uppercase()
2262}
2263
2264fn decode_name_hex(hex: &str) -> Option<String> {
2265    let bytes = crate::decode_hex(hex).ok()?;
2266    String::from_utf8(bytes).ok()
2267}
2268
2269fn create_private_dir(path: &Path) -> Result<()> {
2270    fs::create_dir_all(path).map_err(|err| Error::Io(err.to_string()))?;
2271    set_private_dir_permissions(path)
2272}
2273
2274#[cfg(unix)]
2275fn set_private_dir_permissions(path: &Path) -> Result<()> {
2276    use std::os::unix::fs::PermissionsExt;
2277    fs::set_permissions(path, fs::Permissions::from_mode(0o700))
2278        .map_err(|err| Error::Io(err.to_string()))
2279}
2280
2281#[cfg(not(unix))]
2282fn set_private_dir_permissions(_path: &Path) -> Result<()> {
2283    Ok(())
2284}
2285
2286#[cfg(unix)]
2287fn set_private_file_permissions(path: &Path) -> Result<()> {
2288    use std::os::unix::fs::PermissionsExt;
2289    fs::set_permissions(path, fs::Permissions::from_mode(0o600))
2290        .map_err(|err| Error::Io(err.to_string()))
2291}
2292
2293#[cfg(not(unix))]
2294fn set_private_file_permissions(_path: &Path) -> Result<()> {
2295    Ok(())
2296}