pub(crate) fn write_secret_file(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
let parent = path
.parent()
.ok_or_else(|| std::io::Error::other("secret path has no parent directory"))?;
let name = path
.file_name()
.ok_or_else(|| std::io::Error::other("secret path has no file name"))?
.to_string_lossy();
let mut nonce = [0u8; 8];
getrandom::fill(&mut nonce).map_err(std::io::Error::other)?;
let tmp = parent.join(format!(
".{name}.tmp.{}.{}",
std::process::id(),
hex::encode(nonce)
));
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
let result = (|| {
let mut f = opts.open(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
drop(f);
crate::core::atomic_fs::replace_file(&tmp, path)
})();
if result.is_err() {
let _ = std::fs::remove_file(&tmp);
}
result
}
pub fn private_dir(dir: &std::path::Path) -> std::io::Result<()> {
let step = |what: &str, e: std::io::Error| {
std::io::Error::new(e.kind(), format!("{what} {}: {e}", dir.display()))
};
std::fs::create_dir_all(dir).map_err(|e| step("create", e))?;
restrict_to_owner(dir).map_err(|e| step("restrict", e))
}
pub fn restrict_to_owner(path: &std::path::Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
if std::fs::metadata(path)?.permissions().mode() & 0o7777 != 0o700 {
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
}
}
#[cfg(not(unix))]
let _ = path;
Ok(())
}
pub fn write_private(dir: &std::path::Path, name: &str, bytes: &[u8]) -> std::io::Result<()> {
private_dir(dir)?;
write_secret_file(&dir.join(name), bytes).map_err(|e| {
std::io::Error::new(e.kind(), format!("write {name} in {}: {e}", dir.display()))
})
}
#[cfg(test)]
mod tests {
use super::write_secret_file;
#[test]
fn concurrent_writers_of_one_path_all_succeed_and_leave_one_whole_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("secret.json");
let writers: Vec<_> = (0..8u8)
.map(|i| {
let path = path.clone();
std::thread::spawn(move || write_secret_file(&path, &[i; 64]))
})
.collect();
for writer in writers {
writer.join().unwrap().unwrap();
}
let written = std::fs::read(&path).unwrap();
assert_eq!(written.len(), 64);
assert!(written.iter().all(|byte| *byte == written[0]));
let names: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
assert_eq!(names, ["secret.json"], "no temporary file is left");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o600);
}
}
#[cfg(unix)]
#[test]
fn restrict_to_owner_leaves_a_private_dir_untouched() {
use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("data");
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
let ctime = |p: &std::path::Path| {
let m = std::fs::metadata(p).unwrap();
(m.ctime(), m.ctime_nsec())
};
let before = ctime(&dir);
std::thread::sleep(std::time::Duration::from_millis(1100));
super::restrict_to_owner(&dir).unwrap();
super::private_dir(&dir).unwrap();
crate::core::data_dir::ensure_dir_permissions(&dir);
assert_eq!(
ctime(&dir),
before,
"an already private dir is not chmodded"
);
}
#[cfg(unix)]
#[test]
fn restrict_to_owner_still_tightens_an_open_dir() {
use std::os::unix::fs::PermissionsExt as _;
let tmp = tempfile::tempdir().unwrap();
let mode =
|p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o7777;
for (i, restrict) in [
(|p: &std::path::Path| super::restrict_to_owner(p).unwrap()) as fn(&std::path::Path),
|p| super::private_dir(p).unwrap(),
|p| crate::core::data_dir::ensure_dir_permissions(p),
]
.into_iter()
.enumerate()
{
let dir = tmp.path().join(format!("open-{i}"));
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
restrict(&dir);
assert_eq!(mode(&dir), 0o700);
}
}
}