]> git.proxmox.com Git - proxmox-backup.git/blob - src/config/tfa.rs
update to proxmox-sys 0.2 crate
[proxmox-backup.git] / src / config / tfa.rs
1 use std::fs::File;
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;
6
7 use anyhow::{bail, format_err, Error};
8 use nix::sys::stat::Mode;
9
10 use proxmox_sys::error::SysError;
11 use proxmox_sys::fs::CreateOptions;
12 use proxmox_tfa::totp::Totp;
13
14 pub use proxmox_tfa::api::{
15 TfaChallenge, TfaConfig, TfaResponse, WebauthnConfig, WebauthnConfigUpdater,
16 };
17
18 use pbs_api_types::{User, Userid};
19 use pbs_buildcfg::configdir;
20 use pbs_config::{open_backup_lockfile, BackupLockGuard};
21
22 const CONF_FILE: &str = configdir!("/tfa.json");
23 const LOCK_FILE: &str = configdir!("/tfa.json.lock");
24
25 const CHALLENGE_DATA_PATH: &str = pbs_buildcfg::rundir!("/tfa/challenges");
26
27 pub fn read_lock() -> Result<BackupLockGuard, Error> {
28 open_backup_lockfile(LOCK_FILE, None, false)
29 }
30
31 pub fn write_lock() -> Result<BackupLockGuard, Error> {
32 open_backup_lockfile(LOCK_FILE, None, true)
33 }
34
35 /// Read the TFA entries.
36 pub fn read() -> Result<TfaConfig, Error> {
37 let file = match File::open(CONF_FILE) {
38 Ok(file) => file,
39 Err(ref err) if err.not_found() => return Ok(TfaConfig::default()),
40 Err(err) => return Err(err.into()),
41 };
42
43 Ok(serde_json::from_reader(file)?)
44 }
45
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))
49 }
50
51 /// Get the webauthn config with a digest.
52 ///
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 {
58 Some(wa) => {
59 let digest = webauthn_config_digest(&wa)?;
60 Some((wa, digest))
61 }
62 None => None,
63 })
64 }
65
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));
69
70 let json = serde_json::to_vec(data)?;
71 proxmox_sys::fs::replace_file(CONF_FILE, &json, options, true)
72 }
73
74 /// Cleanup non-existent users from the tfa config.
75 pub fn cleanup_users(data: &mut TfaConfig, config: &proxmox_section_config::SectionConfigData) {
76 data.users
77 .retain(|user, _| config.lookup::<User>("user", user.as_str()).is_ok());
78 }
79
80 /// Container of `TfaUserChallenges` with the corresponding file lock guard.
81 ///
82 /// TODO: Implement a general file lock guarded struct container in the `proxmox` crate.
83 pub struct TfaUserChallengeData {
84 inner: proxmox_tfa::api::TfaUserChallenges,
85 path: PathBuf,
86 lock: File,
87 }
88
89 fn challenge_data_path_str(userid: &str) -> PathBuf {
90 PathBuf::from(format!("{}/{}", CHALLENGE_DATA_PATH, userid))
91 }
92
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))?;
97 if pos != 0 {
98 bail!(
99 "unexpected result trying to rewind file, position is {}",
100 pos
101 );
102 }
103
104 proxmox_sys::c_try!(unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) });
105
106 Ok(())
107 }
108
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...
113 ///
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> {
117 self.rewind()?;
118
119 serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {
120 format_err!("failed to update challenge file {:?}: {}", self.path, err)
121 })?;
122
123 Ok(())
124 }
125 }
126
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())
131 }
132
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);
138 write(&data)?;
139 Ok(id)
140 }
141
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();
145
146 let mut data = read()?;
147 let out = data.add_recovery(userid.as_str())?;
148 write(&data)?;
149 Ok(out)
150 }
151
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)?;
157 write(&data)?;
158 Ok(challenge)
159 }
160
161 /// Finish a u2f registration challenge for a user.
162 pub fn finish_u2f_registration(
163 userid: &Userid,
164 challenge: &str,
165 response: &str,
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)?;
170 write(&data)?;
171 Ok(id)
172 }
173
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()?;
178 let challenge =
179 data.webauthn_registration_challenge(UserAccess, userid.as_str(), description)?;
180 write(&data)?;
181 Ok(challenge)
182 }
183
184 /// Finish a webauthn registration challenge for a user.
185 pub fn finish_webauthn_registration(
186 userid: &Userid,
187 challenge: &str,
188 response: &str,
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)?;
193 write(&data)?;
194 Ok(id)
195 }
196
197 /// Verify a TFA challenge.
198 pub fn verify_challenge(
199 userid: &Userid,
200 challenge: &TfaChallenge,
201 response: TfaResponse,
202 ) -> Result<(), Error> {
203 let _lock = crate::config::tfa::write_lock();
204 let mut data = read()?;
205 if data
206 .verify(UserAccess, userid.as_str(), challenge, response)?
207 .needs_saving()
208 {
209 write(&data)?;
210 }
211 Ok(())
212 }
213
214 #[derive(Clone, Copy)]
215 #[repr(transparent)]
216 pub struct UserAccess;
217
218 /// Build th
219 impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
220 type Data = TfaUserChallengeData;
221
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))
228 .map_err(|err| {
229 format_err!(
230 "failed to crate challenge data dir {:?}: {}",
231 CHALLENGE_DATA_PATH,
232 err
233 )
234 })?;
235
236 let path = challenge_data_path_str(userid);
237
238 let mut file = std::fs::OpenOptions::new()
239 .create(true)
240 .read(true)
241 .write(true)
242 .truncate(false)
243 .mode(0o600)
244 .open(&path)
245 .map_err(|err| format_err!("failed to create challenge file {:?}: {}", path, err))?;
246
247 proxmox_sys::fs::lock_file(&mut file, true, None)?;
248
249 // the file may be empty, so read to a temporary buffer first:
250 let mut data = Vec::with_capacity(4096);
251
252 file.read_to_end(&mut data).map_err(|err| {
253 format_err!("failed to read challenge data for user {}: {}", userid, err)
254 })?;
255
256 let inner = if data.is_empty() {
257 Default::default()
258 } else {
259 match serde_json::from_slice(&data) {
260 Ok(inner) => inner,
261 Err(err) => {
262 eprintln!(
263 "failed to parse challenge data for user {}: {}",
264 userid,
265 err
266 );
267 Default::default()
268 },
269 }
270 };
271
272 Ok(TfaUserChallengeData {
273 inner,
274 path,
275 lock: file,
276 })
277 }
278
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()
283 .read(true)
284 .write(true)
285 .truncate(false)
286 .mode(0o600)
287 .open(&path)
288 {
289 Ok(file) => file,
290 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
291 Err(err) => return Err(err.into()),
292 };
293
294 proxmox_sys::fs::lock_file(&mut file, true, None)?;
295
296 let inner = serde_json::from_reader(&mut file).map_err(|err| {
297 format_err!("failed to read challenge data for user {}: {}", userid, err)
298 })?;
299
300 Ok(Some(TfaUserChallengeData {
301 inner,
302 path,
303 lock: file,
304 }))
305 }
306
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) {
311 Ok(()) => Ok(true),
312 Err(err) if err.not_found() => Ok(false),
313 Err(err) => Err(err.into()),
314 }
315 }
316 }
317
318 impl proxmox_tfa::api::UserChallengeAccess for TfaUserChallengeData {
319 fn get_mut(&mut self) -> &mut proxmox_tfa::api::TfaUserChallenges {
320 &mut self.inner
321 }
322
323 fn save(self) -> Result<(), Error> {
324 TfaUserChallengeData::save(self)
325 }
326 }