fireblocks_config/
config.rs

1#[cfg(feature = "gpg")]
2use gpgme::{Context, Protocol};
3use {
4    crate::{Error, OutputFormat, Result},
5    config::{Config, File, FileFormat},
6    serde::Deserialize,
7    std::{
8        fs,
9        path::{Path, PathBuf},
10    },
11};
12
13fn expand_tilde(path: &str) -> PathBuf {
14    if path.starts_with('~') {
15        match dirs::home_dir() {
16            Some(mut home) => {
17                home.push(&path[2..]);
18                home
19            }
20            None => PathBuf::from(path),
21        }
22    } else {
23        PathBuf::from(path)
24    }
25}
26
27#[derive(Clone, Debug, Default, Deserialize)]
28pub struct DisplayConfig {
29    pub output: OutputFormat,
30}
31
32#[derive(Clone, Debug, Default, Deserialize)]
33pub struct Signer {
34    pub poll_timeout: Option<u16>,
35    pub poll_interval: Option<u16>,
36    /// The vault id, default is "0"
37    pub vault: String,
38}
39
40#[derive(Clone, Debug, Default, Deserialize)]
41pub struct FireblocksConfig {
42    pub api_key: String,
43    pub url: String,
44    pub secret_path: Option<PathBuf>,
45    pub secret: Option<String>,
46    #[serde(rename = "display")]
47    pub display_config: DisplayConfig,
48    pub signer: Signer,
49}
50
51impl FireblocksConfig {
52    pub fn get_key(&self) -> Result<Vec<u8>> {
53        // Try secret_key first (simpler case)
54        if let Some(ref key) = self.secret {
55            return Ok(key.clone().into_bytes());
56        }
57
58        // Then try secret_path
59        let path = self.secret_path.as_ref().ok_or(Error::MissingSecret)?;
60        let expanded_path = if path.starts_with("~") {
61            expand_tilde(&path.to_string_lossy())
62        } else {
63            path.clone()
64        };
65
66        #[cfg(feature = "gpg")]
67        if expanded_path
68            .extension()
69            .map_or(false, |ext| ext.eq_ignore_ascii_case("gpg"))
70        {
71            return self.decrypt_gpg_file(&expanded_path);
72        }
73
74        // Regular file read
75        fs::read(&expanded_path).map_err(|e| Error::IOError {
76            source: e,
77            path: expanded_path.to_string_lossy().to_string(),
78        })
79    }
80
81    #[cfg(feature = "gpg")]
82    fn decrypt_gpg_file(&self, path: &Path) -> Result<Vec<u8>> {
83        let mut ctx = Context::from_protocol(Protocol::OpenPgp)?;
84
85        let mut input = fs::File::open(path).map_err(|e| Error::IOError {
86            source: e,
87            path: path.to_string_lossy().to_string(),
88        })?;
89
90        let mut output = Vec::new();
91        ctx.decrypt(&mut input, &mut output)?;
92
93        Ok(output)
94    }
95}
96impl FireblocksConfig {
97    pub fn new<P: AsRef<Path>>(cfg: P, cfg_overrides: &[P]) -> Result<Self> {
98        let cfg_path = cfg.as_ref();
99        tracing::debug!("using config {}", cfg_path.display());
100
101        let mut config_builder =
102            Config::builder().add_source(File::new(&cfg_path.to_string_lossy(), FileFormat::Toml));
103
104        // Add all override files in order
105        for override_path in cfg_overrides {
106            let path = override_path.as_ref();
107            tracing::debug!("adding config override: {}", path.display());
108            config_builder = config_builder
109                .add_source(File::new(&path.to_string_lossy(), FileFormat::Toml).required(true));
110        }
111
112        // Environment variables still take highest precedence
113        config_builder = config_builder
114            .add_source(config::Environment::with_prefix("FIREBLOCKS").try_parsing(true));
115
116        let conf: Self = config_builder.build()?.try_deserialize()?;
117        tracing::trace!("loaded config {conf:#?}");
118        Ok(conf)
119    }
120
121    pub fn with_overrides<P: AsRef<Path>>(
122        cfg: P,
123        overrides: impl IntoIterator<Item = P>,
124    ) -> Result<Self> {
125        let override_vec: Vec<P> = overrides.into_iter().collect();
126        Self::new(cfg, &override_vec)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use {super::*, std::path::PathBuf};
133
134    #[ignore]
135    #[test]
136    fn test_gpg_config() -> anyhow::Result<()> {
137        let b = "examples/config-gpg.toml";
138        let cfg = FireblocksConfig::new(b, &[])?;
139        cfg.get_key()?;
140        Ok(())
141    }
142
143    #[test]
144    fn test_config() -> anyhow::Result<()> {
145        let b = "examples/default.toml";
146        let cfg = FireblocksConfig::new(b, &[])?;
147        assert_eq!("blah", cfg.api_key);
148        assert!(cfg.secret_path.is_some());
149        if let Some(p) = cfg.secret_path.as_ref() {
150            assert_eq!(PathBuf::from("examples/test.pem"), *p);
151        }
152        assert_eq!("https://sandbox-api.fireblocks.io/v1", cfg.url);
153        assert_eq!(OutputFormat::Table, cfg.display_config.output);
154        unsafe {
155            std::env::set_var("FIREBLOCKS_SECRET", "override");
156        }
157        let cfg = FireblocksConfig::new(b, &[])?;
158        assert!(cfg.secret.is_some());
159        assert_eq!(String::from("override").as_bytes(), cfg.get_key()?);
160        if let Some(ref k) = cfg.secret_path {
161            assert_eq!(PathBuf::from("examples/test.pem"), *k);
162        }
163
164        assert_eq!(cfg.signer.vault, "0");
165        unsafe {
166            std::env::remove_var("FIREBLOCKS_SECRET");
167        }
168        Ok(())
169    }
170
171    #[test]
172    fn test_config_override() -> anyhow::Result<()> {
173        let b = "examples/default.toml";
174        let cfg_override = "examples/override.toml";
175        let cfg = FireblocksConfig::with_overrides(b, vec![cfg_override])?;
176        assert_eq!("production", cfg.api_key);
177        assert!(cfg.secret_path.is_some());
178        if let Some(p) = cfg.secret_path.as_ref() {
179            assert_eq!(PathBuf::from("examples/test.pem"), *p);
180        }
181        assert_eq!("https://api.fireblocks.io/v1", cfg.url);
182        assert_eq!(OutputFormat::Table, cfg.display_config.output);
183        Ok(())
184    }
185
186    #[test]
187    fn test_embedded_key() -> anyhow::Result<()> {
188        let b = "examples/default.toml";
189        let cfg_override = "examples/embedded.toml";
190        let cfg = FireblocksConfig::new(b, &[cfg_override])?;
191        assert!(cfg.secret.is_some());
192        let secret = cfg.secret.unwrap();
193        assert_eq!(String::from("i am a secret").as_bytes(), secret.as_bytes());
194        Ok(())
195    }
196}