Skip to main content

orch/
secret_ref.rs

1//! Config values that name a secret instead of holding it, so the config file holds no secret.
2//! `passbox:<name>` is read once through passbox, any other value is used as written
3
4/// Blocking, it waits on passbox's Touch ID prompt, so call it from `spawn_blocking`
5pub fn resolve(value: &str) -> anyhow::Result<String> {
6    resolve_with("passbox", value)
7}
8
9fn resolve_with(passbox: &str, value: &str) -> anyhow::Result<String> {
10    let Some(name) = value.strip_prefix("passbox:") else {
11        return Ok(value.to_string());
12    };
13
14    // `exec --stdin` hands the value to a child, so it works without a terminal and does not
15    // depend on a flag of `get` that passbox versions differ on
16    let out = std::process::Command::new(passbox)
17        .args(["exec", "--stdin", name, "--", "/bin/cat"])
18        .stdin(std::process::Stdio::null())
19        .output()?;
20
21    if !out.status.success() {
22        let reason = String::from_utf8_lossy(&out.stderr);
23        let reason = reason.lines().next().unwrap_or("no error output").trim();
24        anyhow::bail!("passbox could not read {name}: {reason}");
25    }
26
27    // The first line is the secret, later `key: value` lines are fields
28    let text = String::from_utf8(out.stdout)?;
29    let secret = text.lines().next().unwrap_or_default().to_string();
30    anyhow::ensure!(!secret.is_empty(), "passbox returned no value for {name}");
31    Ok(secret)
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    fn stub(dir: &std::path::Path, script: &str) -> String {
39        use std::os::unix::fs::PermissionsExt;
40        let path = dir.join("passbox");
41        std::fs::write(&path, format!("#!/bin/sh\n{script}\n")).unwrap();
42        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
43        path.to_string_lossy().into_owned()
44    }
45
46    #[test]
47    fn a_plain_value_is_used_as_written() {
48        assert_eq!(resolve_with("/nonexistent", "plain").unwrap(), "plain");
49    }
50
51    #[test]
52    fn a_passbox_reference_reads_the_first_line() {
53        let dir = tempfile::tempdir().unwrap();
54        let bin = stub(
55            dir.path(),
56            r#"[ "$1 $2 $3 $4 $5" = "exec --stdin orch/relay-token -- /bin/cat" ] && printf 's3cret\nnote: x\n'"#,
57        );
58        assert_eq!(
59            resolve_with(&bin, "passbox:orch/relay-token").unwrap(),
60            "s3cret"
61        );
62    }
63
64    #[test]
65    fn a_failed_or_empty_read_is_an_error() {
66        let dir = tempfile::tempdir().unwrap();
67        let err = resolve_with(
68            &stub(dir.path(), "echo 'error: denied' >&2; exit 1"),
69            "passbox:x",
70        )
71        .unwrap_err()
72        .to_string();
73        assert!(
74            err.contains("passbox could not read x: error: denied"),
75            "{err}"
76        );
77        let empty = tempfile::tempdir().unwrap();
78        assert!(resolve_with(&stub(empty.path(), "true"), "passbox:x").is_err());
79    }
80}