use std::path::Path;
use sha2::{Digest, Sha256};
use crate::error::Error;
pub fn hash_binary(binary_path: &Path) -> Result<String, Error> {
let bytes = std::fs::read(binary_path).map_err(|e| {
Error::BinaryResolution(format!(
"failed to read binary for hashing at {}: {e}",
binary_path.display()
))
})?;
let mut hasher = Sha256::new();
hasher.update(&bytes);
let hash = hasher.finalize();
Ok(hex::encode(hash))
}
fn parse_sha256_hex(s: &str) -> Result<[u8; 32], Error> {
if s.len() != 64 {
return Err(Error::CryptoFailure(format!(
"invalid SHA-256 hex length: {} (expected 64)",
s.len()
)));
}
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).map_err(|_| {
Error::CryptoFailure("invalid hex digit in SHA-256 fingerprint".to_string())
})?;
}
Ok(out)
}
pub fn verify_binary_hash(binary_path: &Path, expected_hash: &str) -> Result<(), Error> {
let actual_hash = hash_binary(binary_path)?;
let Ok(expected) = parse_sha256_hex(expected_hash) else {
return Err(Error::BinaryTampered {
binary_path: binary_path.display().to_string(),
expected_hash: expected_hash.to_string(),
actual_hash,
});
};
let actual = parse_sha256_hex(&actual_hash)?;
if super::constant_time_eq(&expected, &actual) {
Ok(())
} else {
Err(Error::BinaryTampered {
binary_path: binary_path.display().to_string(),
expected_hash: expected_hash.to_string(),
actual_hash,
})
}
}
pub fn verify_file_hash(
mut file: &std::fs::File,
expected_hash: &str,
binary_path: &Path,
) -> Result<(), Error> {
use std::io::{Read, Seek, SeekFrom};
file.seek(SeekFrom::Start(0))
.map_err(|e| Error::BinaryResolution(format!("failed to seek binary for hashing: {e}")))?;
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
let n = file.read(&mut buffer).map_err(|e| {
Error::BinaryResolution(format!("failed to read binary for hashing: {e}"))
})?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
let actual_hash = hex::encode(hasher.finalize());
let Ok(expected) = parse_sha256_hex(expected_hash) else {
return Err(Error::BinaryTampered {
binary_path: binary_path.display().to_string(),
expected_hash: expected_hash.to_string(),
actual_hash: actual_hash.clone(),
});
};
let actual = parse_sha256_hex(&actual_hash).map_err(|_| Error::BinaryTampered {
binary_path: binary_path.display().to_string(),
expected_hash: expected_hash.to_string(),
actual_hash: actual_hash.clone(),
})?;
if super::constant_time_eq(&expected, &actual) {
Ok(())
} else {
Err(Error::BinaryTampered {
binary_path: binary_path.display().to_string(),
expected_hash: expected_hash.to_string(),
actual_hash,
})
}
}
pub fn hash_open_file(file: &mut std::fs::File) -> Result<String, Error> {
use std::io::{Read, Seek, SeekFrom};
file.seek(SeekFrom::Start(0))
.map_err(|e| Error::BinaryResolution(format!("failed to seek binary for hashing: {e}")))?;
let mut hasher = Sha256::new();
let mut buffer = [0; 8192];
loop {
let n = file.read(&mut buffer).map_err(|e| {
Error::BinaryResolution(format!("failed to read binary for hashing: {e}"))
})?;
if n == 0 {
break;
}
hasher.update(&buffer[..n]);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn verify_not_symlink(path: &std::path::Path) -> Result<(), Error> {
match std::fs::symlink_metadata(path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(Error::StorageIo(e)),
Ok(meta) => {
if meta.file_type().is_symlink() {
return Err(Error::PolicyTampered(format!(
"file is a symlink: {}. symlinks in the vault directory are \
not allowed — they can be used to redirect reads/writes \
to attacker-controlled locations.",
path.display()
)));
}
#[cfg(windows)]
{
if is_reparse_point(path) {
return Err(Error::PolicyTampered(format!(
"file is an NTFS reparse point: {}. reparse points (junctions, \
mount points, cloud-file placeholders, …) silently redirect \
path resolution and are not allowed in the vault directory.",
path.display()
)));
}
}
Ok(())
}
}
}
#[cfg(windows)]
fn is_reparse_point(path: &std::path::Path) -> bool {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::{
GetFileAttributesW, FILE_ATTRIBUTE_REPARSE_POINT, INVALID_FILE_ATTRIBUTES,
};
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let attrs = unsafe { GetFileAttributesW(wide.as_ptr()) };
if attrs == INVALID_FILE_ATTRIBUTES {
return false;
}
(attrs & FILE_ATTRIBUTE_REPARSE_POINT) != 0
}
use crate::hex;