Skip to main content

dove_core/
secrets.rs

1//! dove's scoped S3 credentials — the access key of the least-privilege IAM
2//! user `dove provision` mints. `share` / `ls` / `revoke` sign and act with
3//! this key, not your full account credentials. Lives at
4//! `~/.config/dove/secrets.toml`, mode 0600, and is never committed.
5
6use anyhow::{anyhow, Context, Result};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11pub struct Secrets {
12    pub access_key_id: String,
13    pub secret_access_key: String,
14    /// The full-tier gate secret (hex): the HMAC key that mints unforgeable share
15    /// ids. `share` reads it; the gate Lambda holds the same value to verify.
16    #[serde(default)]
17    pub gate_secret: Option<String>,
18}
19
20impl Secrets {
21    pub fn load() -> Result<Self> {
22        let path = secrets_path()?;
23        let text = std::fs::read_to_string(&path).map_err(|_| {
24            anyhow!(
25                "no dove credentials at {} — run `dove provision` first",
26                path.display()
27            )
28        })?;
29        toml::from_str(&text).map_err(|e| anyhow!("parsing dove secrets: {e}"))
30    }
31
32    pub fn save(&self) -> Result<()> {
33        let path = secrets_path()?;
34        std::fs::create_dir_all(path.parent().unwrap())
35            .with_context(|| format!("creating {}", path.parent().unwrap().display()))?;
36        let text = toml::to_string_pretty(self).context("serializing dove secrets")?;
37        std::fs::write(&path, text).with_context(|| format!("writing {}", path.display()))?;
38        set_private(&path)
39    }
40
41    pub fn exists() -> bool {
42        secrets_path().map(|p| p.exists()).unwrap_or(false)
43    }
44}
45
46/// Lock the secrets file down to the owner (0600) on Unix.
47#[cfg(unix)]
48fn set_private(path: &Path) -> Result<()> {
49    use std::os::unix::fs::PermissionsExt;
50    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
51        .with_context(|| format!("chmod 600 {}", path.display()))
52}
53#[cfg(not(unix))]
54fn set_private(_path: &Path) -> Result<()> {
55    Ok(())
56}
57
58fn secrets_path() -> Result<PathBuf> {
59    if let Ok(x) = std::env::var("XDG_CONFIG_HOME") {
60        if !x.is_empty() {
61            return Ok(PathBuf::from(x).join("dove/secrets.toml"));
62        }
63    }
64    let home = std::env::var("HOME").map_err(|_| anyhow!("HOME is not set"))?;
65    Ok(PathBuf::from(home).join(".config/dove/secrets.toml"))
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn round_trips_through_toml() {
74        let s = Secrets {
75            access_key_id: "AKIAEXAMPLE".into(),
76            secret_access_key: "shhh".into(),
77            gate_secret: Some("deadbeef".into()),
78        };
79        let text = toml::to_string(&s).unwrap();
80        assert_eq!(toml::from_str::<Secrets>(&text).unwrap(), s);
81    }
82}