327 lines
10 KiB
Rust
327 lines
10 KiB
Rust
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<BackupLockGuard, Error> {
|
|
open_backup_lockfile(LOCK_FILE, None, false)
|
|
}
|
|
|
|
pub fn write_lock() -> Result<BackupLockGuard, Error> {
|
|
open_backup_lockfile(LOCK_FILE, None, true)
|
|
}
|
|
|
|
/// Read the TFA entries.
|
|
pub fn read() -> Result<TfaConfig, Error> {
|
|
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<Option<(WebauthnConfig, [u8; 32])>, 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", 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<Option<TfaChallenge>, 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<String, Error> {
|
|
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<Vec<String>, 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<String, Error> {
|
|
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<String, Error> {
|
|
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<String, Error> {
|
|
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<String, Error> {
|
|
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<Self::Data, Error> {
|
|
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<Option<Self::Data>, 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<bool, Error> {
|
|
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)
|
|
}
|
|
}
|