fireblocks_config/
config.rs1#[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 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 if let Some(ref key) = self.secret {
55 return Ok(key.clone().into_bytes());
56 }
57
58 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 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 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 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}