crypt4ghfs/
config.rs

1use std::collections::HashSet;
2use std::convert::{TryFrom, TryInto};
3use std::fs::File;
4use std::io::Read;
5use std::path::Path;
6
7use crypt4gh::error::Crypt4GHError;
8use crypt4gh::Keys;
9use fuser::MountOption;
10use itertools::Itertools;
11use rpassword::prompt_password;
12use serde::Deserialize;
13
14use crate::error::Crypt4GHFSError;
15
16const PASSPHRASE: &str = "C4GH_PASSPHRASE";
17
18#[derive(Deserialize, Debug, Copy, Clone)]
19#[serde(rename_all = "UPPERCASE")]
20pub enum LogLevel {
21	#[serde(alias = "CRITICAL")]
22	Critical,
23	Warn,
24	Info,
25	Debug,
26	#[serde(alias = "NOTSET")]
27	Trace,
28}
29
30#[derive(Deserialize, Debug, Copy, Clone)]
31#[serde(rename_all = "snake_case")]
32pub enum Facility {
33	Kern,
34	User,
35	Mail,
36	Daemon,
37	Auth,
38	Syslog,
39	Lpr,
40	News,
41	Uucp,
42	Cron,
43	Authpriv,
44	Ftp,
45	Local0,
46	Local1,
47	Local2,
48	Local3,
49	Local4,
50	Local5,
51	Local6,
52	Local7,
53}
54
55#[derive(Deserialize, Debug)]
56pub struct Default {
57	extensions: Vec<String>,
58}
59
60#[derive(Deserialize, Debug)]
61pub struct Fuse {
62	options: Option<Vec<String>>,
63	rootdir: String,
64	cache_directories: Option<bool>,
65}
66
67#[derive(Deserialize, Debug)]
68pub struct Crypt4GH {
69	#[serde(rename = "seckey")]
70	seckey_path: Option<String>,
71	recipients: Option<Vec<String>>,
72	include_myself_as_recipient: Option<bool>,
73}
74
75#[derive(Deserialize, Debug)]
76pub struct LoggerConfig {
77	pub log_level: LogLevel,
78	pub use_syslog: bool,
79	pub log_facility: Option<Facility>,
80}
81
82#[derive(Deserialize, Debug)]
83#[serde(rename_all = "UPPERCASE")]
84pub struct Config {
85	default: Default,
86	pub logger: LoggerConfig,
87	fuse: Fuse,
88	crypt4gh: Crypt4GH,
89}
90
91impl Config {
92	pub fn new_with_defaults(rootdir: String, seckey_path: Option<String>) -> Self {
93		Self {
94			default: Default { extensions: vec![] },
95			fuse: Fuse {
96				rootdir,
97				options: Some(vec!["ro".into(), "default_permissions".into(), "auto_unmount".into()]),
98				cache_directories: Some(true),
99			},
100			crypt4gh: Crypt4GH {
101				seckey_path,
102				recipients: Some(vec![]),
103				include_myself_as_recipient: Some(true),
104			},
105			logger: LoggerConfig {
106				log_level: LogLevel::Info,
107				use_syslog: false,
108				log_facility: None,
109			},
110		}
111	}
112
113	#[must_use]
114	pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
115		self.default.extensions = extensions;
116		self
117	}
118
119	#[must_use]
120	pub const fn with_log_level(mut self, log_level: LogLevel) -> Self {
121		self.logger.log_level = log_level;
122		self
123	}
124
125	pub fn get_options(&self) -> Vec<MountOption> {
126		self.fuse.options.clone().map_or_else(
127			|| vec![MountOption::RW, MountOption::DefaultPermissions],
128			|options| {
129				options
130					.iter()
131					.map(String::as_str)
132					.map(str_to_mount_option)
133					.inspect(|option| {
134						log::info!("+ fuse option: {:?}", option);
135					})
136					.collect()
137			},
138		)
139	}
140
141	pub const fn get_cache(&self) -> bool {
142		if let Some(cache_directories) = self.fuse.cache_directories {
143			return cache_directories;
144		}
145		true
146	}
147
148	pub fn get_extensions(&self) -> Vec<String> {
149		self.default.extensions.clone()
150	}
151
152	pub fn get_secret_key(&self) -> Result<Option<Vec<u8>>, Crypt4GHFSError> {
153		match &self.crypt4gh.seckey_path {
154			Some(seckey_path_str) => {
155				let seckey_path = Path::new(&seckey_path_str);
156				log::info!("Loading secret key from {}", seckey_path.display());
157
158				if !seckey_path.is_file() {
159					return Err(Crypt4GHFSError::SecretNotFound(seckey_path.into()));
160				}
161
162				let callback: Box<dyn Fn() -> Result<String, Crypt4GHError>> = match std::env::var(PASSPHRASE) {
163					Ok(_) => {
164						log::warn!("Warning: Using a passphrase in an environment variable is insecure");
165						Box::new(|| std::env::var(PASSPHRASE).map_err(|e| Crypt4GHError::NoPassphrase(e.into())))
166					},
167					Err(_) => Box::new(|| {
168						prompt_password(format!("Passphrase for {}: ", seckey_path.display()))
169							.map_err(|e| Crypt4GHError::NoPassphrase(e.into()))
170					}),
171				};
172
173				let key = crypt4gh::keys::get_private_key(seckey_path, callback)
174					.map_err(|e| Crypt4GHFSError::SecretKeyError(e.to_string()))?;
175
176				Ok(Some(key))
177			},
178			None => Ok(None),
179		}
180	}
181
182	pub fn get_recipients(&self, seckey: &[u8]) -> HashSet<Keys> {
183		let recipient_paths = &self.crypt4gh.recipients.clone().unwrap_or_default();
184
185		let mut recipient_pubkeys: HashSet<_> = recipient_paths
186			.iter()
187			.map(Path::new)
188			.filter(|path| path.exists())
189			.filter_map(|path| {
190				log::debug!("Recipient pubkey path: {}", path.display());
191				crypt4gh::keys::get_public_key(path).ok()
192			})
193			.unique()
194			.map(|key| Keys {
195				method: 0,
196				privkey: seckey.to_vec(),
197				recipient_pubkey: key,
198			})
199			.collect();
200
201		if self.crypt4gh.include_myself_as_recipient.unwrap_or(true) {
202			let k = crypt4gh::keys::get_public_key_from_private_key(seckey)
203				.expect("Unable to extract public key from seckey");
204			recipient_pubkeys.insert(Keys {
205				method: 0,
206				privkey: seckey.to_vec(),
207				recipient_pubkey: k,
208			});
209		}
210
211		recipient_pubkeys
212	}
213
214	pub const fn get_log_level(&self) -> LogLevel {
215		self.logger.log_level
216	}
217
218	pub fn get_rootdir(&self) -> String {
219		self.fuse.rootdir.to_string()
220	}
221
222	pub fn from_file(mut config_file: File) -> Result<Self, Crypt4GHFSError> {
223		let mut config_string = String::new();
224		config_file
225			.read_to_string(&mut config_string)
226			.map_err(|e| Crypt4GHFSError::BadConfig(e.to_string()))?;
227		let config_toml = toml::from_str(config_string.as_str()).map_err(|e| Crypt4GHFSError::BadConfig(e.to_string()));
228		config_toml
229	}
230
231	pub fn get_facility(&self) -> syslog::Facility {
232		match self.logger.log_facility.unwrap_or(Facility::User) {
233			Facility::Kern => syslog::Facility::LOG_KERN,
234			Facility::User => syslog::Facility::LOG_USER,
235			Facility::Mail => syslog::Facility::LOG_MAIL,
236			Facility::Daemon => syslog::Facility::LOG_DAEMON,
237			Facility::Auth => syslog::Facility::LOG_AUTH,
238			Facility::Syslog => syslog::Facility::LOG_SYSLOG,
239			Facility::Lpr => syslog::Facility::LOG_LPR,
240			Facility::News => syslog::Facility::LOG_NEWS,
241			Facility::Uucp => syslog::Facility::LOG_UUCP,
242			Facility::Cron => syslog::Facility::LOG_CRON,
243			Facility::Authpriv => syslog::Facility::LOG_AUTHPRIV,
244			Facility::Ftp => syslog::Facility::LOG_FTP,
245			Facility::Local0 => syslog::Facility::LOG_LOCAL0,
246			Facility::Local1 => syslog::Facility::LOG_LOCAL1,
247			Facility::Local2 => syslog::Facility::LOG_LOCAL2,
248			Facility::Local3 => syslog::Facility::LOG_LOCAL3,
249			Facility::Local4 => syslog::Facility::LOG_LOCAL4,
250			Facility::Local5 => syslog::Facility::LOG_LOCAL5,
251			Facility::Local6 => syslog::Facility::LOG_LOCAL6,
252			Facility::Local7 => syslog::Facility::LOG_LOCAL7,
253		}
254	}
255
256	pub fn setup_logger(&self) -> Result<(), Crypt4GHFSError> {
257		let log_level: LogLevel = if let Ok(log_level_str) = std::env::var("RUST_LOG") {
258			log_level_str
259				.as_str()
260				.try_into()
261				.expect("Unable to parse RUST_LOG environment variable")
262		}
263		else {
264			let log_level = self.get_log_level();
265			let log_level_str = match log_level {
266				LogLevel::Critical => "error",
267				LogLevel::Warn => "warn",
268				LogLevel::Info => "info",
269				LogLevel::Debug => "debug",
270				LogLevel::Trace => "trace",
271			};
272			std::env::set_var("RUST_LOG", log_level_str);
273			log_level
274		};
275
276		// Choose logger
277		if self.logger.use_syslog {
278			syslog::init(self.get_facility(), log_level.into(), None)?;
279		}
280		else {
281			let _ = pretty_env_logger::try_init(); // Ignore error
282		}
283
284		Ok(())
285	}
286}
287
288impl TryFrom<&str> for LogLevel {
289	type Error = Crypt4GHFSError;
290
291	fn try_from(level: &str) -> Result<Self, Self::Error> {
292		match level {
293			"error" => Ok(Self::Critical),
294			"warn" => Ok(Self::Warn),
295			"info" => Ok(Self::Info),
296			"debug" => Ok(Self::Debug),
297			"trace" => Ok(Self::Trace),
298			_ => Err(Crypt4GHFSError::BadConfig("Wrong log level".into())),
299		}
300	}
301}
302
303impl From<LogLevel> for log::LevelFilter {
304	fn from(val: LogLevel) -> Self {
305		match val {
306			LogLevel::Critical => Self::Error,
307			LogLevel::Warn => Self::Warn,
308			LogLevel::Info => Self::Info,
309			LogLevel::Debug => Self::Debug,
310			LogLevel::Trace => Self::Trace,
311		}
312	}
313}
314
315fn str_to_mount_option(s: &str) -> MountOption {
316	match s {
317		"auto_unmount" => MountOption::AutoUnmount,
318		"allow_other" => MountOption::AllowOther,
319		"allow_root" => MountOption::AllowRoot,
320		"default_permissions" => MountOption::DefaultPermissions,
321		"dev" => MountOption::Dev,
322		"nodev" => MountOption::NoDev,
323		"suid" => MountOption::Suid,
324		"nosuid" => MountOption::NoSuid,
325		"ro" => MountOption::RO,
326		"rw" => MountOption::RW,
327		"exec" => MountOption::Exec,
328		"noexec" => MountOption::NoExec,
329		"atime" => MountOption::Atime,
330		"noatime" => MountOption::NoAtime,
331		"dirsync" => MountOption::DirSync,
332		"sync" => MountOption::Sync,
333		"async" => MountOption::Async,
334		x if x.starts_with("fsname=") => MountOption::FSName(x[7..].into()),
335		x if x.starts_with("subtype=") => MountOption::Subtype(x[8..].into()),
336		x => MountOption::CUSTOM(x.into()),
337	}
338}