//! [`write_secret_0600`]: the one way hive-c0re puts a credential on disk. use std::io::Write as _; use std::os::unix::fs::OpenOptionsExt as _; use std::path::Path; use anyhow::{Context as _, Result}; /// Write `contents` to `path` as a `0600` file, creating the parent directory. /// /// The bytes go to a sibling temp file opened `0600` and are renamed over /// `path`, so the file is never readable by anyone else for an instant, and a /// reader (or a crash) never sees half of it. Errors carry paths, never /// `contents`. pub fn write_secret_0600(path: &Path, contents: &[u8]) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; } let mut tmp_name = path .file_name() .with_context(|| format!("{} has no file name", path.display()))? .to_owned(); tmp_name.push(".tmp"); let tmp = path.with_file_name(tmp_name); // A temp file left by a crashed earlier write would fail `create_new`. let _ = std::fs::remove_file(&tmp); let mut f = std::fs::OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&tmp) .with_context(|| format!("create {}", tmp.display()))?; f.write_all(contents) .and_then(|()| f.sync_all()) .with_context(|| format!("write {}", tmp.display()))?; std::fs::rename(&tmp, path).with_context(|| format!("rename onto {}", path.display())) } #[cfg(test)] mod tests { use super::write_secret_0600; use std::os::unix::fs::PermissionsExt as _; fn mode(path: &std::path::Path) -> u32 { std::fs::metadata(path).expect("stat").permissions().mode() & 0o777 } #[test] fn creates_parent_and_writes_0600() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("sub").join("token"); write_secret_0600(&path, b"secret\n").expect("writes"); assert_eq!(std::fs::read(&path).expect("read"), b"secret\n"); assert_eq!(mode(&path), 0o600); assert!(!dir.path().join("sub").join("token.tmp").exists()); } /// An existing file at a looser mode is replaced, not rewritten in place: /// the new file is 0600 and holds only the new bytes. #[test] fn replaces_an_existing_looser_file() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("token"); std::fs::write(&path, "a much longer old value\n").expect("seed"); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).expect("chmod"); write_secret_0600(&path, b"new\n").expect("writes"); assert_eq!(std::fs::read(&path).expect("read"), b"new\n"); assert_eq!(mode(&path), 0o600); } #[test] fn a_stale_temp_file_does_not_block_the_write() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("token"); std::fs::write(dir.path().join("token.tmp"), "stale").expect("seed"); write_secret_0600(&path, b"new\n").expect("writes"); assert_eq!(std::fs::read(&path).expect("read"), b"new\n"); } }