//! The `SecretStore`: a secret from a systemd credential, an environment variable, or an owner-only //! file (M4a spec, section 4; the brief after P15). A `Secret` cannot be printed. use std::ffi::OsString; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::{Path, PathBuf}; use zeroize::Zeroizing; use crate::config::SecretSource; pub const RUNBOOK: &str = "see docs/runbook.md#secret-unavailable"; pub const RUNBOOK_FILE: &str = "see docs/runbook.md#secret-in-a-file"; /// A secret's text. No `Display`; `Debug` shows nothing of it; wiped when dropped. pub struct Secret(Zeroizing); impl Secret { pub fn new(text: String) -> Secret { Secret(Zeroizing::new(text)) } /// The text, for the one place that must send it. pub fn expose(&self) -> &str { &self.0 } } impl std::fmt::Debug for Secret { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("Secret(…)") } } /// A loaded secret, and the warning to print for it, if any. #[derive(Debug)] pub struct Loaded { pub secret: Secret, pub warning: Option, } #[derive(Debug)] pub struct SecretError { pub name: String, pub why: String, } impl std::fmt::Display for SecretError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "secret {}: {}\n{RUNBOOK}", self.name, self.why) } } impl std::error::Error for SecretError {} /// Load secret `name` from `source`. `env` reads an environment variable (in `gatewayd`, /// `std::env::var_os`); tests pass their own. pub fn load( name: &str, source: &SecretSource, env: &dyn Fn(&str) -> Option, ) -> Result { // By source (spec section 4). Credential: read $CREDENTIALS_DIRECTORY/ (through `env`, // not std::env); an unset variable, or a file that cannot be read, is an error. Env: the // variable, UTF-8; unset is an error. File: `check_file` first, then read it, and set `warning` // to the exact text in the task. Every value goes through `value`. Every error is a // `SecretError` naming the secret, never the value. todo!() } /// An owner-only regular file, not a link. fn check_file(path: &Path) -> Result<(), String> { // In this order, each its own error: not absolute; `symlink_metadata` fails; a symbolic link; // not a regular file; owner uid differs from the uid of /proc/self; mode & 0o077 != 0. todo!() } /// The text without one trailing newline; not empty; UTF-8. fn value(bytes: Vec) -> Result { // UTF-8 (else an error), one trailing newline removed, not empty. Keep it in `Zeroizing` // throughout. todo!() }