Skip to main content

openstack_keystone_core/token/backend/fernet/
utils.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14//! # Fernet utils
15use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
16use chrono::{DateTime, Utc};
17use fernet::Fernet;
18use nix::sys::stat::{Mode, umask};
19use nix::unistd::{Gid, Uid, getegid, geteuid, setegid, seteuid};
20use rmp::{
21    Marker,
22    decode::{self, *},
23    encode::{self, *},
24};
25use secrecy::{ExposeSecret, SecretString};
26use std::collections::BTreeMap;
27use std::fs;
28//use std::io;
29use std::io::{self, Read, Write};
30use std::path::PathBuf;
31use tempfile::NamedTempFile;
32use tokio::fs as fs_async;
33use tracing::{error, info, trace, warn};
34use uuid::Uuid;
35
36use crate::token::error::TokenProviderError;
37
38/// Fernet utils.
39#[derive(Clone, Debug, Default)]
40pub struct FernetUtils {
41    pub key_repository: PathBuf,
42    pub max_active_keys: usize,
43}
44
45impl FernetUtils {
46    /// Validate fernet key key_repository.
47    ///
48    /// Perform validation of the fernet keys repository.
49    fn validate_key_repository(&self) -> Result<bool, TokenProviderError> {
50        Ok(self.key_repository.exists())
51    }
52
53    /// Securely create a new tmp encryption key.
54    ///
55    /// This created key is not effective until `become_valid_new_key` method is
56    /// executed.
57    fn create_tmp_new_key(
58        &self,
59        user_id: Option<u32>,
60        group_id: Option<u32>,
61    ) -> Result<(), TokenProviderError> {
62        // 1. Generate key and wrap in a secret-protecting type
63        let key = SecretString::new(Fernet::generate_key().into());
64        let target_path = self.key_repository.join("0.tmp");
65
66        // 2. Set umask and handle privilege escalation using scope_guard
67        let old_umask = umask(Mode::from_bits_truncate(0o177));
68        let _umask_guard = scopeguard::guard(old_umask, |old| {
69            umask(old);
70        });
71
72        if let (Some(uid), Some(gid)) = (user_id, group_id) {
73            let (old_euid, old_egid) = (geteuid(), getegid());
74
75            setegid(Gid::from_raw(gid)).map_err(|e| TokenProviderError::NixErrno {
76                context: "setting effective process GID".into(),
77                source: e,
78            })?;
79            seteuid(Uid::from_raw(uid)).map_err(|e| TokenProviderError::NixErrno {
80                context: "setting effective process UID".into(),
81                source: e,
82            })?;
83
84            // This guard ensures IDs are restored even if the function returns early
85            let _id_guard = scopeguard::guard((old_euid, old_egid), |(u, g)| {
86                let _ = seteuid(u);
87                let _ = setegid(g);
88            });
89        }
90
91        // 3. Atomic Write: Create a temp file in the same directory
92        // This handles the "cleanup on failure" automatically.
93        let mut tmp_file = NamedTempFile::new_in(self.key_repository.clone())?;
94
95        // Write the actual secret data
96        tmp_file.write_all(key.expose_secret().as_bytes())?;
97        tmp_file.flush()?;
98
99        // 4. Atomically persist the file to "0"
100        // If persist() isn't called, the file is deleted when tmp_file goes out of
101        // scope.
102        info!("Created new Fernet key at {:?}", target_path);
103        tmp_file.persist(&target_path)?;
104
105        Ok(())
106    }
107
108    /// Make the tmp new key a valid new key.
109    /// Renames '0.tmp' to '0' atomically.
110    fn become_valid_new_key(&self) -> Result<(), TokenProviderError> {
111        let tmp_key_file = self.key_repository.join("0.tmp");
112        let valid_key_file = self.key_repository.join("0");
113
114        // Check if the source exists before attempting rename to provide better errors
115        if !tmp_key_file.exists() {
116            error!("Temporary key file not found: {:?}", tmp_key_file);
117            return Err(TokenProviderError::FernetKeysMissing);
118        }
119
120        // std::fs::rename is atomic on most Unix-like systems.
121        // If '0' already exists, it will be overwritten in a single operation.
122        fs::rename(&tmp_key_file, &valid_key_file)?;
123
124        // Sync the directory to ensure the rename is persisted to disk
125        // This is a "pro" Rust move for high-reliability systems.
126        let dir = fs::File::open(&self.key_repository)?;
127        dir.sync_all()?;
128
129        info!("Become a valid new key: {:?}", valid_key_file);
130        Ok(())
131    }
132
133    pub fn initialize_key_repository(&self) -> Result<(), TokenProviderError> {
134        self.create_tmp_new_key(None, None)?;
135        self.become_valid_new_key()?;
136        Ok(())
137    }
138
139    pub fn load_keys(&self) -> Result<impl IntoIterator<Item = Fernet>, TokenProviderError> {
140        info!("loading keys from {:?}", self.key_repository);
141        let mut keys: BTreeMap<i8, Fernet> = BTreeMap::new();
142        if self.validate_key_repository()? {
143            for entry in fs::read_dir(&self.key_repository)? {
144                let entry = entry?;
145                if let Ok(fname) = entry.file_name().into_string()
146                    && let Ok(key_order) = fname.parse::<i8>()
147                {
148                    // We are only interested in files named as integer (0, 1, 2, ...)
149                    trace!("Loading key {:?}", entry.file_name());
150                    if let Some(fernet) = Fernet::new(
151                        fs::read_to_string(entry.path())
152                            .map_err(|e| TokenProviderError::FernetKeyRead {
153                                source: e,
154                                path: entry.path(),
155                            })?
156                            .trim_end(),
157                    ) {
158                        keys.insert(key_order, fernet);
159                    } else {
160                        warn!(
161                            "The key {:?} is not usable for Fernet library",
162                            entry.file_name()
163                        )
164                    }
165                }
166            }
167        }
168        if keys.is_empty() {
169            return Err(TokenProviderError::FernetKeysMissing);
170        }
171        Ok(keys.into_values().rev())
172    }
173
174    pub async fn load_keys_async(
175        &self,
176    ) -> Result<impl IntoIterator<Item = Fernet>, TokenProviderError> {
177        let mut keys: BTreeMap<i8, Fernet> = BTreeMap::new();
178        if self.validate_key_repository()? {
179            let mut entries = fs_async::read_dir(&self.key_repository).await?;
180            while let Some(entry) = entries.next_entry().await? {
181                if let Ok(fname) = entry.file_name().into_string()
182                    && let Ok(key_order) = fname.parse::<i8>()
183                {
184                    // We are only interested in files named as integer (0, 1, 2, ...)
185                    trace!("Loading key {:?}", entry.file_name());
186                    if let Some(fernet) = Fernet::new(
187                        fs::read_to_string(entry.path())
188                            .map_err(|e| TokenProviderError::FernetKeyRead {
189                                source: e,
190                                path: entry.path(),
191                            })?
192                            .trim_end(),
193                    ) {
194                        keys.insert(key_order, fernet);
195                    } else {
196                        warn!(
197                            "The key {:?} is not usable for Fernet library",
198                            entry.file_name()
199                        )
200                    }
201                }
202            }
203        }
204        if keys.is_empty() {
205            return Err(TokenProviderError::FernetKeysMissing);
206        }
207        Ok(keys.into_values().rev())
208    }
209}
210
211/// Read binary data from the payload.
212pub fn read_bin_data<R: Read>(len: u32, rd: &mut R) -> Result<Vec<u8>, io::Error> {
213    let mut buf = Vec::with_capacity(len.min(1 << 16) as usize);
214    let bytes_read = rd.take(u64::from(len)).read_to_end(&mut buf)?;
215    if bytes_read != len as usize {
216        return Err(io::ErrorKind::UnexpectedEof.into());
217    }
218    Ok(buf)
219}
220
221/// Read string data.
222pub fn read_str_data<R: Read>(len: u32, rd: &mut R) -> Result<String, io::Error> {
223    Ok(String::from_utf8_lossy(&read_bin_data(len, rd)?).into_owned())
224}
225
226/// Write string.
227pub fn write_str<W: RmpWrite>(wd: &mut W, data: &str) -> Result<(), TokenProviderError> {
228    encode::write_str(wd, data).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
229    Ok(())
230}
231
232/// Read bytes as string.
233pub fn read_str<R: Read>(rd: &mut R) -> Result<String, TokenProviderError> {
234    match read_marker(rd).map_err(ValueReadError::from)? {
235        Marker::Bin8 => {
236            Ok(String::from_utf8_lossy(&read_bin_data(read_pfix(rd)?.into(), rd)?).to_string())
237        }
238        Marker::FixStr(len) => Ok(read_str_data(len.into(), rd)?),
239        other => Err(TokenProviderError::InvalidTokenUuidMarker(other)),
240    }
241}
242
243/// Read the UUID from the payload.
244/// It is represented as an Array[bool, bytes] where first bool indicates
245/// whether following bytes are UUID or just bytes that should be treated as a
246/// string (for cases where ID is not a valid UUID).
247pub fn read_uuid(rd: &mut &[u8]) -> Result<String, TokenProviderError> {
248    match read_marker(rd).map_err(ValueReadError::from)? {
249        Marker::FixArray(_) => {
250            match read_marker(rd).map_err(ValueReadError::from)? {
251                Marker::True => {
252                    // This is uuid as bytes
253                    // Technically we may fail reading it into bytes, but python part is
254                    // responsible that it doesn not happen
255                    if let Marker::Bin8 = read_marker(rd).map_err(ValueReadError::from)? {
256                        return Ok(Uuid::try_from(read_bin_data(read_pfix(rd)?.into(), rd)?)?
257                            .as_simple()
258                            .to_string());
259                    }
260                }
261                Marker::False => {
262                    // This is not uuid
263                    match read_marker(rd).map_err(ValueReadError::from)? {
264                        Marker::Bin8 => {
265                            return Ok(String::from_utf8_lossy(&read_bin_data(
266                                read_pfix(rd)?.into(),
267                                rd,
268                            )?)
269                            .to_string());
270                        }
271                        Marker::FixStr(len) => {
272                            return Ok(read_str_data(len.into(), rd)?);
273                        }
274                        other => {
275                            return Err(TokenProviderError::InvalidTokenUuidMarker(other));
276                        }
277                    }
278                }
279                other => {
280                    return Err(TokenProviderError::InvalidTokenUuidMarker(other));
281                }
282            }
283        }
284        Marker::FixStr(len) => {
285            return Ok(read_str_data(len.into(), rd)?);
286        }
287        other => {
288            return Err(TokenProviderError::InvalidTokenUuidMarker(other));
289        }
290    }
291    Err(TokenProviderError::InvalidTokenUuid)
292}
293
294/// Write the UUID to the payload.
295/// It is represented as an Array[bool, bytes] where first bool indicates
296/// whether following bytes are UUID or just bytes that should be treated as a
297/// string (for cases where ID is not a valid UUID).
298pub fn write_uuid<W: RmpWrite>(wd: &mut W, uid: &str) -> Result<(), TokenProviderError> {
299    match Uuid::parse_str(uid) {
300        Ok(uuid) => {
301            write_array_len(wd, 2).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
302            write_bool(wd, true).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
303            write_bin(wd, uuid.as_bytes())
304                .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
305        }
306        _ => {
307            write_array_len(wd, 2).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
308            write_bool(wd, false).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
309            write_bin(wd, uid.as_bytes())
310                .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
311        }
312    }
313    Ok(())
314}
315
316/// Read the time represented as a f64 of the UTC seconds.
317pub fn read_time(rd: &mut &[u8]) -> Result<DateTime<Utc>, TokenProviderError> {
318    DateTime::from_timestamp(read_f64(rd)?.round() as i64, 0)
319        .ok_or(TokenProviderError::InvalidToken)
320}
321
322/// Write the time represented as a f64 of the UTC seconds.
323pub fn write_time<W: RmpWrite>(wd: &mut W, time: DateTime<Utc>) -> Result<(), TokenProviderError> {
324    write_f64(wd, time.timestamp() as f64)
325        .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
326    Ok(())
327}
328
329/// Decode array of audit ids from the payload.
330pub fn read_audit_ids(
331    rd: &mut &[u8],
332) -> Result<impl IntoIterator<Item = String> + use<>, TokenProviderError> {
333    if let Marker::FixArray(len) = read_marker(rd).map_err(ValueReadError::from)? {
334        let mut result: Vec<String> = Vec::new();
335        for _ in 0..len {
336            if let Marker::Bin8 = read_marker(rd).map_err(ValueReadError::from)? {
337                let dt = read_bin_data(read_pfix(rd)?.into(), rd)?;
338                let audit_id: String = URL_SAFE_NO_PAD.encode(dt);
339                result.push(audit_id);
340            } else {
341                return Err(TokenProviderError::InvalidToken);
342            }
343        }
344        return Ok(result.into_iter());
345    }
346    Err(TokenProviderError::InvalidToken)
347}
348
349/// Encode array of audit ids into the payload.
350pub fn write_audit_ids<W: RmpWrite, I: IntoIterator<Item = String>>(
351    wd: &mut W,
352    data: I,
353) -> Result<(), TokenProviderError> {
354    let vals = Vec::from_iter(data);
355    write_array_len(wd, vals.len() as u32)
356        .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
357    for val in vals.iter() {
358        write_bin(
359            wd,
360            &URL_SAFE_NO_PAD
361                .decode(val)
362                .map_err(|_| TokenProviderError::AuditIdWrongFormat)?,
363        )
364        .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
365    }
366    Ok(())
367}
368
369/// Decode array of strings ids from the payload.
370pub fn read_list_of_uuids(
371    rd: &mut &[u8],
372) -> Result<impl IntoIterator<Item = String> + use<>, TokenProviderError> {
373    if let Marker::FixArray(len) = read_marker(rd).map_err(ValueReadError::from)? {
374        let mut result: Vec<String> = Vec::new();
375        for _ in 0..len {
376            result.push(read_uuid(rd)?);
377        }
378        return Ok(result.into_iter());
379    }
380    Err(TokenProviderError::InvalidToken)
381}
382
383/// Encode array of bytes into the payload.
384pub fn write_list_of_uuids<W: RmpWrite, I: IntoIterator<Item = V>, V: AsRef<str>>(
385    wd: &mut W,
386    data: I,
387) -> Result<(), TokenProviderError> {
388    let vals = Vec::from_iter(data);
389    write_array_len(wd, vals.len() as u32)
390        .map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
391    for val in vals.iter() {
392        write_uuid(wd, val.as_ref())?;
393    }
394    Ok(())
395}
396
397/// Read boolean.
398pub fn read_bool<R: Read>(rd: &mut R) -> Result<bool, TokenProviderError> {
399    Ok(decode::read_bool(rd)?)
400}
401
402/// Write boolean.
403pub fn write_bool<W: RmpWrite>(wd: &mut W, data: bool) -> Result<(), TokenProviderError> {
404    encode::write_bool(wd, data).map_err(|x| TokenProviderError::RmpEncode(x.to_string()))?;
405    Ok(())
406}
407
408#[cfg(test)]
409mod tests {
410    use super::FernetUtils;
411    use chrono::{Local, SubsecRound};
412    use std::fs::File;
413    use std::io::Write;
414    use tempfile::tempdir;
415
416    use super::*;
417
418    #[tokio::test]
419    async fn test_load_keys_valid() {
420        let tmp_dir = tempdir().unwrap();
421        for i in 0..5 {
422            let file_path = tmp_dir.path().join(format!("{i}"));
423            let mut tmp_file = File::create(file_path).unwrap();
424            write!(tmp_file, "{}", Fernet::generate_key()).unwrap();
425        }
426        let utils = FernetUtils {
427            key_repository: tmp_dir.keep(),
428            ..Default::default()
429        };
430        let keys: Vec<Fernet> = utils.load_keys().unwrap().into_iter().collect();
431        assert_eq!(5, keys.len());
432    }
433
434    #[tokio::test]
435    async fn test_load_keys_all_invalid() {
436        let tmp_dir = tempdir().unwrap();
437        for i in 0..5 {
438            let file_path = tmp_dir.path().join(format!("{i}"));
439            let mut tmp_file = File::create(file_path).unwrap();
440            write!(tmp_file, "{i}").unwrap();
441        }
442        // write dummy file to check it is ignored
443        let file_path = tmp_dir.path().join("dummy");
444        let mut tmp_file = File::create(file_path).unwrap();
445        write!(tmp_file, "foo").unwrap();
446
447        let utils = FernetUtils {
448            key_repository: tmp_dir.keep(),
449            ..Default::default()
450        };
451        let res = utils.load_keys();
452
453        if let Err(TokenProviderError::FernetKeysMissing) = res {
454        } else {
455            panic!("Should have raised an exception");
456        }
457    }
458
459    #[tokio::test]
460    async fn test_load_keys_trim() {
461        let tmp_dir = tempdir().unwrap();
462        for i in 0..5 {
463            let file_path = tmp_dir.path().join(format!("{i}"));
464            let mut tmp_file = File::create(file_path).unwrap();
465            writeln!(tmp_file, "{}", Fernet::generate_key()).unwrap();
466        }
467        let utils = FernetUtils {
468            key_repository: tmp_dir.keep(),
469            ..Default::default()
470        };
471        let keys: Vec<Fernet> = utils.load_keys().unwrap().into_iter().collect();
472        assert_eq!(5, keys.len());
473    }
474
475    #[test]
476    fn test_write_read_uuid_str() {
477        let mut buf = Vec::with_capacity(36);
478        let uuid = "abc";
479        write_uuid(&mut buf, uuid).unwrap();
480        let msg = buf.clone();
481        let mut decode_data = msg.as_slice();
482        let decoded = read_uuid(&mut decode_data).unwrap();
483        assert_eq!(uuid, decoded);
484    }
485
486    #[test]
487    fn test_write_read_uuid() {
488        let mut buf = Vec::with_capacity(36);
489        let test = Uuid::new_v4();
490        write_uuid(&mut buf, &test.to_string()).unwrap();
491        let msg = buf.clone();
492        let mut decode_data = msg.as_slice();
493        let decoded = read_uuid(&mut decode_data).unwrap();
494        assert_eq!(test.simple().to_string(), decoded);
495    }
496
497    #[test]
498    fn test_write_read_time() {
499        let test = Local::now().trunc_subsecs(0);
500        let mut buf = Vec::with_capacity(36);
501        write_time(&mut buf, test.into()).unwrap();
502        let msg = buf.clone();
503        let mut decode_data = msg.as_slice();
504        let decoded = read_time(&mut decode_data).unwrap();
505        assert_eq!(test, decoded);
506    }
507
508    #[test]
509    fn test_write_audit_ids() {
510        let test = vec!["Zm9vCg".into()];
511        let mut buf = Vec::with_capacity(36);
512        write_audit_ids(&mut buf, test.clone()).unwrap();
513        let msg = buf.clone();
514        let mut decode_data = msg.as_slice();
515        let decoded: Vec<String> = read_audit_ids(&mut decode_data)
516            .unwrap()
517            .into_iter()
518            .collect();
519        assert_eq!(test, decoded);
520    }
521
522    #[test]
523    fn test_write_bool() {
524        let test = true;
525        let mut buf = Vec::with_capacity(1);
526        write_bool(&mut buf, test).unwrap();
527        let msg = buf.clone();
528        let mut decode_data = msg.as_slice();
529        let decoded = read_bool(&mut decode_data).unwrap();
530        assert_eq!(test, decoded);
531    }
532}