Skip to main content

hashiverse_server_lib/server/
passphrase.rs

1//! # Passphrase resolution for server secrets
2//!
3//! Finds the operator's passphrase (used to decrypt the persisted
4//! [`hashiverse_lib::tools::server_id::ServerId`]) in a platform-agnostic way, looking
5//! in this order:
6//!
7//! 1. an explicit file path supplied on the command line,
8//! 2. standard container-secret mounts (`/run/secrets/`, `/mnt/secrets/`,
9//!    `/etc/secrets/`) — Kubernetes, Docker Swarm and Podman all land here,
10//! 3. an environment variable — still supported for legacy non-container deployments.
11//!
12//! The resolved passphrase is wrapped in `secrecy::SecretString` so it is zeroised on
13//! drop and never accidentally logged.
14
15use std::fs;
16use secrecy::SecretString;
17
18/// Where to look for a passphrase file when none is given explicitly, most secure first:
19/// secret files mount-injected by the container, then the working directory.
20pub const DEFAULT_PASSPHRASE_PATHS: &[&str] = &[
21    "/run/secrets/hashiverse_passphrase",
22    "/run/secrets/HASHIVERSE_PASSPHRASE",
23    "/mnt/secrets/hashiverse_passphrase",
24    "/mnt/secrets/HASHIVERSE_PASSPHRASE",
25    "/etc/secrets/hashiverse_passphrase",
26    "/etc/secrets/HASHIVERSE_PASSPHRASE",
27    "./.hashiverse_passphrase",
28    "./.HASHIVERSE_PASSPHRASE",
29];
30
31pub fn get_passphrase(passphrase_path: Option<String>) -> anyhow::Result<SecretString> {
32    fn get_passphrase_from_file_if_exists(path: &str) -> Option<SecretString> {
33        let contents = fs::read_to_string(path);
34        match contents {
35            Ok(contents) => Some(SecretString::new(Box::from(contents))),
36            Err(_) => None
37        }
38    }
39
40    // If the user has specified a container-injected passphrase, use that or die
41    if let Some(passphrase_path) = passphrase_path {
42        let passphrase = get_passphrase_from_file_if_exists(&passphrase_path);
43        match passphrase {
44            Some(passphrase) => return Ok(passphrase),
45            None => anyhow::bail!("no passphrase found at {}", passphrase_path)
46        }
47    }
48
49    // Else try the most secure routes - a secret file mount-injected by the container
50    for default_passphrase_path in DEFAULT_PASSPHRASE_PATHS {
51        if let Some(passphrase) = get_passphrase_from_file_if_exists(default_passphrase_path) { return Ok(passphrase); }
52    }
53
54    // Else try an env variable
55    if let Ok(passphrase) = std::env::var("HASHIVERSE_PASSPHRASE") { return Ok(SecretString::new(Box::from(passphrase))); }
56
57    // Error!
58    anyhow::bail!("no passphrase found - please (at worst) set the HASHIVERSE_PASSPHRASE environment variable to something memorable");
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use secrecy::ExposeSecret;
65
66    // Regression: every default path used to start with a literal tab, so none was ever found.
67    #[test]
68    fn default_passphrase_paths_have_no_stray_whitespace() {
69        for default_passphrase_path in DEFAULT_PASSPHRASE_PATHS {
70            assert_eq!(default_passphrase_path.trim(), *default_passphrase_path, "whitespace in {:?}", default_passphrase_path);
71            assert!(default_passphrase_path.starts_with('/') || default_passphrase_path.starts_with("./"), "not a path: {:?}", default_passphrase_path);
72        }
73    }
74
75    #[test]
76    fn explicit_passphrase_path_is_read() {
77        let (_temp_dir, temp_dir_path) = hashiverse_lib::tools::tools::get_temp_dir().unwrap();
78        let passphrase_path = std::path::Path::new(&temp_dir_path).join("passphrase");
79        fs::write(&passphrase_path, "correct horse").unwrap();
80        let passphrase = get_passphrase(Some(passphrase_path.to_string_lossy().into_owned())).unwrap();
81        assert_eq!(passphrase.expose_secret(), "correct horse");
82    }
83
84    #[test]
85    fn missing_explicit_passphrase_path_is_an_error() {
86        let (_temp_dir, temp_dir_path) = hashiverse_lib::tools::tools::get_temp_dir().unwrap();
87        let passphrase_path = std::path::Path::new(&temp_dir_path).join("does-not-exist");
88        assert!(get_passphrase(Some(passphrase_path.to_string_lossy().into_owned())).is_err());
89    }
90}