1use std::fs::File;
9use std::io::BufReader;
10use std::path::PathBuf;
11
12use clap::{ArgAction, Parser, Subcommand};
13use clap_serde_derive::ClapSerde;
14use libc::{MCL_CURRENT, MCL_FUTURE, PR_SET_DUMPABLE, mlockall, prctl};
15use log::debug;
16use nix::sys::resource::{Resource, setrlimit};
17use passless_config_doc::ConfigDoc;
18use serde::{Deserialize, Serialize};
19
20pub fn local_path() -> String {
22 dirs::data_dir()
23 .expect("Could not determine data directory: $XDG_DATA_HOME or $HOME/.local/share")
24 .join("passless/local")
25 .to_string_lossy()
26 .into_owned()
27}
28
29#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)]
31#[group(id = "local-backend-config")]
32pub struct LocalBackendConfig {
33 #[arg(
35 long = "local-path",
36 env = "PASSLESS_LOCAL_PATH",
37 id = "local-path",
38 value_name = "PATH"
39 )]
40 #[serde(default)]
41 #[default(local_path())]
42 pub path: String,
43}
44
45pub fn pass_store_path() -> String {
47 dirs::home_dir()
48 .expect("Could not determine home directory: $HOME")
49 .join(".password-store")
50 .to_string_lossy()
51 .into_owned()
52}
53#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)]
55#[group(id = "pass-backend-config")]
56pub struct PassBackendConfig {
57 #[arg(
59 long = "pass-store-path",
60 env = "PASSLESS_PASS_STORE_PATH",
61 id = "pass-store-path",
62 value_name = "PATH"
63 )]
64 #[serde(default)]
65 #[default(pass_store_path())]
66 pub store_path: String,
67
68 #[arg(
70 long = "pass-path",
71 env = "PASSLESS_PASS_PATH",
72 id = "pass-path",
73 value_name = "PATH"
74 )]
75 #[serde(default)]
76 #[default("fido2".to_string())]
77 pub path: String,
78
79 #[arg(
81 long = "pass-gpg-backend",
82 env = "PASSLESS_PASS_GPG_BACKEND",
83 value_name = "BACKEND"
84 )]
85 #[serde(default)]
86 #[default("gnupg-bin".to_string())]
87 pub gpg_backend: String,
88}
89
90pub fn tpm_path() -> String {
92 dirs::data_dir()
93 .expect("Could not determine data directory: $XDG_DATA_HOME or $HOME/.local/share")
94 .join("passless/tpm")
95 .to_string_lossy()
96 .into_owned()
97}
98
99#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)]
101#[group(id = "tpm-backend-config")]
102pub struct TpmBackendConfig {
103 #[arg(
105 long = "tpm-path",
106 env = "PASSLESS_TPM_PATH",
107 id = "tpm-path",
108 value_name = "PATH"
109 )]
110 #[serde(default)]
111 #[default(tpm_path())]
112 pub path: String,
113
114 #[arg(long = "tpm-tcti", env = "PASSLESS_TPM_TCTI", value_name = "TCTI")]
116 #[serde(default)]
117 #[default("device:/dev/tpmrm0".to_string())]
118 pub tcti: String,
119}
120
121#[derive(ClapSerde, Debug, Clone, Serialize, Deserialize, ConfigDoc)]
123#[group(id = "security")]
124pub struct SecurityConfig {
125 #[arg(long = "use-mlock", env = "PASSLESS_USE_MLOCK")]
127 #[serde(default)]
128 #[default(true)]
129 pub use_mlock: bool,
130
131 #[arg(long = "disable-core-dumps", env = "PASSLESS_DISABLE_CORE_DUMPS")]
133 #[serde(default)]
134 #[default(true)]
135 pub disable_core_dumps: bool,
136
137 #[arg(
139 long = "constant-signature-counter",
140 env = "PASSLESS_CONSTANT_SIGNATURE_COUNTER",
141 action = ArgAction::Set,
142 require_equals = true,
143 num_args = 0..=1,
144 default_missing_value = "true"
145 )]
146 #[serde(default)]
147 #[default(false)]
148 pub constant_signature_counter: bool,
149
150 #[arg(
152 long = "user-verification-registration",
153 env = "PASSLESS_USER_VERIFICATION_REGISTRATION"
154 )]
155 #[serde(default)]
156 #[default(true)]
157 pub user_verification_registration: bool,
158
159 #[arg(
161 long = "user-verification-authentication",
162 env = "PASSLESS_USER_VERIFICATION_AUTHENTICATION"
163 )]
164 #[serde(default)]
165 #[default(true)]
166 pub user_verification_authentication: bool,
167}
168
169impl SecurityConfig {
170 pub fn apply_hardening(&self) -> Result<(), Box<dyn std::error::Error>> {
172 if self.disable_core_dumps {
173 self.disable_core_dumps_impl()?;
174 }
175 if self.use_mlock {
176 self.lock_all_memory()?;
177 }
178 Ok(())
179 }
180
181 fn disable_core_dumps_impl(&self) -> Result<(), Box<dyn std::error::Error>> {
183 debug!("Disabling core dumps to prevent credential leakage");
184 setrlimit(Resource::RLIMIT_CORE, 0, 0)?;
185 let r = unsafe { prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) };
186 if r != 0 {
187 log::warn!("prctl(PR_SET_DUMPABLE) failed: {}", r);
188 }
189 Ok(())
190 }
191
192 fn lock_all_memory(&self) -> Result<(), Box<dyn std::error::Error>> {
194 debug!("Locking all memory to prevent swapping");
195 let r = unsafe { mlockall(MCL_CURRENT | MCL_FUTURE) };
196 if r != 0 {
197 return Err(format!(
198 "mlockall failed (errno {}).\n\
199 Hint: increase DefaultLimitMEMLOCK at /etc/systemd/system.conf and /etc/systemd/user.conf level.\n\
200 Hint: grant CAP_IPC_LOCK to the binary with: 'sudo setcap cap_ipc_lock=+ep $(which passless)'",
201 std::io::Error::last_os_error()
202 )
203 .into());
204 }
205 Ok(())
206 }
207}
208
209#[derive(ClapSerde, Serialize, Deserialize, Debug, ConfigDoc)]
212pub struct AppConfig {
213 #[arg(short = 't', long = "backend-type", env = "PASSLESS_BACKEND_TYPE")]
215 #[serde(default)]
216 #[default("local".to_string())]
217 pub backend_type: String,
218
219 #[arg(
222 short,
223 long,
224 env = "PASSLESS_VERBOSE",
225 action = ArgAction::Set,
226 require_equals = true,
227 num_args = 0..=1,
228 default_missing_value = "true"
229 )]
230 #[default(true)]
231 #[serde(default)]
232 pub verbose: bool,
233
234 #[clap_serde]
236 #[serde(default)]
237 #[command(flatten)]
238 pub local: LocalBackendConfig,
239
240 #[clap_serde]
242 #[serde(default)]
243 #[command(flatten)]
244 pub pass: PassBackendConfig,
245
246 #[clap_serde]
248 #[serde(default)]
249 #[command(flatten)]
250 pub tpm: TpmBackendConfig,
251
252 #[clap_serde]
254 #[serde(default)]
255 #[command(flatten)]
256 pub security: SecurityConfig,
257}
258
259#[derive(Debug, Clone)]
261pub enum BackendConfig {
262 Local {
263 path: String,
264 },
265 Pass {
266 store_path: String,
267 path: String,
268 gpg_backend: String,
269 },
270 Tpm {
271 path: String,
272 tcti: String,
273 },
274}
275
276impl AppConfig {
277 pub fn load(args: &mut Args) -> Self {
279 let default_config_path = dirs::config_dir().map(|p| p.join("passless/config.toml"));
281
282 let config_file_path = args
283 .config_path
284 .as_ref()
285 .or(default_config_path.as_ref())
286 .filter(|p| p.exists());
287
288 if let Some(path) = config_file_path
289 && let Ok(f) = File::open(path)
290 {
291 log::info!("Loading configuration from: {}", path.display());
292 let content = std::io::read_to_string(BufReader::new(f)).unwrap_or_default();
293 match toml::from_str::<AppConfig>(&content) {
294 Ok(file_config) => {
295 return file_config.merge(&mut args.config);
297 }
298 Err(e) => log::warn!("Failed to parse config file {}: {}", path.display(), e),
299 }
300 }
301
302 AppConfig::from(&mut args.config)
304 }
305
306 pub fn backend(&self) -> crate::error::Result<BackendConfig> {
308 match self.backend_type.as_str() {
309 "local" => Ok(BackendConfig::Local {
310 path: self.local.path.clone(),
311 }),
312 "pass" => Ok(BackendConfig::Pass {
313 store_path: self.pass.store_path.clone(),
314 path: self.pass.path.clone(),
315 gpg_backend: self.pass.gpg_backend.clone(),
316 }),
317 "tpm" => Ok(BackendConfig::Tpm {
318 path: self.tpm.path.clone(),
319 tcti: self.tpm.tcti.clone(),
320 }),
321 _ => Err(crate::error::Error::Config(format!(
322 "Invalid backend_type '{}'. Must be one of: local, pass, tpm",
323 self.backend_type
324 ))),
325 }
326 }
327
328 pub fn apply_security_hardening(&self) -> Result<(), Box<dyn std::error::Error>> {
330 self.security.apply_hardening()
331 }
332
333 pub fn security_config(&self) -> SecurityConfig {
335 self.security.clone()
336 }
337}
338
339#[derive(Parser)]
341#[command(author, version, about)]
342pub struct Args {
343 #[arg(short, long, env = "PASSLESS_CONFIG")]
345 pub config_path: Option<PathBuf>,
346
347 #[command(flatten)]
349 pub config: <AppConfig as ClapSerde>::Opt,
350
351 #[command(subcommand)]
353 pub command: Option<Commands>,
354}
355
356#[derive(Subcommand, Debug, Clone)]
358pub enum Commands {
359 Config {
361 #[command(subcommand)]
362 action: ConfigAction,
363 },
364}
365
366#[derive(Subcommand, Debug, Clone)]
368pub enum ConfigAction {
369 Print,
371}