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")
}
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)
}
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(())
}
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(_))));
}
}