2 use std
::io
::{self, Read, Seek, SeekFrom}
;
3 use std
::os
::unix
::fs
::OpenOptionsExt
;
4 use std
::os
::unix
::io
::AsRawFd
;
5 use std
::path
::PathBuf
;
7 use anyhow
::{bail, format_err, Error}
;
8 use nix
::sys
::stat
::Mode
;
10 use proxmox_sys
::error
::SysError
;
11 use proxmox_sys
::fs
::CreateOptions
;
12 use proxmox_tfa
::totp
::Totp
;
14 pub use proxmox_tfa
::api
::{
15 TfaChallenge
, TfaConfig
, TfaResponse
, WebauthnConfig
, WebauthnConfigUpdater
,
18 use pbs_api_types
::{User, Userid}
;
19 use pbs_buildcfg
::configdir
;
20 use pbs_config
::{open_backup_lockfile, BackupLockGuard}
;
22 const CONF_FILE
: &str = configdir
!("/tfa.json");
23 const LOCK_FILE
: &str = configdir
!("/tfa.json.lock");
25 const CHALLENGE_DATA_PATH
: &str = pbs_buildcfg
::rundir
!("/tfa/challenges");
27 pub fn read_lock() -> Result
<BackupLockGuard
, Error
> {
28 open_backup_lockfile(LOCK_FILE
, None
, false)
31 pub fn write_lock() -> Result
<BackupLockGuard
, Error
> {
32 open_backup_lockfile(LOCK_FILE
, None
, true)
35 /// Read the TFA entries.
36 pub fn read() -> Result
<TfaConfig
, Error
> {
37 let file
= match File
::open(CONF_FILE
) {
39 Err(ref err
) if err
.not_found() => return Ok(TfaConfig
::default()),
40 Err(err
) => return Err(err
.into()),
43 Ok(serde_json
::from_reader(file
)?
)
46 pub(crate) fn webauthn_config_digest(config
: &WebauthnConfig
) -> Result
<[u8; 32], Error
> {
47 let digest_data
= pbs_tools
::json
::to_canonical_json(&serde_json
::to_value(config
)?
)?
;
48 Ok(openssl
::sha
::sha256(&digest_data
))
51 /// Get the webauthn config with a digest.
53 /// This is meant only for configuration updates, which currently only means webauthn updates.
54 /// Since this is meant to be done only once (since changes will lock out users), this should be
55 /// used rarely, since the digest calculation is currently a bit more involved.
56 pub fn webauthn_config() -> Result
<Option
<(WebauthnConfig
, [u8; 32])>, Error
> {
57 Ok(match read()?
.webauthn
{
59 let digest
= webauthn_config_digest(&wa
)?
;
66 /// Requires the write lock to be held.
67 pub fn write(data
: &TfaConfig
) -> Result
<(), Error
> {
68 let options
= CreateOptions
::new().perm(Mode
::from_bits_truncate(0o0600));
70 let json
= serde_json
::to_vec(data
)?
;
71 proxmox_sys
::fs
::replace_file(CONF_FILE
, &json
, options
, true)
74 /// Cleanup non-existent users from the tfa config.
75 pub fn cleanup_users(data
: &mut TfaConfig
, config
: &proxmox_section_config
::SectionConfigData
) {
77 .retain(|user
, _
| config
.lookup
::<User
>("user", user
.as_str()).is_ok());
80 /// Container of `TfaUserChallenges` with the corresponding file lock guard.
82 /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
83 pub struct TfaUserChallengeData
{
84 inner
: proxmox_tfa
::api
::TfaUserChallenges
,
89 fn challenge_data_path_str(userid
: &str) -> PathBuf
{
90 PathBuf
::from(format
!("{}/{}", CHALLENGE_DATA_PATH
, userid
))
93 impl TfaUserChallengeData
{
94 /// Rewind & truncate the file for an update.
95 fn rewind(&mut self) -> Result
<(), Error
> {
96 let pos
= self.lock
.seek(SeekFrom
::Start(0))?
;
99 "unexpected result trying to rewind file, position is {}",
104 proxmox_sys
::c_try
!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) }
);
109 /// Save the current data. Note that we do not replace the file here since we lock the file
110 /// itself, as it is in `/run`, and the typical error case for this particular situation
111 /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
112 /// other reasons then...
114 /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
115 /// way also unlocks early.
116 fn save(mut self) -> Result
<(), Error
> {
119 serde_json
::to_writer(&mut &self.lock
, &self.inner
).map_err(|err
| {
120 format_err
!("failed to update challenge file {:?}: {}", self.path
, err
)
127 /// Get an optional TFA challenge for a user.
128 pub fn login_challenge(userid
: &Userid
) -> Result
<Option
<TfaChallenge
>, Error
> {
129 let _lock
= write_lock()?
;
130 read()?
.authentication_challenge(UserAccess
, userid
.as_str())
133 /// Add a TOTP entry for a user. Returns the ID.
134 pub fn add_totp(userid
: &Userid
, description
: String
, value
: Totp
) -> Result
<String
, Error
> {
135 let _lock
= write_lock();
136 let mut data
= read()?
;
137 let id
= data
.add_totp(userid
.as_str(), description
, value
);
142 /// Add recovery tokens for the user. Returns the token list.
143 pub fn add_recovery(userid
: &Userid
) -> Result
<Vec
<String
>, Error
> {
144 let _lock
= write_lock();
146 let mut data
= read()?
;
147 let out
= data
.add_recovery(userid
.as_str())?
;
152 /// Add a u2f registration challenge for a user.
153 pub fn add_u2f_registration(userid
: &Userid
, description
: String
) -> Result
<String
, Error
> {
154 let _lock
= crate::config
::tfa
::write_lock();
155 let mut data
= read()?
;
156 let challenge
= data
.u2f_registration_challenge(UserAccess
, userid
.as_str(), description
)?
;
161 /// Finish a u2f registration challenge for a user.
162 pub fn finish_u2f_registration(
166 ) -> Result
<String
, Error
> {
167 let _lock
= crate::config
::tfa
::write_lock();
168 let mut data
= read()?
;
169 let id
= data
.u2f_registration_finish(UserAccess
, userid
.as_str(), challenge
, response
)?
;
174 /// Add a webauthn registration challenge for a user.
175 pub fn add_webauthn_registration(userid
: &Userid
, description
: String
) -> Result
<String
, Error
> {
176 let _lock
= crate::config
::tfa
::write_lock();
177 let mut data
= read()?
;
179 data
.webauthn_registration_challenge(UserAccess
, userid
.as_str(), description
)?
;
184 /// Finish a webauthn registration challenge for a user.
185 pub fn finish_webauthn_registration(
189 ) -> Result
<String
, Error
> {
190 let _lock
= crate::config
::tfa
::write_lock();
191 let mut data
= read()?
;
192 let id
= data
.webauthn_registration_finish(UserAccess
, userid
.as_str(), challenge
, response
)?
;
197 /// Verify a TFA challenge.
198 pub fn verify_challenge(
200 challenge
: &TfaChallenge
,
201 response
: TfaResponse
,
202 ) -> Result
<(), Error
> {
203 let _lock
= crate::config
::tfa
::write_lock();
204 let mut data
= read()?
;
206 .verify(UserAccess
, userid
.as_str(), challenge
, response
)?
214 #[derive(Clone, Copy)]
216 pub struct UserAccess
;
219 impl proxmox_tfa
::api
::OpenUserChallengeData
for UserAccess
{
220 type Data
= TfaUserChallengeData
;
222 /// Load the user's current challenges with the intent to create a challenge (create the file
223 /// if it does not exist), and keep a lock on the file.
224 fn open(&self, userid
: &str) -> Result
<Self::Data
, Error
> {
225 crate::server
::create_run_dir()?
;
226 let options
= CreateOptions
::new().perm(Mode
::from_bits_truncate(0o0600));
227 proxmox_sys
::fs
::create_path(CHALLENGE_DATA_PATH
, Some(options
.clone()), Some(options
))
230 "failed to crate challenge data dir {:?}: {}",
236 let path
= challenge_data_path_str(userid
);
238 let mut file
= std
::fs
::OpenOptions
::new()
245 .map_err(|err
| format_err
!("failed to create challenge file {:?}: {}", path
, err
))?
;
247 proxmox_sys
::fs
::lock_file(&mut file
, true, None
)?
;
249 // the file may be empty, so read to a temporary buffer first:
250 let mut data
= Vec
::with_capacity(4096);
252 file
.read_to_end(&mut data
).map_err(|err
| {
253 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
256 let inner
= if data
.is_empty() {
259 match serde_json
::from_slice(&data
) {
263 "failed to parse challenge data for user {}: {}",
272 Ok(TfaUserChallengeData
{
279 /// `open` without creating the file if it doesn't exist, to finish WA authentications.
280 fn open_no_create(&self, userid
: &str) -> Result
<Option
<Self::Data
>, Error
> {
281 let path
= challenge_data_path_str(userid
);
282 let mut file
= match std
::fs
::OpenOptions
::new()
290 Err(err
) if err
.kind() == io
::ErrorKind
::NotFound
=> return Ok(None
),
291 Err(err
) => return Err(err
.into()),
294 proxmox_sys
::fs
::lock_file(&mut file
, true, None
)?
;
296 let inner
= serde_json
::from_reader(&mut file
).map_err(|err
| {
297 format_err
!("failed to read challenge data for user {}: {}", userid
, err
)
300 Ok(Some(TfaUserChallengeData
{
307 /// `remove` user data if it exists.
308 fn remove(&self, userid
: &str) -> Result
<bool
, Error
> {
309 let path
= challenge_data_path_str(userid
);
310 match std
::fs
::remove_file(&path
) {
312 Err(err
) if err
.not_found() => Ok(false),
313 Err(err
) => Err(err
.into()),
318 impl proxmox_tfa
::api
::UserChallengeAccess
for TfaUserChallengeData
{
319 fn get_mut(&mut self) -> &mut proxmox_tfa
::api
::TfaUserChallenges
{
323 fn save(self) -> Result
<(), Error
> {
324 TfaUserChallengeData
::save(self)