]> git.proxmox.com Git - proxmox-perl-rs.git/blame - pmg-rs/src/tfa.rs
pmg: add tfa_lock_status_query and api_unlock_tfa
[proxmox-perl-rs.git] / pmg-rs / src / tfa.rs
CommitLineData
83ac3450
WB
1//! This implements the `tfa.cfg` parser & TFA API calls for PMG.
2//!
3//! The exported `PMG::RS::TFA` perl package provides access to rust's `TfaConfig`.
4//! Contrary to the PVE implementation, this does not need to provide any backward compatible
5//! entries.
6//!
7//! NOTE: In PMG the tfa config is behind `PVE::INotify`'s `ccache`, so PMG sets it to `noclone` in
8//! order to avoid losing the rust magic-ref.
9
10use std::fs::File;
11use std::io::{self, Read};
12use std::os::unix::fs::OpenOptionsExt;
13use std::os::unix::io::{AsRawFd, RawFd};
14use std::path::{Path, PathBuf};
15
16use anyhow::{bail, format_err, Error};
17use nix::errno::Errno;
18use nix::sys::stat::Mode;
19
20pub(self) use proxmox_tfa::api::{
9fdb289d
WB
21 RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, UserChallengeAccess,
22 WebauthnConfig,
83ac3450
WB
23};
24
25#[perlmod::package(name = "PMG::RS::TFA")]
26mod export {
aed16575 27 use std::collections::HashMap;
83ac3450
WB
28 use std::convert::TryInto;
29 use std::sync::Mutex;
30
31 use anyhow::{bail, format_err, Error};
32 use serde_bytes::ByteBuf;
33 use url::Url;
34
35 use perlmod::Value;
72140ad5 36 use proxmox_tfa::api::{methods, TfaResult};
83ac3450
WB
37
38 use super::{TfaConfig, UserAccess};
39
40 perlmod::declare_magic!(Box<Tfa> : &Tfa as "PMG::RS::TFA");
41
42 /// A TFA Config instance.
43 pub struct Tfa {
44 inner: Mutex<TfaConfig>,
45 }
46
47 /// Prevent 'dclone'.
48 #[export(name = "STORABLE_freeze", raw_return)]
49 fn storable_freeze(#[try_from_ref] _this: &Tfa, _cloning: bool) -> Result<Value, Error> {
50 bail!("freezing TFA config not supported!");
51 }
52
53 /// Parse a TFA configuration.
54 #[export(raw_return)]
55 fn new(#[raw] class: Value, config: &[u8]) -> Result<Value, Error> {
56 let mut inner: TfaConfig = serde_json::from_slice(config)
57 .map_err(|err| format_err!("failed to parse TFA file: {}", err))?;
58
59 // PMG does not support U2F.
60 inner.u2f = None;
61 Ok(perlmod::instantiate_magic!(
62 &class, MAGIC => Box::new(Tfa { inner: Mutex::new(inner) })
63 ))
64 }
65
66 /// Write the configuration out into a JSON string.
67 #[export]
68 fn write(#[try_from_ref] this: &Tfa) -> Result<serde_bytes::ByteBuf, Error> {
69 let inner = this.inner.lock().unwrap();
70 Ok(ByteBuf::from(serde_json::to_vec(&*inner)?))
71 }
72
73 /// Debug helper: serialize the TFA user data into a perl value.
74 #[export]
75 fn to_perl(#[try_from_ref] this: &Tfa) -> Result<Value, Error> {
76 let inner = this.inner.lock().unwrap();
77 Ok(perlmod::to_value(&*inner)?)
78 }
79
80 /// Get a list of all the user names in this config.
81 /// PMG uses this to verify users and purge the invalid ones.
82 #[export]
83 fn users(#[try_from_ref] this: &Tfa) -> Result<Vec<String>, Error> {
84 Ok(this.inner.lock().unwrap().users.keys().cloned().collect())
85 }
86
87 /// Remove a user from the TFA configuration.
88 #[export]
89 fn remove_user(#[try_from_ref] this: &Tfa, userid: &str) -> Result<bool, Error> {
90 Ok(this.inner.lock().unwrap().users.remove(userid).is_some())
91 }
92
93 /// Get the TFA data for a specific user.
94 #[export(raw_return)]
95 fn get_user(#[try_from_ref] this: &Tfa, userid: &str) -> Result<Value, perlmod::Error> {
96 perlmod::to_value(&this.inner.lock().unwrap().users.get(userid))
97 }
98
99 /// Add a u2f registration. This modifies the config (adds the user to it), so it needs be
100 /// written out.
101 #[export]
102 fn add_u2f_registration(
103 #[raw] raw_this: Value,
104 //#[try_from_ref] this: &Tfa,
105 userid: &str,
106 description: String,
107 ) -> Result<String, Error> {
108 let this: &Tfa = (&raw_this).try_into()?;
109 let mut inner = this.inner.lock().unwrap();
9fdb289d 110 inner.u2f_registration_challenge(&UserAccess::new(&raw_this)?, userid, description)
83ac3450
WB
111 }
112
113 /// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
114 /// needs to be written out!
115 #[export]
116 fn finish_u2f_registration(
117 #[raw] raw_this: Value,
118 //#[try_from_ref] this: &Tfa,
119 userid: &str,
120 challenge: &str,
121 response: &str,
122 ) -> Result<String, Error> {
123 let this: &Tfa = (&raw_this).try_into()?;
124 let mut inner = this.inner.lock().unwrap();
9fdb289d 125 inner.u2f_registration_finish(&UserAccess::new(&raw_this)?, userid, challenge, response)
83ac3450
WB
126 }
127
128 /// Check if a user has any TFA entries of a given type.
129 #[export]
130 fn has_type(#[try_from_ref] this: &Tfa, userid: &str, typename: &str) -> Result<bool, Error> {
131 Ok(match this.inner.lock().unwrap().users.get(userid) {
132 Some(user) => match typename {
133 "totp" | "oath" => !user.totp.is_empty(),
134 "u2f" => !user.u2f.is_empty(),
135 "webauthn" => !user.webauthn.is_empty(),
136 "yubico" => !user.yubico.is_empty(),
137 "recovery" => match &user.recovery {
138 Some(r) => r.count_available() > 0,
139 None => false,
140 },
141 _ => bail!("unrecognized TFA type {:?}", typename),
142 },
143 None => false,
144 })
145 }
146
147 /// Generates a space separated list of yubico keys of this account.
148 #[export]
149 fn get_yubico_keys(#[try_from_ref] this: &Tfa, userid: &str) -> Result<Option<String>, Error> {
150 Ok(this.inner.lock().unwrap().users.get(userid).map(|user| {
151 user.enabled_yubico_entries()
152 .fold(String::new(), |mut s, k| {
153 if !s.is_empty() {
154 s.push(' ');
155 }
156 s.push_str(k);
157 s
158 })
159 }))
160 }
161
162 #[export]
163 fn set_u2f_config(#[try_from_ref] this: &Tfa, config: Option<super::U2fConfig>) {
164 this.inner.lock().unwrap().u2f = config;
165 }
166
167 #[export]
168 fn set_webauthn_config(
169 #[try_from_ref] this: &Tfa,
170 config: Option<super::WebauthnConfig>,
171 ) -> Result<(), Error> {
172 this.inner.lock().unwrap().webauthn = config.map(TryInto::try_into).transpose()?;
173 Ok(())
174 }
175
176 #[export]
177 fn get_webauthn_config(
178 #[try_from_ref] this: &Tfa,
179 ) -> Result<(Option<String>, Option<super::WebauthnConfig>), Error> {
180 Ok(match this.inner.lock().unwrap().webauthn.clone() {
181 Some(config) => (Some(hex::encode(&config.digest())), Some(config.into())),
182 None => (None, None),
183 })
184 }
185
186 #[export]
187 fn has_webauthn_origin(#[try_from_ref] this: &Tfa) -> bool {
188 match &this.inner.lock().unwrap().webauthn {
189 Some(wa) => wa.origin.is_some(),
190 None => false,
191 }
192 }
193
194 /// Create an authentication challenge.
195 ///
196 /// Returns the challenge as a json string.
197 /// Returns `undef` if no second factor is configured.
198 #[export]
199 fn authentication_challenge(
200 #[raw] raw_this: Value,
201 //#[try_from_ref] this: &Tfa,
202 userid: &str,
203 origin: Option<Url>,
204 ) -> Result<Option<String>, Error> {
205 let this: &Tfa = (&raw_this).try_into()?;
206 let mut inner = this.inner.lock().unwrap();
207 match inner.authentication_challenge(
9fdb289d 208 &UserAccess::new(&raw_this)?,
83ac3450
WB
209 userid,
210 origin.as_ref(),
211 )? {
212 Some(challenge) => Ok(Some(serde_json::to_string(&challenge)?)),
213 None => Ok(None),
214 }
215 }
216
217 /// Get the recovery state (suitable for a challenge object).
218 #[export]
219 fn recovery_state(#[try_from_ref] this: &Tfa, userid: &str) -> Option<super::RecoveryState> {
220 this.inner
221 .lock()
222 .unwrap()
223 .users
224 .get(userid)
72140ad5 225 .and_then(|user| user.recovery_state())
83ac3450
WB
226 }
227
228 /// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
229 /// it.
230 ///
231 /// NOTE: This returns a boolean whether the config data needs to be *saved* after this call
232 /// (to use up recovery keys!).
233 #[export]
234 fn authentication_verify(
235 #[raw] raw_this: Value,
236 //#[try_from_ref] this: &Tfa,
237 userid: &str,
238 challenge: &str, //super::TfaChallenge,
239 response: &str,
240 origin: Option<Url>,
241 ) -> Result<bool, Error> {
242 let this: &Tfa = (&raw_this).try_into()?;
243 let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
244 let response: super::TfaResponse = response.parse()?;
245 let mut inner = this.inner.lock().unwrap();
72140ad5
WB
246 let result = inner.verify(
247 &UserAccess::new(&raw_this)?,
248 userid,
249 &challenge,
250 response,
251 origin.as_ref(),
252 );
253 match result {
254 TfaResult::Success { needs_saving } => Ok(needs_saving),
255 _ => bail!("TFA authentication failed"),
256 }
83ac3450
WB
257 }
258
e8857729
WB
259 /// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
260 /// it.
261 ///
262 /// Returns a result hash of the form:
263 /// ```text
264 /// {
265 /// "result": bool, // whether TFA was successful
266 /// "needs-saving": bool, // whether the user config needs saving
267 /// "tfa-limit-reached": bool, // whether the TFA limit was reached (config needs saving)
268 /// "totp-limit-reached": bool, // whether the TOTP limit was reached (config needs saving)
269 /// }
270 /// ```
271 #[export]
272 fn authentication_verify2(
273 #[raw] raw_this: Value,
274 //#[try_from_ref] this: &Tfa,
275 userid: &str,
276 challenge: &str, //super::TfaChallenge,
277 response: &str,
278 origin: Option<Url>,
279 ) -> Result<TfaReturnValue, Error> {
280 let this: &Tfa = (&raw_this).try_into()?;
281 let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
282 let response: super::TfaResponse = response.parse()?;
283 let mut inner = this.inner.lock().unwrap();
284 let result = inner.verify(
285 &UserAccess::new(&raw_this)?,
286 userid,
287 &challenge,
288 response,
289 origin.as_ref(),
290 );
291 Ok(match result {
292 TfaResult::Success { needs_saving } => TfaReturnValue {
293 result: true,
294 needs_saving,
295 ..Default::default()
296 },
297 TfaResult::Locked => TfaReturnValue::default(),
298 TfaResult::Failure {
299 needs_saving,
300 totp_limit_reached,
301 tfa_limit_reached,
302 } => TfaReturnValue {
303 result: false,
304 needs_saving,
305 totp_limit_reached,
306 tfa_limit_reached,
307 },
308 })
309 }
310
311 #[derive(Default, serde::Serialize)]
312 #[serde(rename_all = "kebab-case")]
313 struct TfaReturnValue {
314 result: bool,
315 needs_saving: bool,
316 totp_limit_reached: bool,
317 tfa_limit_reached: bool,
318 }
319
83ac3450
WB
320 /// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
321 #[export]
322 fn get_current_totp_value(otp_uri: &str) -> Result<String, Error> {
323 let totp: proxmox_tfa::totp::Totp = otp_uri.parse()?;
324 Ok(totp.time(std::time::SystemTime::now())?.to_string())
325 }
326
327 #[export]
328 fn api_list_user_tfa(
329 #[try_from_ref] this: &Tfa,
330 userid: &str,
331 ) -> Result<Vec<methods::TypedTfaInfo>, Error> {
332 methods::list_user_tfa(&this.inner.lock().unwrap(), userid)
333 }
334
335 #[export]
336 fn api_get_tfa_entry(
337 #[try_from_ref] this: &Tfa,
338 userid: &str,
339 id: &str,
340 ) -> Option<methods::TypedTfaInfo> {
341 methods::get_tfa_entry(&this.inner.lock().unwrap(), userid, id)
342 }
343
344 /// Returns `true` if the user still has other TFA entries left, `false` if the user has *no*
345 /// more tfa entries.
346 #[export]
347 fn api_delete_tfa(#[try_from_ref] this: &Tfa, userid: &str, id: String) -> Result<bool, Error> {
348 let mut this = this.inner.lock().unwrap();
349 match methods::delete_tfa(&mut this, userid, &id) {
350 Ok(has_entries_left) => Ok(has_entries_left),
351 Err(methods::EntryNotFound) => bail!("no such entry"),
352 }
353 }
354
355 #[export]
356 fn api_list_tfa(
357 #[try_from_ref] this: &Tfa,
358 authid: &str,
359 top_level_allowed: bool,
360 ) -> Result<Vec<methods::TfaUser>, Error> {
361 methods::list_tfa(&this.inner.lock().unwrap(), authid, top_level_allowed)
362 }
363
364 #[export]
365 fn api_add_tfa_entry(
366 #[raw] raw_this: Value,
367 //#[try_from_ref] this: &Tfa,
368 userid: &str,
369 description: Option<String>,
370 totp: Option<String>,
371 value: Option<String>,
372 challenge: Option<String>,
373 ty: methods::TfaType,
374 origin: Option<Url>,
375 ) -> Result<methods::TfaUpdateInfo, Error> {
376 let this: &Tfa = (&raw_this).try_into()?;
377 methods::add_tfa_entry(
378 &mut this.inner.lock().unwrap(),
9fdb289d 379 &UserAccess::new(&raw_this)?,
83ac3450
WB
380 userid,
381 description,
382 totp,
383 value,
384 challenge,
385 ty,
386 origin.as_ref(),
387 )
388 }
389
390 /// Add a totp entry without validating it, used for user.cfg keys.
391 /// Returns the ID.
392 #[export]
393 fn add_totp_entry(
394 #[try_from_ref] this: &Tfa,
395 userid: &str,
396 description: String,
397 totp: String,
398 ) -> Result<String, Error> {
399 Ok(this
400 .inner
401 .lock()
402 .unwrap()
403 .add_totp(userid, description, totp.parse()?))
404 }
405
406 /// Add a yubico entry without validating it, used for user.cfg keys.
407 /// Returns the ID.
408 #[export]
409 fn add_yubico_entry(
410 #[try_from_ref] this: &Tfa,
411 userid: &str,
412 description: String,
413 yubico: String,
414 ) -> String {
415 this.inner
416 .lock()
417 .unwrap()
418 .add_yubico(userid, description, yubico)
419 }
420
421 #[export]
422 fn api_update_tfa_entry(
423 #[try_from_ref] this: &Tfa,
424 userid: &str,
425 id: &str,
426 description: Option<String>,
427 enable: Option<bool>,
428 ) -> Result<(), Error> {
429 match methods::update_tfa_entry(
430 &mut this.inner.lock().unwrap(),
431 userid,
432 id,
433 description,
434 enable,
435 ) {
436 Ok(()) => Ok(()),
437 Err(methods::EntryNotFound) => bail!("no such entry"),
438 }
439 }
aed16575
WB
440
441 #[export]
442 fn api_unlock_tfa(#[try_from_ref] this: &Tfa, userid: &str) -> Result<bool, Error> {
443 Ok(methods::unlock_tfa(
444 &mut this.inner.lock().unwrap(),
445 userid,
446 )?)
447 }
448
449 #[derive(serde::Serialize)]
450 #[serde(rename_all = "kebab-case")]
451 struct TfaLockStatus {
452 /// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use
453 /// a recovery key.
454 #[serde(skip_serializing_if = "bool_is_false", default)]
455 totp_locked: bool,
456
457 /// If a user hits too many 2nd factor failures, they get completely blocked for a while.
458 #[serde(skip_serializing_if = "Option::is_none", default)]
459 #[serde(deserialize_with = "filter_expired_timestamp")]
460 tfa_locked_until: Option<i64>,
461 }
462
463 impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus {
464 fn from(data: &proxmox_tfa::api::TfaUserData) -> Self {
465 Self {
466 totp_locked: data.totp_locked,
467 tfa_locked_until: data.tfa_locked_until,
468 }
469 }
470 }
471
472 fn bool_is_false(b: &bool) -> bool {
473 !*b
474 }
475
476 #[export]
477 fn tfa_lock_status(
478 #[try_from_ref] this: &Tfa,
479 userid: Option<&str>,
480 ) -> Result<Option<perlmod::Value>, Error> {
481 let this = this.inner.lock().unwrap();
482 if let Some(userid) = userid {
483 if let Some(user) = this.users.get(userid) {
484 Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?))
485 } else {
486 Ok(None)
487 }
488 } else {
489 Ok(Some(perlmod::to_value(
490 &HashMap::<String, TfaLockStatus>::from_iter(
491 this.users
492 .iter()
493 .map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))),
494 ),
495 )?))
496 }
497 }
83ac3450
WB
498}
499
500/// Attach the path to errors from [`nix::mkir()`].
501pub(crate) fn mkdir<P: AsRef<Path>>(path: P, mode: libc::mode_t) -> Result<(), Error> {
502 let path = path.as_ref();
503 match nix::unistd::mkdir(path, unsafe { Mode::from_bits_unchecked(mode) }) {
504 Ok(()) => Ok(()),
b67ff27d 505 Err(Errno::EEXIST) => Ok(()),
83ac3450
WB
506 Err(err) => bail!("failed to create directory {:?}: {}", path, err),
507 }
508}
509
510#[cfg(debug_assertions)]
511#[derive(Clone)]
512#[repr(transparent)]
513pub struct UserAccess(perlmod::Value);
514
515#[cfg(debug_assertions)]
516impl UserAccess {
517 #[inline]
518 fn new(value: &perlmod::Value) -> Result<Self, Error> {
519 value
520 .dereference()
521 .ok_or_else(|| format_err!("bad TFA config object"))
522 .map(Self)
523 }
524
525 #[inline]
526 fn is_debug(&self) -> bool {
527 self.0
528 .as_hash()
529 .and_then(|v| v.get("-debug"))
530 .map(|v| v.iv() != 0)
531 .unwrap_or(false)
532 }
533}
534
535#[cfg(not(debug_assertions))]
536#[derive(Clone, Copy)]
537#[repr(transparent)]
538pub struct UserAccess;
539
540#[cfg(not(debug_assertions))]
541impl UserAccess {
542 #[inline]
543 const fn new(_value: &perlmod::Value) -> Result<Self, std::convert::Infallible> {
544 Ok(Self)
545 }
546
547 #[inline]
548 const fn is_debug(&self) -> bool {
549 false
550 }
551}
552
553/// Build the path to the challenge data file for a user.
554fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
555 if debug {
556 PathBuf::from(format!("./local-tfa-challenges/{}", userid))
557 } else {
558 PathBuf::from(format!("/run/pmg-private/tfa-challenges/{}", userid))
559 }
560}
561
562impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
9fdb289d 563 fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
83ac3450
WB
564 if self.is_debug() {
565 mkdir("./local-tfa-challenges", 0o700)?;
566 } else {
567 mkdir("/run/pmg-private", 0o700)?;
568 mkdir("/run/pmg-private/tfa-challenges", 0o700)?;
569 }
570
571 let path = challenge_data_path(userid, self.is_debug());
572
573 let mut file = std::fs::OpenOptions::new()
574 .create(true)
575 .read(true)
576 .write(true)
577 .truncate(false)
578 .mode(0o600)
579 .open(&path)
580 .map_err(|err| format_err!("failed to create challenge file {:?}: {}", &path, err))?;
581
582 UserChallengeData::lock_file(file.as_raw_fd())?;
583
584 // the file may be empty, so read to a temporary buffer first:
585 let mut data = Vec::with_capacity(4096);
586
587 file.read_to_end(&mut data).map_err(|err| {
588 format_err!("failed to read challenge data for user {}: {}", userid, err)
589 })?;
590
591 let inner = if data.is_empty() {
592 Default::default()
593 } else {
594 match serde_json::from_slice(&data) {
595 Ok(inner) => inner,
596 Err(err) => {
597 eprintln!(
598 "failed to parse challenge data for user {}: {}",
599 userid, err
600 );
601 Default::default()
602 }
603 }
604 };
605
9fdb289d 606 Ok(Box::new(UserChallengeData {
83ac3450
WB
607 inner,
608 path,
609 lock: file,
9fdb289d 610 }))
83ac3450
WB
611 }
612
613 /// `open` without creating the file if it doesn't exist, to finish WA authentications.
9fdb289d 614 fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
83ac3450
WB
615 let path = challenge_data_path(userid, self.is_debug());
616
617 let mut file = match std::fs::OpenOptions::new()
618 .read(true)
619 .write(true)
620 .truncate(false)
621 .mode(0o600)
622 .open(&path)
623 {
624 Ok(file) => file,
625 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
626 Err(err) => return Err(err.into()),
627 };
628
629 UserChallengeData::lock_file(file.as_raw_fd())?;
630
631 let inner = serde_json::from_reader(&mut file).map_err(|err| {
632 format_err!("failed to read challenge data for user {}: {}", userid, err)
633 })?;
634
9fdb289d 635 Ok(Some(Box::new(UserChallengeData {
83ac3450
WB
636 inner,
637 path,
638 lock: file,
9fdb289d 639 })))
83ac3450
WB
640 }
641
642 fn remove(&self, userid: &str) -> Result<bool, Error> {
643 let path = challenge_data_path(userid, self.is_debug());
644 match std::fs::remove_file(&path) {
645 Ok(()) => Ok(true),
646 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
647 Err(err) => Err(err.into()),
648 }
649 }
72140ad5 650
e8857729
WB
651 // TODO: enable once we have UI/API admin stuff to unlock locked accounts
652 fn enable_lockout(&self) -> bool {
653 false
72140ad5 654 }
83ac3450
WB
655}
656
657/// Container of `TfaUserChallenges` with the corresponding file lock guard.
658///
659/// Basically provides the TFA API to the REST server by persisting, updating and verifying active
660/// challenges.
661pub struct UserChallengeData {
662 inner: proxmox_tfa::api::TfaUserChallenges,
663 path: PathBuf,
664 lock: File,
665}
666
667impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
668 fn get_mut(&mut self) -> &mut proxmox_tfa::api::TfaUserChallenges {
669 &mut self.inner
670 }
671
9fdb289d 672 fn save(&mut self) -> Result<(), Error> {
83ac3450
WB
673 UserChallengeData::save(self)
674 }
675}
676
677impl UserChallengeData {
678 fn lock_file(fd: RawFd) -> Result<(), Error> {
679 let rc = unsafe { libc::flock(fd, libc::LOCK_EX) };
680
681 if rc != 0 {
682 let err = io::Error::last_os_error();
683 bail!("failed to lock tfa user challenge data: {}", err);
684 }
685
686 Ok(())
687 }
688
689 /// Rewind & truncate the file for an update.
690 fn rewind(&mut self) -> Result<(), Error> {
691 use std::io::{Seek, SeekFrom};
692
693 let pos = self.lock.seek(SeekFrom::Start(0))?;
694 if pos != 0 {
695 bail!(
696 "unexpected result trying to rewind file, position is {}",
697 pos
698 );
699 }
700
701 let rc = unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) };
702 if rc != 0 {
703 let err = io::Error::last_os_error();
704 bail!("failed to truncate challenge data: {}", err);
705 }
706
707 Ok(())
708 }
709
710 /// Save the current data. Note that we do not replace the file here since we lock the file
711 /// itself, as it is in `/run`, and the typical error case for this particular situation
712 /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
713 /// other reasons then...
714 ///
715 /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
716 /// way also unlocks early.
9fdb289d 717 fn save(&mut self) -> Result<(), Error> {
83ac3450
WB
718 self.rewind()?;
719
720 serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {
721 format_err!("failed to update challenge file {:?}: {}", self.path, err)
722 })?;
723
724 Ok(())
725 }
726}