use std::fs::File; use std::io::{self, Read, Seek, SeekFrom}; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::AsRawFd; use std::path::PathBuf; use anyhow::{bail, format_err, Error}; use nix::sys::stat::Mode; use proxmox_sys::error::SysError; use proxmox_sys::fs::CreateOptions; use proxmox_tfa::totp::Totp; pub use proxmox_tfa::api::{ TfaChallenge, TfaConfig, TfaResponse, WebauthnConfig, WebauthnConfigUpdater, }; use pbs_api_types::{User, Userid}; use pbs_buildcfg::configdir; use pbs_config::{open_backup_lockfile, BackupLockGuard}; const CONF_FILE: &str = configdir!("/tfa.json"); const LOCK_FILE: &str = configdir!("/tfa.json.lock"); const CHALLENGE_DATA_PATH: &str = pbs_buildcfg::rundir!("/tfa/challenges"); pub fn read_lock() -> Result { open_backup_lockfile(LOCK_FILE, None, false) } pub fn write_lock() -> Result { open_backup_lockfile(LOCK_FILE, None, true) } /// Read the TFA entries. pub fn read() -> Result { let file = match File::open(CONF_FILE) { Ok(file) => file, Err(ref err) if err.not_found() => return Ok(TfaConfig::default()), Err(err) => return Err(err.into()), }; Ok(serde_json::from_reader(file)?) } pub(crate) fn webauthn_config_digest(config: &WebauthnConfig) -> Result<[u8; 32], Error> { let digest_data = pbs_tools::json::to_canonical_json(&serde_json::to_value(config)?)?; Ok(openssl::sha::sha256(&digest_data)) } /// Get the webauthn config with a digest. /// /// This is meant only for configuration updates, which currently only means webauthn updates. /// Since this is meant to be done only once (since changes will lock out users), this should be /// used rarely, since the digest calculation is currently a bit more involved. pub fn webauthn_config() -> Result, Error> { Ok(match read()?.webauthn { Some(wa) => { let digest = webauthn_config_digest(&wa)?; Some((wa, digest)) } None => None, }) } /// Requires the write lock to be held. pub fn write(data: &TfaConfig) -> Result<(), Error> { let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600)); let json = serde_json::to_vec(data)?; proxmox_sys::fs::replace_file(CONF_FILE, &json, options, true) } /// Cleanup non-existent users from the tfa config. pub fn cleanup_users(data: &mut TfaConfig, config: &proxmox_section_config::SectionConfigData) { data.users .retain(|user, _| config.lookup::("user", user.as_str()).is_ok()); } /// Container of `TfaUserChallenges` with the corresponding file lock guard. /// /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate. pub struct TfaUserChallengeData { inner: proxmox_tfa::api::TfaUserChallenges, path: PathBuf, lock: File, } fn challenge_data_path_str(userid: &str) -> PathBuf { PathBuf::from(format!("{}/{}", CHALLENGE_DATA_PATH, userid)) } impl TfaUserChallengeData { /// Rewind & truncate the file for an update. fn rewind(&mut self) -> Result<(), Error> { let pos = self.lock.seek(SeekFrom::Start(0))?; if pos != 0 { bail!( "unexpected result trying to rewind file, position is {}", pos ); } proxmox_sys::c_try!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) }); Ok(()) } /// Save the current data. Note that we do not replace the file here since we lock the file /// itself, as it is in `/run`, and the typical error case for this particular situation /// (machine loses power) simply prevents some login, but that'll probably fail anyway for /// other reasons then... /// /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this /// way also unlocks early. fn save(mut self) -> Result<(), Error> { self.rewind()?; serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| { format_err!("failed to update challenge file {:?}: {}", self.path, err) })?; Ok(()) } } /// Get an optional TFA challenge for a user. pub fn login_challenge(userid: &Userid) -> Result, Error> { let _lock = write_lock()?; read()?.authentication_challenge(UserAccess, userid.as_str(), None) } /// Add a TOTP entry for a user. Returns the ID. pub fn add_totp(userid: &Userid, description: String, value: Totp) -> Result { let _lock = write_lock(); let mut data = read()?; let id = data.add_totp(userid.as_str(), description, value); write(&data)?; Ok(id) } /// Add recovery tokens for the user. Returns the token list. pub fn add_recovery(userid: &Userid) -> Result, Error> { let _lock = write_lock(); let mut data = read()?; let out = data.add_recovery(userid.as_str())?; write(&data)?; Ok(out) } /// Add a u2f registration challenge for a user. pub fn add_u2f_registration(userid: &Userid, description: String) -> Result { let _lock = crate::config::tfa::write_lock(); let mut data = read()?; let challenge = data.u2f_registration_challenge(UserAccess, userid.as_str(), description)?; write(&data)?; Ok(challenge) } /// Finish a u2f registration challenge for a user. pub fn finish_u2f_registration( userid: &Userid, challenge: &str, response: &str, ) -> Result { let _lock = crate::config::tfa::write_lock(); let mut data = read()?; let id = data.u2f_registration_finish(UserAccess, userid.as_str(), challenge, response)?; write(&data)?; Ok(id) } /// Add a webauthn registration challenge for a user. pub fn add_webauthn_registration(userid: &Userid, description: String) -> Result { let _lock = crate::config::tfa::write_lock(); let mut data = read()?; let challenge = data.webauthn_registration_challenge(UserAccess, userid.as_str(), description, None)?; write(&data)?; Ok(challenge) } /// Finish a webauthn registration challenge for a user. pub fn finish_webauthn_registration( userid: &Userid, challenge: &str, response: &str, ) -> Result { let _lock = crate::config::tfa::write_lock(); let mut data = read()?; let id = data.webauthn_registration_finish(UserAccess, userid.as_str(), challenge, response, None)?; write(&data)?; Ok(id) } /// Verify a TFA challenge. pub fn verify_challenge( userid: &Userid, challenge: &TfaChallenge, response: TfaResponse, ) -> Result<(), Error> { let _lock = crate::config::tfa::write_lock(); let mut data = read()?; if data .verify(UserAccess, userid.as_str(), challenge, response, None)? .needs_saving() { write(&data)?; } Ok(()) } #[derive(Clone, Copy)] #[repr(transparent)] pub struct UserAccess; /// Build th impl proxmox_tfa::api::OpenUserChallengeData for UserAccess { type Data = TfaUserChallengeData; /// Load the user's current challenges with the intent to create a challenge (create the file /// if it does not exist), and keep a lock on the file. fn open(&self, userid: &str) -> Result { crate::server::create_run_dir()?; let options = CreateOptions::new().perm(Mode::from_bits_truncate(0o0600)); proxmox_sys::fs::create_path(CHALLENGE_DATA_PATH, Some(options.clone()), Some(options)) .map_err(|err| { format_err!( "failed to crate challenge data dir {:?}: {}", CHALLENGE_DATA_PATH, err ) })?; let path = challenge_data_path_str(userid); let mut file = std::fs::OpenOptions::new() .create(true) .read(true) .write(true) .truncate(false) .mode(0o600) .open(&path) .map_err(|err| format_err!("failed to create challenge file {:?}: {}", path, err))?; proxmox_sys::fs::lock_file(&mut file, true, None)?; // the file may be empty, so read to a temporary buffer first: let mut data = Vec::with_capacity(4096); file.read_to_end(&mut data).map_err(|err| { format_err!("failed to read challenge data for user {}: {}", userid, err) })?; let inner = if data.is_empty() { Default::default() } else { match serde_json::from_slice(&data) { Ok(inner) => inner, Err(err) => { eprintln!( "failed to parse challenge data for user {}: {}", userid, err ); Default::default() } } }; Ok(TfaUserChallengeData { inner, path, lock: file, }) } /// `open` without creating the file if it doesn't exist, to finish WA authentications. fn open_no_create(&self, userid: &str) -> Result, Error> { let path = challenge_data_path_str(userid); let mut file = match std::fs::OpenOptions::new() .read(true) .write(true) .truncate(false) .mode(0o600) .open(&path) { Ok(file) => file, Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None), Err(err) => return Err(err.into()), }; proxmox_sys::fs::lock_file(&mut file, true, None)?; let inner = serde_json::from_reader(&mut file).map_err(|err| { format_err!("failed to read challenge data for user {}: {}", userid, err) })?; Ok(Some(TfaUserChallengeData { inner, path, lock: file, })) } /// `remove` user data if it exists. fn remove(&self, userid: &str) -> Result { let path = challenge_data_path_str(userid); match std::fs::remove_file(&path) { Ok(()) => Ok(true), Err(err) if err.not_found() => Ok(false), Err(err) => Err(err.into()), } } } impl proxmox_tfa::api::UserChallengeAccess for TfaUserChallengeData { fn get_mut(&mut self) -> &mut proxmox_tfa::api::TfaUserChallenges { &mut self.inner } fn save(self) -> Result<(), Error> { TfaUserChallengeData::save(self) } }