revault_lockbox_api 0.0.1

reVault lockbox API to create and manage lockboxes
Documentation
use crate::commit_root::{decode_commit_root, CommitRoot};
use crate::crypto::derive_page_content_key;
use crate::lockbox_id::LockboxId;
use crate::page::{
    decode_page, decode_single_object_page_secure, page_decode_slice,
    physical_page_size_from_page_slice, scan_page_records, PageObjectKind, PAGE_MAGIC,
};
use crate::record::{DecodedRecord, RecordHeader, RecordKind};
use crate::scan::Scan;
use crate::secret_vec::SecureVec;
use crate::{Error, Result};
use zeroize::Zeroize;

pub(crate) struct PageScanner<'a> {
    bytes: &'a [u8],
    lockbox_id: LockboxId,
    key: &'a [u8],
}

impl<'a> PageScanner<'a> {
    pub(crate) fn new(bytes: &'a [u8], lockbox_id: LockboxId, key: &'a [u8]) -> Self {
        Self {
            bytes,
            lockbox_id,
            key,
        }
    }

    pub(crate) fn scan_records(&self) -> Scan {
        scan_page_records(self.bytes, self.lockbox_id, self.key)
    }

    pub(crate) fn commit_root_at(&self, offset: u64) -> Result<CommitRoot> {
        let payload = self.commit_root_payload_at(offset)?;
        decode_commit_root(&payload)
    }

    pub(crate) fn commit_root_payload_at(&self, offset: u64) -> Result<Vec<u8>> {
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let decoded = decode_page(page, self.lockbox_id, self.key)?;
        let Some(commit_root_object) = decoded
            .objects
            .iter()
            .find(|object| object.kind == PageObjectKind::CommitRoot)
        else {
            return Err(Error::CorruptRecord);
        };
        commit_root_object.with_payload(|payload| payload.to_vec())
    }

    pub(crate) fn commit_auth_payload_at(&self, offset: u64) -> Result<Vec<u8>> {
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let decoded = decode_page(page, self.lockbox_id, self.key)?;
        let Some(auth_object) = decoded
            .objects
            .iter()
            .find(|object| object.kind == PageObjectKind::CommitAuth)
        else {
            return Err(Error::CorruptRecord);
        };
        auth_object.with_payload(|payload| payload.to_vec())
    }

    pub(crate) fn toc_node_payload_at(&self, offset: u64) -> Result<Vec<u8>> {
        if self.bytes.get(checked_range(offset, 8)?) != Some(PAGE_MAGIC.as_slice()) {
            return Err(Error::CorruptRecord);
        }
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let decoded = decode_page(page, self.lockbox_id, self.key)?;
        let Some(toc_object) = decoded.objects.iter().find(|object| {
            matches!(
                object.kind,
                PageObjectKind::TocLeaf | PageObjectKind::TocInternal
            )
        }) else {
            return Err(Error::CorruptRecord);
        };
        toc_object.with_payload(|payload| payload.to_vec())
    }

    pub(crate) fn secure_object_payload_at(
        &self,
        offset: u64,
        expected: &[PageObjectKind],
    ) -> Result<SecureVec> {
        if self.bytes.get(checked_range(offset, 8)?) != Some(PAGE_MAGIC.as_slice()) {
            return Err(Error::CorruptRecord);
        }
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let mut secure_page = SecureVec::try_from_slice(page)?;
        let mut content_key = derive_page_content_key(self.key);
        let decoded =
            decode_single_object_page_secure(&mut secure_page, self.lockbox_id, &content_key)?;
        content_key.zeroize();
        let Some(object) = decoded
            .objects
            .into_iter()
            .find(|object| expected.contains(&object.kind))
        else {
            return Err(Error::CorruptRecord);
        };
        Ok(object
            .secure_payload()
            .ok_or(Error::CorruptRecord)?
            .try_clone()?)
    }

    pub(crate) fn record_at(&self, offset: u64) -> Result<DecodedRecord> {
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let decoded = decode_page(page, self.lockbox_id, self.key)?;
        let Some(object) = decoded.objects.first() else {
            return Err(Error::CorruptRecord);
        };
        let kind = record_kind_from_page_object(object.kind)?;
        Ok(DecodedRecord {
            header: RecordHeader {
                kind,
                sequence: decoded.sequence,
                total_len: physical_page_size_from_page_slice(page)? as u64,
            },
            offset,
            object_id: object.id,
            payload: object.with_payload(|payload| payload.to_vec())?,
        })
    }

    pub(crate) fn record_object_at(&self, offset: u64, object_id: u64) -> Result<DecodedRecord> {
        let page = self.page_at(offset).ok_or(Error::Truncated)?;
        let decoded = decode_page(page, self.lockbox_id, self.key)?;
        let Some(object) = decoded.objects.iter().find(|object| object.id == object_id) else {
            return Err(Error::CorruptRecord);
        };
        let kind = record_kind_from_page_object(object.kind)?;
        Ok(DecodedRecord {
            header: RecordHeader {
                kind,
                sequence: decoded.sequence,
                total_len: physical_page_size_from_page_slice(page)? as u64,
            },
            offset,
            object_id: object.id,
            payload: object.with_payload(|payload| payload.to_vec())?,
        })
    }

    fn page_at(&self, offset: u64) -> Option<&'a [u8]> {
        page_decode_slice(self.bytes, usize::try_from(offset).ok()?)
    }
}

fn checked_range(offset: u64, len: usize) -> Result<std::ops::Range<usize>> {
    let start = usize::try_from(offset).map_err(|_| Error::CorruptRecord)?;
    let end = start.checked_add(len).ok_or(Error::CorruptRecord)?;
    Ok(start..end)
}

fn record_kind_from_page_object(kind: PageObjectKind) -> Result<RecordKind> {
    Ok(match kind {
        PageObjectKind::PackedFileData | PageObjectKind::FileData => RecordKind::FilePage,
        PageObjectKind::Symlink => RecordKind::Symlink,
        PageObjectKind::VariableSet => RecordKind::Variable,
        PageObjectKind::VariableDelete => RecordKind::VariableDelete,
        PageObjectKind::Delete => RecordKind::Delete,
        PageObjectKind::TocLeaf | PageObjectKind::TocInternal => RecordKind::TocNode,
        PageObjectKind::CommitRoot => RecordKind::CommitRoot,
        PageObjectKind::CommitAuth => RecordKind::CommitAuth,
        PageObjectKind::FreeIndexLeaf | PageObjectKind::FreeIndexInternal => RecordKind::FreeIndex,
        PageObjectKind::KeyDirectory
        | PageObjectKind::VariableLeaf
        | PageObjectKind::VariableInternal
        | PageObjectKind::FormLeaf
        | PageObjectKind::FormInternal => return Err(Error::CorruptRecord),
    })
}