armdb 0.7.0

sharded bitcask key-value storage optimized for NVMe
Documentation
//! `db.salt` sidecar: a 16-byte random HKDF salt for per-shard key derivation.
//!
//! Encryption-only. Stored in the clear next to `db.meta` (an HKDF salt is not
//! secret). Written once at DB init and then immutable; losing it makes the
//! database undecryptable, so writes mirror `write_meta`'s durability model
//! (tmp + rename + file/dir fsync).
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::error::{DbError, DbResult};

pub(crate) const SALT_LEN: usize = 16;

fn salt_path(db_path: &Path) -> PathBuf {
    db_path.join("db.salt")
}

/// Generate a fresh random salt.
pub(crate) fn generate() -> DbResult<[u8; SALT_LEN]> {
    use ring::rand::SecureRandom;

    let rng = ring::rand::SystemRandom::new();
    let mut salt = [0u8; SALT_LEN];
    rng.fill(&mut salt)
        .map_err(|_| DbError::EncryptionError("failed to generate db.salt".into()))?;
    Ok(salt)
}

/// Write `db.salt` durably (tmp + rename + file/dir fsync), mirroring `write_meta`.
pub(crate) fn write(db_path: &Path, salt: &[u8; SALT_LEN]) -> DbResult<()> {
    let path = salt_path(db_path);
    let tmp = db_path.join("db.salt.tmp");
    let mut f = std::fs::File::create(&tmp)?;
    f.write_all(salt)?;
    f.sync_data()?;
    drop(f);
    std::fs::rename(&tmp, &path)?;
    let dir = std::fs::File::open(db_path)?;
    dir.sync_all()?;
    Ok(())
}

/// Read `db.salt`. A missing or wrong-sized file is a fail-closed
/// `FormatMismatch` — this is also how a pre-fix encrypted DB (which has no
/// salt) is rejected on open.
pub(crate) fn read(db_path: &Path) -> DbResult<[u8; SALT_LEN]> {
    let path = salt_path(db_path);
    let bytes = std::fs::read(&path).map_err(|e| {
        if e.kind() == std::io::ErrorKind::NotFound {
            DbError::FormatMismatch("db.salt missing for encrypted database".into())
        } else {
            DbError::Io(e)
        }
    })?;
    if bytes.len() != SALT_LEN {
        return Err(DbError::FormatMismatch(format!(
            "db.salt: expected {SALT_LEN} bytes, got {}",
            bytes.len()
        )));
    }
    let mut salt = [0u8; SALT_LEN];
    salt.copy_from_slice(&bytes);
    Ok(salt)
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn salt_round_trip_16_bytes() {
        let dir = tempdir().unwrap();
        let s = generate().unwrap();
        write(dir.path(), &s).unwrap();
        assert_eq!(read(dir.path()).unwrap(), s);
        assert_eq!(
            std::fs::metadata(dir.path().join("db.salt")).unwrap().len(),
            SALT_LEN as u64
        );
    }

    #[test]
    fn salt_read_missing_is_format_mismatch() {
        let dir = tempdir().unwrap();
        assert!(matches!(read(dir.path()), Err(DbError::FormatMismatch(_))));
    }
}