hive-c0re: stop silently dropping errors on credential/marker writes
Add secret_file::write_secret_0600: create the parent dir, write a sibling temp file opened create_new + mode 0600, sync_all, rename over the target. Use it for the matrix sender token, the space/chat room-id files and the forge core token, which previously wrote at the process umask, then chmod-ed with the error discarded, and dropped the create_dir_all error. agent_identity::write_identity uses it too instead of its own open-0600 loop; that loop opened with create + truncate, so a pre-existing file kept its old mode. Also log instead of discarding: - the manager's applied-rev marker write in auto_update - the knowledge `git remote set-url` scrub (spawn error and non-zero exit; the exit status only, since stderr may echo the old URL) - the parent create_dir_all in paths::move_if_legacy Closes #5037
This commit is contained in:
parent
54226e027d
commit
f53ccb2740
8 changed files with 119 additions and 53 deletions
79
hive-c0re/src/secret_file.rs
Normal file
79
hive-c0re/src/secret_file.rs
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//! [`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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue