use std::fs;
use std::path::Path;
use hmac::{Hmac, Mac};
use rand::TryRngCore;
use sha2::Sha256;
use crate::errors::{EnvVaultError, Result};
const KEYFILE_LEN: usize = 32;
pub fn generate_keyfile(path: &Path) -> Result<Vec<u8>> {
if path.exists() {
return Err(EnvVaultError::KeyfileError(format!(
"keyfile already exists at {}",
path.display()
)));
}
let mut keyfile = vec![0u8; KEYFILE_LEN];
rand::rngs::OsRng
.try_fill_bytes(&mut keyfile)
.expect("OS RNG failed");
if let Some(parent) = path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).map_err(|e| {
EnvVaultError::KeyfileError(format!("cannot create keyfile directory: {e}"))
})?;
}
}
fs::write(path, &keyfile)
.map_err(|e| EnvVaultError::KeyfileError(format!("failed to write keyfile: {e}")))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o600);
fs::set_permissions(path, perms).map_err(|e| {
EnvVaultError::KeyfileError(format!("failed to set keyfile permissions: {e}"))
})?;
}
Ok(keyfile)
}
pub fn load_keyfile(path: &Path) -> Result<Vec<u8>> {
if !path.exists() {
return Err(EnvVaultError::KeyfileError(format!(
"keyfile not found at {}",
path.display()
)));
}
let data = fs::read(path)
.map_err(|e| EnvVaultError::KeyfileError(format!("failed to read keyfile: {e}")))?;
if data.len() != KEYFILE_LEN {
return Err(EnvVaultError::KeyfileError(format!(
"keyfile must be exactly {} bytes, got {}",
KEYFILE_LEN,
data.len()
)));
}
Ok(data)
}
pub fn combine_password_keyfile(password: &[u8], keyfile_bytes: &[u8]) -> Result<Vec<u8>> {
let mut mac = Hmac::<Sha256>::new_from_slice(keyfile_bytes)
.map_err(|e| EnvVaultError::KeyfileError(format!("HMAC init failed: {e}")))?;
mac.update(password);
Ok(mac.finalize().into_bytes().to_vec())
}
pub fn hash_keyfile(keyfile_bytes: &[u8]) -> String {
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use sha2::Digest;
let hash = Sha256::digest(keyfile_bytes);
BASE64.encode(hash)
}
pub fn verify_keyfile_hash(keyfile_bytes: &[u8], expected_hash: &str) -> Result<()> {
use subtle::ConstantTimeEq;
let actual_hash = hash_keyfile(keyfile_bytes);
if actual_hash
.as_bytes()
.ct_eq(expected_hash.as_bytes())
.into()
{
Ok(())
} else {
Err(EnvVaultError::KeyfileError(
"wrong keyfile — hash does not match the vault".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn generate_and_load_keyfile_roundtrip() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.keyfile");
let generated = generate_keyfile(&path).unwrap();
assert_eq!(generated.len(), KEYFILE_LEN);
let loaded = load_keyfile(&path).unwrap();
assert_eq!(generated, loaded);
}
#[test]
fn generate_keyfile_fails_if_exists() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("test.keyfile");
generate_keyfile(&path).unwrap();
let result = generate_keyfile(&path);
assert!(result.is_err());
}
#[test]
fn load_keyfile_fails_if_missing() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nonexistent.keyfile");
let result = load_keyfile(&path);
assert!(result.is_err());
}
#[test]
fn load_keyfile_fails_on_wrong_length() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("bad.keyfile");
fs::write(&path, [0u8; 16]).unwrap();
let result = load_keyfile(&path);
assert!(result.is_err());
}
#[test]
fn combine_password_keyfile_is_deterministic() {
let password = b"my-password";
let keyfile = [0xABu8; 32];
let result1 = combine_password_keyfile(password, &keyfile).unwrap();
let result2 = combine_password_keyfile(password, &keyfile).unwrap();
assert_eq!(result1, result2);
}
#[test]
fn combine_differs_with_different_keyfile() {
let password = b"my-password";
let keyfile1 = [0xABu8; 32];
let keyfile2 = [0xCDu8; 32];
let result1 = combine_password_keyfile(password, &keyfile1).unwrap();
let result2 = combine_password_keyfile(password, &keyfile2).unwrap();
assert_ne!(result1, result2);
}
#[test]
fn combine_differs_with_different_password() {
let keyfile = [0xABu8; 32];
let result1 = combine_password_keyfile(b"password1", &keyfile).unwrap();
let result2 = combine_password_keyfile(b"password2", &keyfile).unwrap();
assert_ne!(result1, result2);
}
#[test]
fn hash_keyfile_is_deterministic() {
let keyfile = [0x42u8; 32];
let hash1 = hash_keyfile(&keyfile);
let hash2 = hash_keyfile(&keyfile);
assert_eq!(hash1, hash2);
}
#[test]
fn verify_keyfile_hash_succeeds_for_correct_keyfile() {
let keyfile = [0x42u8; 32];
let hash = hash_keyfile(&keyfile);
assert!(verify_keyfile_hash(&keyfile, &hash).is_ok());
}
#[test]
fn verify_keyfile_hash_fails_for_wrong_keyfile() {
let keyfile = [0x42u8; 32];
let wrong_keyfile = [0x43u8; 32];
let hash = hash_keyfile(&keyfile);
assert!(verify_keyfile_hash(&wrong_keyfile, &hash).is_err());
}
}