Skip to main content

FileBackend

Struct FileBackend 

Source
pub struct FileBackend { /* private fields */ }
Expand description

Filesystem-backed keychain.

Thread-safe — KeychainBackend is Send + Sync, and all operations use OS-level atomic primitives (rename, unlink). Multiple FileBackend instances pointing at the same root directory coexist without mutual serialization; the tmp-file names include a random suffix so concurrent writes to the same BackendKey do not step on each other’s tmp files.

§Example

use std::sync::Arc;
use dig_keystore::{
    backend::{FileBackend, BackendKey, KeychainBackend},
};

let backend: Arc<dyn KeychainBackend> = Arc::new(
    FileBackend::new("/var/lib/dig/keys")
);
backend.write(&BackendKey::new("v1"), b"...").unwrap();

Implementations§

Source§

impl FileBackend

Source

pub fn new(root: impl Into<PathBuf>) -> Self

Create a new file backend rooted at root.

The directory is not created immediately — it is lazily created on the first write call (with mode 0700 on Unix). This lets callers construct a FileBackend in tests without side effects; no files are written until the first write.

§Example
use dig_keystore::backend::FileBackend;
let be = FileBackend::new("/var/lib/dig/keys");
let _ = be;  // directory not created yet
Source

pub fn root(&self) -> &Path

The root directory this backend writes to.

Trait Implementations§

Source§

impl KeychainBackend for FileBackend

Source§

fn read(&self, key: &BackendKey) -> Result<Vec<u8>>

Read the entire file at <root>/<key>.dks.

Returns KeystoreError::Backend wrapping an io::Error with ErrorKind::NotFound if the file does not exist.

Source§

fn write(&self, key: &BackendKey, data: &[u8]) -> Result<()>

Atomically write data to <root>/<key>.dks.

Steps:

  1. Ensure root exists, is a directory rather than a symlink, and is verified owner-only.
  2. Create sibling <key>.dks.tmp.<random> file with mode 0600 requested in the open(2) call on Unix, then verify the mode that actually took effect before any bytes are written — so a root that cannot hold key material safely yields KeystoreError::InsecurePermissions and an empty, removed tmp file rather than an exposed blob.
  3. Write data, fsync the file handle.
  4. rename the tmp file onto the final name.
  5. On Unix, fsync the containing directory so the rename is durable.
  6. On error in step 4, best-effort unlink the tmp file.

The random suffix in step 2 is not cryptographic — it exists only to disambiguate two concurrent writes to the same key from the same process. Uses a hash of (nanoseconds_since_epoch, pid).

Source§

fn delete(&self, key: &BackendKey) -> Result<()>

Best-effort secure delete, then unlink.

Steps:

  1. No-op if file does not exist (idempotent).
  2. Open the file for writing; overwrite with zeros in 4 KiB chunks.
  3. fsync the overwritten file so zeros hit storage.
  4. unlink the file.

Step 2 is best-effort. On SSDs with flash translation layer or on copy-on-write filesystems (btrfs, ZFS), the zero pass may not reach the sectors that held the ciphertext. Use full-disk encryption for stronger guarantees.

Source§

fn list(&self, prefix: &str) -> Result<Vec<BackendKey>>

Enumerate keys whose names start with prefix.

Scans the root directory; skips any file that:

  • does not end in .dks
  • has a non-UTF-8 name
  • does not start with prefix

Returns an empty vec if the root directory does not exist.

Source§

fn exists(&self, key: &BackendKey) -> Result<bool>

Stat the path without opening it, preserving the trait’s three-valued contract: present, confidently absent, or could not determine.

Uses symlink_metadata rather than Path::exists() or try_exists(). Path::exists() maps every error to false, which turns an inspection failure into a confident negative — and the caller uses that answer to decide whether to mint over a write that replaces.

symlink_metadata is also the stricter of the two honest options: it does not follow links, so a dangling symlink at the key path counts as present. Something occupies that name; refusing to write over it is the fail-closed reading, whereas try_exists() would report false and invite exactly the overwrite this method exists to prevent.

Source§

fn write_new(&self, key: &BackendKey, data: &[u8]) -> Result<()>

Establish <root>/<key>.dks only if it does not already exist.

Exclusivity comes from the OS: the file is opened with create_new, so exactly one racer creates it and every other gets KeystoreError::AlreadyExists — a distinguishable error the loser can adopt on, rather than a generic I/O failure it can only give up on.

§Why this does not use tmp + rename

rename always replaces, so it cannot express “only if absent”; the two guarantees are not simultaneously available without a hard link, which not every filesystem supports. Exclusivity is the one that matters here, and the cost is bounded: a crash mid-write leaves a short file, which the format’s magic, length and CRC all detect on the next read (SPEC.md §3.2), and which is repaired by deleting it and retrying. The state this method exists to prevent — a coupled pair that settled mismatched — is neither detectable nor repairable. A best-effort unlink removes the partial file on the way out of any failure.

Source§

fn write_new_exclusivity(&self) -> Exclusivity

create_new(true) is an atomic create-if-absent at the OS level, so two concurrent calls cannot both succeed.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.