use crate::error::WSError;
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
pub const SECURE_FILE_MODE: u32 = 0o600;
#[cfg(unix)]
pub fn check_permissions(path: &Path) -> Result<(), WSError> {
use std::os::unix::fs::PermissionsExt;
let metadata = fs::metadata(path)?;
let mode = metadata.permissions().mode();
let perm_bits = mode & 0o777;
if perm_bits & 0o077 != 0 {
log::warn!(
"SECURITY WARNING: File '{}' has overly permissive permissions (mode {:o}). \
Sensitive files should have mode 0600 (owner read/write only). \
Consider running: chmod 600 '{}'",
path.display(),
perm_bits,
path.display()
);
}
Ok(())
}
#[cfg(not(unix))]
pub fn check_permissions(path: &Path) -> Result<(), WSError> {
log::debug!(
"Permission check skipped for '{}': not supported on this platform. \
On Windows, ensure proper ACLs are set for sensitive files.",
path.display()
);
Ok(())
}
#[cfg(unix)]
pub fn set_secure_permissions(path: &Path) -> Result<(), WSError> {
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(path)?.permissions();
perms.set_mode(SECURE_FILE_MODE);
fs::set_permissions(path, perms)?;
Ok(())
}
#[cfg(not(unix))]
pub fn set_secure_permissions(path: &Path) -> Result<(), WSError> {
log::warn!(
"Cannot set restrictive file permissions for '{}': not supported on this platform. \
Ensure proper access controls are configured for sensitive files.",
path.display()
);
Ok(())
}
#[cfg(unix)]
pub fn create_secure_file(path: &Path) -> Result<File, WSError> {
use std::os::unix::fs::OpenOptionsExt;
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(SECURE_FILE_MODE)
.open(path)?;
Ok(file)
}
#[cfg(not(unix))]
pub fn create_secure_file(path: &Path) -> Result<File, WSError> {
log::warn!(
"Creating file '{}' without restrictive permissions: not supported on this platform. \
Ensure proper access controls are configured for sensitive files.",
path.display()
);
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)?;
Ok(file)
}
pub fn write_secure(path: &Path, data: &[u8]) -> Result<(), WSError> {
let mut file = create_secure_file(path)?;
file.write_all(data)?;
file.sync_all()?;
#[cfg(unix)]
{
set_secure_permissions(path)?;
}
Ok(())
}
pub fn write_secure_string(path: &Path, content: &str) -> Result<(), WSError> {
write_secure(path, content.as_bytes())
}
pub fn read_secure(path: &Path) -> Result<Vec<u8>, WSError> {
check_permissions(path)?;
let mut file = File::open(path)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
Ok(contents)
}
pub fn read_secure_string(path: &Path) -> Result<String, WSError> {
let contents = read_secure(path)?;
String::from_utf8(contents).map_err(|e| {
WSError::InternalError(format!("Invalid UTF-8 in secure file: {}", e))
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
fn temp_path(name: &str) -> std::path::PathBuf {
env::temp_dir().join(format!("wsc_test_secure_file_{}", name))
}
#[test]
fn test_write_and_read_secure() {
let path = temp_path("write_read.key");
let data = b"test secret data";
write_secure(&path, data).unwrap();
let read_data = read_secure(&path).unwrap();
assert_eq!(read_data, data);
fs::remove_file(&path).ok();
}
#[test]
fn test_write_and_read_secure_string() {
let path = temp_path("write_read_str.key");
let content = "test secret string content";
write_secure_string(&path, content).unwrap();
let read_content = read_secure_string(&path).unwrap();
assert_eq!(read_content, content);
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_secure_permissions_set_correctly() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("perms.key");
let data = b"test data";
write_secure(&path, data).unwrap();
let metadata = fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, SECURE_FILE_MODE, "File should have mode 0600");
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_check_permissions_secure() {
let path = temp_path("check_secure.key");
write_secure(&path, b"test").unwrap();
let result = check_permissions(&path);
assert!(result.is_ok());
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_check_permissions_insecure_logs_warning() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("check_insecure.key");
fs::write(&path, b"test").unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o644); fs::set_permissions(&path, perms).unwrap();
let result = check_permissions(&path);
assert!(result.is_ok());
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_set_secure_permissions() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("set_perms.key");
fs::write(&path, b"test").unwrap();
set_secure_permissions(&path).unwrap();
let metadata = fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, SECURE_FILE_MODE);
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_create_secure_file() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("create_secure.key");
let mut file = create_secure_file(&path).unwrap();
file.write_all(b"test data").unwrap();
drop(file);
let metadata = fs::metadata(&path).unwrap();
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(mode, SECURE_FILE_MODE);
fs::remove_file(&path).ok();
}
#[test]
fn test_write_secure_creates_parent_dirs_not() {
let path = temp_path("nonexistent_dir/file.key");
let result = write_secure(&path, b"test");
assert!(result.is_err());
}
#[test]
fn test_read_secure_nonexistent_file() {
let path = temp_path("nonexistent.key");
let result = read_secure(&path);
assert!(result.is_err());
}
#[test]
fn test_empty_file() {
let path = temp_path("empty.key");
write_secure(&path, b"").unwrap();
let read_data = read_secure(&path).unwrap();
assert!(read_data.is_empty());
fs::remove_file(&path).ok();
}
#[test]
fn test_large_file() {
let path = temp_path("large.key");
let data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
write_secure(&path, &data).unwrap();
let read_data = read_secure(&path).unwrap();
assert_eq!(read_data, data);
fs::remove_file(&path).ok();
}
#[test]
fn test_overwrite_existing_file() {
let path = temp_path("overwrite.key");
write_secure(&path, b"initial data").unwrap();
write_secure(&path, b"new data").unwrap();
let read_data = read_secure(&path).unwrap();
assert_eq!(read_data, b"new data");
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_overwrite_preserves_secure_permissions() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("overwrite_perms.key");
write_secure(&path, b"initial").unwrap();
let mode1 = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode1, SECURE_FILE_MODE);
write_secure(&path, b"new data").unwrap();
let mode2 = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode2, SECURE_FILE_MODE);
fs::remove_file(&path).ok();
}
#[cfg(unix)]
#[test]
fn test_read_insecure_file_still_reads() {
use std::os::unix::fs::PermissionsExt;
let path = temp_path("read_insecure.key");
fs::write(&path, b"secret data").unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o777); fs::set_permissions(&path, perms).unwrap();
let result = read_secure(&path);
assert!(result.is_ok());
assert_eq!(result.unwrap(), b"secret data");
fs::remove_file(&path).ok();
}
}