revault_lockbox_api 0.0.4

reVault lockbox API to create and manage lockboxes
Documentation
pub(crate) mod cache_options;
pub(crate) mod file_lock;
pub(crate) mod free_index;
pub(crate) mod free_slot;
pub(crate) mod page_cache;

use crate::secret_vec::SecureVec;
use crate::storage::file_lock::{FileLockScope, ScopedFileLock};
use crate::{Error, Result};
use std::fs::{self, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

pub(crate) trait Storage: Clone + std::fmt::Debug {
    fn len(&self) -> Result<u64>;
    fn read_at(&self, offset: u64, len: usize) -> Result<Vec<u8>>;
    fn read_at_into(&self, offset: u64, out: &mut [u8]) -> Result<()>;
    fn append(&mut self, bytes: &[u8]) -> Result<u64>;
    fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<()>;
    fn sync(&self) -> Result<()>;

    fn read_at_secure(&self, offset: u64, len: usize) -> Result<SecureVec> {
        let mut out = SecureVec::new();
        out.resize_zeroed(len)?;
        out.with_mut_bytes(|bytes| self.read_at_into(offset, bytes))??;
        Ok(out)
    }

    fn read_all(&self) -> Result<Vec<u8>> {
        let len = self.len()?;
        if len > usize::MAX as u64 {
            return Err(Error::SecurityLimitExceeded(
                "vault is too large to materialize in memory".to_string(),
            ));
        }
        self.read_at(0, len as usize)
    }
}

#[derive(Debug, Clone)]
pub(crate) enum StorageBackend {
    Memory(MemoryStore),
    File(FileStore),
}

impl StorageBackend {
    pub(crate) fn memory(bytes: Vec<u8>) -> Self {
        Self::Memory(MemoryStore::new(bytes))
    }

    pub(crate) fn file(path: impl AsRef<Path>) -> Result<Self> {
        Ok(Self::File(FileStore::open(path, None)?))
    }

    pub(crate) fn file_for_write(path: impl AsRef<Path>) -> Result<Self> {
        let path = path.as_ref();
        let lock = ScopedFileLock::acquire(path, FileLockScope::Lockbox)?;
        Ok(Self::File(FileStore::open(path, Some(Arc::new(lock)))?))
    }

    pub(crate) fn create_file(path: impl AsRef<Path>, initial_bytes: &[u8]) -> Result<Self> {
        Ok(Self::File(FileStore::create(path, initial_bytes, true)?))
    }

    pub(crate) fn create_file_unlocked(
        path: impl AsRef<Path>,
        initial_bytes: &[u8],
    ) -> Result<Self> {
        Ok(Self::File(FileStore::create(path, initial_bytes, false)?))
    }

    pub(crate) fn path(&self) -> Option<&Path> {
        match self {
            Self::Memory(_) => None,
            Self::File(store) => Some(store.path()),
        }
    }
}

impl Storage for StorageBackend {
    fn len(&self) -> Result<u64> {
        match self {
            Self::Memory(store) => store.len(),
            Self::File(store) => store.len(),
        }
    }

    fn read_at(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
        match self {
            Self::Memory(store) => store.read_at(offset, len),
            Self::File(store) => store.read_at(offset, len),
        }
    }

    fn read_at_into(&self, offset: u64, out: &mut [u8]) -> Result<()> {
        match self {
            Self::Memory(store) => store.read_at_into(offset, out),
            Self::File(store) => store.read_at_into(offset, out),
        }
    }

    fn append(&mut self, bytes: &[u8]) -> Result<u64> {
        match self {
            Self::Memory(store) => store.append(bytes),
            Self::File(store) => store.append(bytes),
        }
    }

    fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<()> {
        match self {
            Self::Memory(store) => store.write_at(offset, bytes),
            Self::File(store) => store.write_at(offset, bytes),
        }
    }

    fn sync(&self) -> Result<()> {
        match self {
            Self::Memory(store) => store.sync(),
            Self::File(store) => store.sync(),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct MemoryStore {
    bytes: Vec<u8>,
    #[cfg(test)]
    fail_append_after_successes: Option<usize>,
    #[cfg(test)]
    fail_next_write_at: Option<u64>,
}

impl MemoryStore {
    fn new(bytes: Vec<u8>) -> Self {
        Self {
            bytes,
            #[cfg(test)]
            fail_append_after_successes: None,
            #[cfg(test)]
            fail_next_write_at: None,
        }
    }

    #[cfg(test)]
    fn fail_append_after_successes(&mut self, successes: usize) {
        self.fail_append_after_successes = Some(successes);
    }

    #[cfg(test)]
    fn fail_next_write_at(&mut self, offset: u64) {
        self.fail_next_write_at = Some(offset);
    }

    #[cfg(test)]
    fn should_fail_append(&mut self) -> bool {
        let Some(remaining) = self.fail_append_after_successes.as_mut() else {
            return false;
        };
        if *remaining == 0 {
            self.fail_append_after_successes = None;
            true
        } else {
            *remaining -= 1;
            false
        }
    }

    #[cfg(test)]
    fn should_fail_write_at(&mut self, offset: u64) -> bool {
        if self.fail_next_write_at == Some(offset) {
            self.fail_next_write_at = None;
            true
        } else {
            false
        }
    }
}

impl Storage for MemoryStore {
    fn len(&self) -> Result<u64> {
        Ok(self.bytes.len() as u64)
    }

    fn read_at(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
        let start = offset as usize;
        let end = start
            .checked_add(len)
            .ok_or_else(|| Error::Io("storage read offset overflow".to_string()))?;
        if end > self.bytes.len() {
            return Err(Error::Truncated);
        }
        Ok(self.bytes[start..end].to_vec())
    }

    fn read_at_into(&self, offset: u64, out: &mut [u8]) -> Result<()> {
        let start = offset as usize;
        let end = start
            .checked_add(out.len())
            .ok_or_else(|| Error::Io("storage read offset overflow".to_string()))?;
        if end > self.bytes.len() {
            return Err(Error::Truncated);
        }
        out.copy_from_slice(&self.bytes[start..end]);
        Ok(())
    }

    fn append(&mut self, bytes: &[u8]) -> Result<u64> {
        #[cfg(test)]
        if self.should_fail_append() {
            return Err(Error::Io("injected storage append failure".to_string()));
        }
        let offset = self.bytes.len() as u64;
        self.bytes.extend_from_slice(bytes);
        Ok(offset)
    }

    fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<()> {
        #[cfg(test)]
        if self.should_fail_write_at(offset) {
            return Err(Error::Io("injected storage write failure".to_string()));
        }
        let start = offset as usize;
        let end = start
            .checked_add(bytes.len())
            .ok_or_else(|| Error::Io("storage write offset overflow".to_string()))?;
        if end > self.bytes.len() {
            return Err(Error::Io("storage write beyond end".to_string()));
        }
        self.bytes[start..end].copy_from_slice(bytes);
        Ok(())
    }

    fn sync(&self) -> Result<()> {
        Ok(())
    }
}

#[cfg(test)]
impl StorageBackend {
    pub(crate) fn fail_memory_append_after_successes(&mut self, successes: usize) {
        match self {
            Self::Memory(store) => store.fail_append_after_successes(successes),
            Self::File(_) => panic!("failure injection is only available for memory storage"),
        }
    }

    pub(crate) fn fail_memory_next_write_at(&mut self, offset: u64) {
        match self {
            Self::Memory(store) => store.fail_next_write_at(offset),
            Self::File(_) => panic!("failure injection is only available for memory storage"),
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct FileStore {
    path: PathBuf,
    file: Arc<Mutex<std::fs::File>>,
    _write_lock: Option<Arc<ScopedFileLock>>,
}

impl FileStore {
    fn open(path: impl AsRef<Path>, write_lock: Option<Arc<ScopedFileLock>>) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .open(&path)
            .map_err(|err| Error::Io(format!("open {}: {err}", path.display())))?;
        Ok(Self {
            path,
            file: Arc::new(Mutex::new(file)),
            _write_lock: write_lock,
        })
    }

    fn create(path: impl AsRef<Path>, initial_bytes: &[u8], lock: bool) -> Result<Self> {
        let path = path.as_ref().to_path_buf();
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).map_err(|err| Error::Io(err.to_string()))?;
        }
        let write_lock = if lock {
            Some(ScopedFileLock::acquire(&path, FileLockScope::Lockbox)?)
        } else {
            None
        };
        let mut file = OpenOptions::new()
            .read(true)
            .write(true)
            .create_new(true)
            .open(&path)
            .map_err(|err| {
                if err.kind() == std::io::ErrorKind::AlreadyExists {
                    Error::AlreadyExists(path.display().to_string())
                } else {
                    Error::Io(format!("create {}: {err}", path.display()))
                }
            })?;
        file.write_all(initial_bytes)
            .map_err(|err| Error::Io(format!("write {}: {err}", path.display())))?;
        file.sync_data()
            .map_err(|err| Error::Io(format!("sync {}: {err}", path.display())))?;
        Ok(Self {
            path,
            file: Arc::new(Mutex::new(file)),
            _write_lock: write_lock.map(Arc::new),
        })
    }

    fn lock_file(&self) -> Result<std::sync::MutexGuard<'_, std::fs::File>> {
        self.file
            .lock()
            .map_err(|_| Error::Io("storage file lock poisoned".to_string()))
    }

    fn path(&self) -> &Path {
        &self.path
    }
}

impl Storage for FileStore {
    fn len(&self) -> Result<u64> {
        Ok(fs::metadata(&self.path)
            .map_err(|err| Error::Io(format!("metadata {}: {err}", self.path.display())))?
            .len())
    }

    fn read_at(&self, offset: u64, len: usize) -> Result<Vec<u8>> {
        let mut out = vec![0; len];
        self.read_at_into(offset, &mut out)?;
        Ok(out)
    }

    fn read_at_into(&self, offset: u64, out: &mut [u8]) -> Result<()> {
        let mut file = self.lock_file()?;
        file.seek(SeekFrom::Start(offset))
            .map_err(|err| Error::Io(format!("seek {}: {err}", self.path.display())))?;
        file.read_exact(out).map_err(|err| {
            if err.kind() == std::io::ErrorKind::UnexpectedEof {
                Error::Truncated
            } else {
                Error::Io(format!("read {}: {err}", self.path.display()))
            }
        })
    }

    fn append(&mut self, bytes: &[u8]) -> Result<u64> {
        let mut file = self.lock_file()?;
        let offset = file
            .seek(SeekFrom::End(0))
            .map_err(|err| Error::Io(format!("seek {}: {err}", self.path.display())))?;
        file.write_all(bytes)
            .map_err(|err| Error::Io(format!("append {}: {err}", self.path.display())))?;
        Ok(offset)
    }

    fn write_at(&mut self, offset: u64, bytes: &[u8]) -> Result<()> {
        let mut file = self.lock_file()?;
        file.seek(SeekFrom::Start(offset))
            .map_err(|err| Error::Io(format!("seek {}: {err}", self.path.display())))?;
        file.write_all(bytes)
            .map_err(|err| Error::Io(format!("write {}: {err}", self.path.display())))
    }

    fn sync(&self) -> Result<()> {
        self.lock_file()?
            .sync_data()
            .map_err(|err| Error::Io(format!("sync {}: {err}", self.path.display())))
    }
}