diff options
Diffstat (limited to 'src/files.rs')
| -rw-r--r-- | src/files.rs | 85 |
1 files changed, 83 insertions, 2 deletions
diff --git a/src/files.rs b/src/files.rs index 0ec741e..bc8fb8e 100644 --- a/src/files.rs +++ b/src/files.rs @@ -167,20 +167,101 @@ fn sync_parent(path: &Path) { } } +/// A key file's bytes: read from `path`; or with `create`, if there is no file there, +/// `len` random bytes in a new one only its owner may read (then also `true`). Without +/// `create`, a missing file is an error saying to give `create_option` (`what`: which +/// key; `hint`: what a new one means). Never created where a file is: a mistyped path +/// stays an error unless asked. +pub(crate) fn key_file( + path: &Path, + what: &str, + create: bool, + create_option: &str, + hint: &str, + len: usize, +) -> anyhow::Result<(Vec<u8>, bool)> { + use anyhow::Context; + use std::io::Write; + match std::fs::read(path) { + Ok(bytes) => { + warn_if_readable_by_others(path, what); + Ok((bytes, false)) + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + if !create { + anyhow::bail!( + "The {what} file {} doesn't exist; add --{create_option} to create a new {what} there ({hint})", + path.display() + ); + } + let mut bytes = vec![0u8; len]; + getrandom::fill(&mut bytes) + .map_err(|error| anyhow::anyhow!("No randomness: {error}"))?; + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let written = (|| { + let mut file = options.open(path)?; + file.write_all(&bytes)?; + file.sync_all() + })(); + written.with_context(|| format!("Creating the {what} file {}", path.display()))?; + Ok((bytes, true)) + } + Err(error) => { + Err(error).with_context(|| format!("Reading the {what} from {}", path.display())) + } + } +} + +/// A key is a secret: a file others may read is worth a warning (not an error: a +/// group may be deliberate, e.g. shared with a deployment tool). `what`: which key. +pub(crate) fn warn_if_readable_by_others(path: &Path, what: &str) { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Ok(metadata) = std::fs::metadata(path) { + let mode = metadata.permissions().mode(); + if mode & 0o077 != 0 { + tracing::warn!( + path = %path.display(), + mode = format!("{:o}", mode & 0o777), + "The {what} file is readable by others than its owner; chmod 600 it" + ); + } + } + } + #[cfg(not(unix))] + let _ = (path, what); +} + fn with_suffix(path: &Path, suffix: &str) -> PathBuf { let mut name = path.as_os_str().to_owned(); name.push(suffix); PathBuf::from(name) } +/// An empty directory for the test `name`: `continuum-<name>-<process ID>` in the +/// temporary directory, emptied first. +#[cfg(test)] +pub(crate) fn scratch_dir(name: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("continuum-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir +} + #[cfg(test)] mod tests { use super::*; #[test] fn writes_reads_and_counts() { - let dir = std::env::temp_dir().join(format!("continuum-files-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + let dir = scratch_dir("files"); let path = dir.join("state.json"); let files = Files::default(); |
