use std::fs::{DirBuilder, File, Metadata, OpenOptions};
use std::io::{self, Error, ErrorKind};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::os::unix::prelude::OpenOptionsExt;
use std::path::Path;
enum Op {
Read = 4,
Write = 2,
Exec = 1,
}
enum Category {
Owner = 2,
Group = 1,
World = 0,
}
fn mode(who: Category, what: Op) -> u32 {
(what as u32) << (3 * who as u32)
}
pub fn secure_open(path: impl AsRef<Path>) -> io::Result<File> {
let mut open_options = OpenOptions::new();
open_options.read(true);
secure_open_impl(path.as_ref(), &mut open_options, false, false)
}
pub fn secure_open_cookie_file(path: impl AsRef<Path>) -> io::Result<File> {
let mut open_options = OpenOptions::new();
open_options
.read(true)
.write(true)
.create(true)
.mode(mode(Category::Owner, Op::Write) | mode(Category::Owner, Op::Read));
secure_open_impl(path.as_ref(), &mut open_options, true, true)
}
fn checks(path: &Path, meta: Metadata) -> io::Result<()> {
let error = |msg| Error::new(ErrorKind::PermissionDenied, msg);
let path_mode = meta.permissions().mode();
if meta.uid() != 0 {
Err(error(format!("{} must be owned by root", path.display())))
} else if meta.gid() != 0 && (path_mode & mode(Category::Group, Op::Write) != 0) {
Err(error(format!(
"{} cannot be group-writable",
path.display()
)))
} else if path_mode & mode(Category::World, Op::Write) != 0 {
Err(error(format!(
"{} cannot be world-writable",
path.display()
)))
} else {
Ok(())
}
}
fn secure_open_impl(
path: &Path,
open_options: &mut OpenOptions,
check_parent_dir: bool,
create_parent_dirs: bool,
) -> io::Result<File> {
let error = |msg| Error::new(ErrorKind::PermissionDenied, msg);
if check_parent_dir || create_parent_dirs {
if let Some(parent_dir) = path.parent() {
if create_parent_dirs && !parent_dir.exists() {
DirBuilder::new()
.recursive(true)
.mode(
mode(Category::Owner, Op::Write)
| mode(Category::Owner, Op::Read)
| mode(Category::Owner, Op::Exec)
| mode(Category::Group, Op::Exec)
| mode(Category::World, Op::Exec),
)
.create(parent_dir)?;
}
if check_parent_dir {
let parent_meta = std::fs::metadata(parent_dir)?;
checks(parent_dir, parent_meta)?;
}
} else {
return Err(error(format!(
"{} has no valid parent directory",
path.display()
)));
}
}
let file = open_options.open(path)?;
let meta = file.metadata()?;
checks(path, meta)?;
Ok(file)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn secure_open_is_predictable() {
assert!(std::fs::File::open("/etc/hosts").is_ok());
assert!(secure_open("/etc/hosts").is_ok());
assert!(std::fs::File::open("/var/log/wtmp").is_ok());
assert!(secure_open("/var/log/wtmp").is_err());
assert!(std::fs::File::open("/etc/shadow").is_err());
assert!(secure_open("/etc/shadow").is_err());
}
#[test]
fn test_secure_open_cookie_file() {
assert!(secure_open_cookie_file("/etc/hosts").is_err());
}
}