cryptr/
keys.rs

1use crate::kdf::KdfValue;
2use crate::utils::{b64_decode, b64_encode, secure_random_alnum, secure_random_vec};
3use crate::value::EncValue;
4use crate::CryptrError;
5use regex::Regex;
6use std::collections::HashMap;
7use std::env;
8use std::fmt::{Display, Formatter, Write};
9use std::sync::OnceLock;
10use tokio::fs;
11use tokio::fs::File;
12use tracing::error;
13
14static RE_KEY_ID: OnceLock<Regex> = OnceLock::new();
15
16#[allow(dead_code)]
17pub(crate) static ENC_KEYS: OnceLock<EncKeys> = OnceLock::new();
18
19/// Password protected, sealed encryption keys
20#[derive(Debug)]
21pub struct EncKeysSealed(String);
22
23impl Display for EncKeysSealed {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}", self.0)
26    }
27}
28
29impl EncKeysSealed {
30    pub fn from_b64(value: String) -> Self {
31        Self(value)
32    }
33
34    pub fn from_bytes(value: &[u8]) -> Self {
35        Self(b64_encode(value))
36    }
37
38    /// Expects the `ENC_KEYS_SEALED` environment variable with a base64 encoded string
39    pub fn try_from_env() -> Result<Self, CryptrError> {
40        dotenvy::dotenv().ok();
41        let s = env::var("ENC_KEYS_SEALED")?;
42        Ok(Self(s))
43    }
44
45    /// Seal the given encryption keys
46    pub fn seal(enc_keys: EncKeys, password: &str) -> Result<Self, CryptrError> {
47        let keys_bytes: Vec<u8> = enc_keys.into_bytes();
48        let enc = EncValue::encrypt_with_password(keys_bytes.as_slice(), password)?;
49        let s = b64_encode(enc.into_bytes().as_ref());
50        Ok(Self(s))
51    }
52
53    /// Unseal the given encryption keys
54    pub fn unseal(self, password: &str) -> Result<EncKeys, CryptrError> {
55        let bytes = b64_decode(&self.0)?;
56        let enc = EncValue::try_from_bytes(bytes)?;
57        let dec = enc.decrypt_with_password(password)?;
58        let keys = EncKeys::try_from(dec.as_ref())?;
59        Ok(keys)
60    }
61
62    pub async fn read_from_file(path: &str) -> Result<Self, CryptrError> {
63        let s = fs::read_to_string(path).await?;
64        Ok(Self(s))
65    }
66
67    pub async fn save_to_file(&self, path_full: &str) -> Result<(), CryptrError> {
68        if let Ok(file) = File::open(&path_full).await {
69            let meta = file.metadata().await?;
70            if meta.is_dir() {
71                return Err(CryptrError::File("target file is a directory"));
72            }
73        }
74
75        fs::write(&path_full, self.0.as_bytes()).await?;
76        Ok(())
77    }
78}
79
80/// Encryption keys used for all operations
81///
82/// These can be either used statically initialized for ease of use, or given dynamically each time.
83/// You just need to use the appropriate functions for the `EncValue`.
84#[derive(Debug, Default, PartialEq, bincode::Encode, bincode::Decode)]
85pub struct EncKeys {
86    pub enc_key_active: String,
87    pub enc_keys: Vec<(String, Vec<u8>)>,
88}
89
90impl TryFrom<&[u8]> for EncKeys {
91    type Error = CryptrError;
92
93    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
94        let (slf, _) =
95            bincode::decode_from_slice(value, bincode::config::legacy()).map_err(|err| {
96                error!("Deserialization error: {:?}", err);
97                CryptrError::Deserialization("Cannot deserialize EncKeys from given bytes")
98            })?;
99        Ok(slf)
100    }
101}
102
103impl From<KdfValue> for EncKeys {
104    fn from(kdf: KdfValue) -> Self {
105        Self {
106            enc_key_active: kdf.enc_key_value(),
107            enc_keys: vec![(kdf.enc_key_value(), kdf.value())],
108        }
109    }
110}
111
112#[allow(dead_code)]
113impl EncKeys {
114    #[inline]
115    pub fn re_key_id() -> &'static Regex {
116        RE_KEY_ID.get_or_init(|| Regex::new(r"^[a-zA-Z0-9:_-]{2,20}$").unwrap())
117    }
118
119    pub fn try_parse(
120        enc_key_active: String,
121        enc_keys_unparsed: Vec<String>,
122    ) -> Result<Self, CryptrError> {
123        let mut enc_keys = Vec::with_capacity(enc_keys_unparsed.len());
124        for key in enc_keys_unparsed {
125            let Some((id, key_bytes)) = Self::parse_raw_key(&key)? else {
126                continue;
127            };
128            enc_keys.push((id, key_bytes));
129        }
130
131        Ok(Self {
132            enc_key_active,
133            enc_keys,
134        })
135    }
136
137    /// Generates and appends a new random encryption key
138    pub fn append_new_random(&mut self) -> Result<(), CryptrError> {
139        let id = secure_random_alnum(12);
140        self.append_new_random_with_id(id)
141    }
142
143    /// Generates and appends a new random encryption key with a specific ID
144    pub fn append_new_random_with_id(&mut self, id: String) -> Result<(), CryptrError> {
145        Self::validate_id(&id, Some(self))?;
146
147        let key = secure_random_vec(32)?;
148        self.enc_key_active.clone_from(&id);
149        self.enc_keys.push((id, key));
150
151        Ok(())
152    }
153
154    /// Returns the default config path.
155    ///
156    /// Available with feature `cli` only
157    #[cfg(feature = "cli")]
158    pub async fn config_path() -> Result<String, CryptrError> {
159        let home_path = home::home_dir().ok_or(CryptrError::File("Cannot get $HOME"))?;
160        let home_str = home_path
161            .to_str()
162            .ok_or(CryptrError::File("Cannot convert $HOME path to str"))?;
163
164        fs::create_dir_all(format!("{home_str}/.cryptr")).await?;
165
166        #[cfg(target_family = "unix")]
167        let path = format!("{home_str}/.cryptr/config");
168        #[cfg(not(target_family = "unix"))]
169        let path = format!("{home_str}\\.cryptr\\config");
170        Ok(path)
171    }
172
173    /// Mutate the keys and deletes the key with the given ID, if it exists
174    pub fn delete(&mut self, enc_key_id: &str) -> Result<(), CryptrError> {
175        if self.enc_key_active == enc_key_id {
176            return Err(CryptrError::Keys("Cannot delete the currently active key"));
177        }
178
179        self.enc_keys = self
180            .enc_keys
181            .clone()
182            .into_iter()
183            .filter(|(id, _key)| id != enc_key_id)
184            .collect();
185
186        Ok(())
187    }
188
189    /// Formats a converted ENC_KEYS string in the correct format for config / K8s secret
190    ///
191    /// This is useful for generating keys somewhere else to paste them into K8s / Docker definitions later on.
192    ///
193    /// # Returns 2 values:
194    /// 1. `ENC_KEYS=` value for a config or environment variable
195    /// 2. `ENC_KEYS: ` with an additional base64 encoding which can be used inside a K8s secret directly
196    pub fn fmt_enc_keys_str_for_config(enc_keys: &str) -> (String, String) {
197        let value_v64 = b64_encode(enc_keys.as_bytes());
198
199        let cfg_value = format!("ENC_KEYS=\"\n{enc_keys}\"");
200        let secrets_value = format!("ENC_KEYS: {value_v64}");
201
202        (cfg_value, secrets_value)
203    }
204
205    /// Reads the keys from the default config
206    ///
207    /// Available with feature `cli` only
208    #[cfg(feature = "cli")]
209    pub async fn read_from_config() -> Result<Self, CryptrError> {
210        let path = Self::config_path().await?;
211        if dotenvy::from_filename(path).is_err() {
212            Err(CryptrError::Config("Config has not been set up yet"))
213        } else {
214            Self::from_env()
215        }
216    }
217
218    /// Reads the keys from a given file location on disk
219    pub fn read_from_file(path: &str) -> Result<Self, CryptrError> {
220        dotenvy::from_filename(path)?;
221        Self::from_env()
222    }
223
224    /// Builds the keys from environment variables
225    ///
226    /// Expects 2 values:
227    /// 1. `ENC_KEY_ACTIVE` which indicates the active, default key
228    /// 2. `ENC_KEYS` with the available keys in the korrect format, for instance:
229    /// ```text
230    /// ENC_KEYS="
231    /// z8ycdOXnOv7E/nxOhIuLo1oiQBpcg6lYz2Jkc3TgAYoD7h4+orRdlYAk=
232    /// test1337/HQyncjvJUNLTv2YvoTWeVmMKQLBe7+xVSHMXUVES8qE=
233    /// "
234    /// ```
235    pub fn from_env() -> Result<Self, CryptrError> {
236        dotenvy::dotenv().ok();
237        let enc_key_active = env::var("ENC_KEY_ACTIVE")?;
238        let raw_enc_keys = env::var("ENC_KEYS")?;
239        let mut enc_keys: Vec<(String, Vec<u8>)> = Vec::with_capacity(2);
240
241        for key in raw_enc_keys.split('\n') {
242            if !key.is_empty() {
243                let Some((id, key_bytes)) = Self::parse_raw_key(key)? else {
244                    continue;
245                };
246                enc_keys.push((id, key_bytes));
247            }
248        }
249
250        Ok(Self {
251            enc_key_active,
252            enc_keys,
253        })
254    }
255
256    fn parse_raw_key(input: &str) -> Result<Option<(String, Vec<u8>)>, CryptrError> {
257        let t: (&str, &str) = match input.split_once('/') {
258            None => {
259                return Ok(None);
260            }
261            Some(k) => k,
262        };
263        let id = t.0.trim();
264        let key_raw = t.1.trim();
265
266        if id.is_empty() || key_raw.is_empty() {
267            return Err(CryptrError::Keys(
268                "ENC_KEYS must not be empty. Format: \"<id>/<key> <id>/<key>\"",
269            ));
270        }
271
272        let key_bytes = b64_decode(key_raw)?;
273        if key_bytes.len() != 32 {
274            return Err(CryptrError::Keys(
275                "An encryption key must be exactly 32 bytes long",
276            ));
277        }
278
279        if !Self::re_key_id().is_match(id) {
280            return Err(CryptrError::Keys(
281                "The IDs for ENC_KEYS must match '^[a-zA-Z0-9:_-]{2,20}$'",
282            ));
283        }
284
285        Ok(Some((id.to_string(), key_bytes)))
286    }
287
288    fn into_bytes(self) -> Vec<u8> {
289        bincode::encode_to_vec(&self, bincode::config::legacy()).unwrap()
290    }
291
292    /// All keys merged into a single `String` for usage via ENV vars
293    pub fn keys_as_b64(&self) -> Result<String, CryptrError> {
294        let mut keys = String::with_capacity(self.enc_keys.len() * 56);
295        for (id, key) in &self.enc_keys {
296            let kb64 = b64_encode(key);
297            writeln!(keys, "{id}/{kb64}")?;
298        }
299        Ok(keys)
300    }
301
302    /// All keys converted to b64 inside a `Vec<_>`.
303    pub fn keys_as_b64_vec(&self) -> Vec<String> {
304        self.enc_keys
305            .iter()
306            .map(|(id, key)| format!("{id}/{}", b64_encode(key)))
307            .collect::<Vec<_>>()
308    }
309
310    pub async fn save_to_file(&self, file: &str) -> Result<(), CryptrError> {
311        // check if we need to split off the a filename from a path
312        match file.rsplit_once('/') {
313            None => {
314                // in this case we have only a filename -> save to current directory
315                self.save_to_file_with_path("./", file).await
316            }
317            Some((path, file)) => self.save_to_file_with_path(path, file).await,
318        }
319    }
320
321    pub async fn save_to_file_with_path(
322        &self,
323        path: &str,
324        file_name: &str,
325    ) -> Result<(), CryptrError> {
326        if self.enc_keys.is_empty() {
327            return Err(CryptrError::Keys("EncKeys is empty - not saving anything"));
328        }
329
330        fs::create_dir_all(path).await?;
331        let path_full = format!("{path}/{file_name}");
332        if let Ok(file) = File::open(&path_full).await {
333            let meta = file.metadata().await?;
334            if meta.is_dir() {
335                return Err(CryptrError::Keys("target path is a directory"));
336            }
337        }
338
339        let mut keys = String::with_capacity(self.enc_keys.len() * 56);
340        for (id, key) in &self.enc_keys {
341            let kb64 = b64_encode(key);
342            writeln!(keys, "{id}/{kb64}")?;
343        }
344        let _ = keys.split_off(keys.len() - 1);
345
346        let content = format!(
347            "ENC_KEY_ACTIVE={}\nENC_KEYS=\"\n{keys}\n\"",
348            self.enc_key_active
349        );
350        fs::write(&path_full, content.as_bytes()).await?;
351        #[cfg(target_family = "unix")]
352        {
353            use std::fs::Permissions;
354            use std::os::unix::fs::PermissionsExt;
355            fs::set_permissions(&path_full, Permissions::from_mode(0o600)).await?;
356        }
357
358        Ok(())
359    }
360
361    /// Returns a reference to specified EncKey
362    pub fn get_key(&self, enc_key_id: &str) -> Result<&[u8], CryptrError> {
363        for (id, key) in &self.enc_keys {
364            if id.as_str() == enc_key_id {
365                return Ok(key.as_slice());
366            }
367        }
368        Err(CryptrError::Keys("EncKey ID {} does not exist"))
369    }
370
371    /// Returns a reference to the initialized EncKeys.
372    ///
373    /// `init()` must have been called at application startup for this to succeed.
374    ///
375    /// # Panics
376    ///
377    /// If the EncKeys have not been set up at startup with `init()`
378    pub fn get_static<'a>() -> &'a Self {
379        ENC_KEYS
380            .get()
381            .expect("`init()` to have been called on valid EncKeys once before")
382    }
383
384    /// Returns a reference to specified EncKey
385    ///
386    /// `init()` must have been called at application startup for this to succeed.
387    pub fn get_static_key<'a>(enc_key_id: &str) -> Result<&'a [u8], CryptrError> {
388        let keys = Self::get_static();
389        for (id, key) in &keys.enc_keys {
390            if id.as_str() == enc_key_id {
391                return Ok(key.as_slice());
392            }
393        }
394        Err(CryptrError::Keys("EncKey ID does not exist"))
395    }
396
397    /// Returns a reference to currently active EncKey
398    ///
399    /// `init()` must have been called at application startup for this to succeed.
400    pub fn get_key_active<'a>() -> Result<&'a [u8], CryptrError> {
401        let keys = Self::get_static();
402        let active_id = &keys.enc_key_active;
403        for (id, key) in &keys.enc_keys {
404            if id == active_id {
405                return Ok(key.as_slice());
406            }
407        }
408        Err(CryptrError::Keys("Active EncKey ID {} does not exist"))
409    }
410
411    /// Initialize the encryption keys statically for ease of use.
412    ///
413    /// This function **must be called** before accessing `EncKeys::get()`, or basically with
414    /// any function that uses the static keys.
415    ///
416    /// Throws an error if called more than once.
417    pub fn init(self) -> Result<(), CryptrError> {
418        if ENC_KEYS.set(self).is_err() {
419            Err(CryptrError::Keys(
420                "EncKeys::init() has already been called before",
421            ))
422        } else {
423            Ok(())
424        }
425    }
426
427    /// Generates a new random encryption key
428    pub fn generate() -> Result<Self, CryptrError> {
429        let id = secure_random_alnum(12);
430        Self::generate_with_id(id)
431    }
432
433    /// Generates a new random set of encryption keys
434    pub fn generate_multiple(number_of_keys: u16) -> Result<Self, CryptrError> {
435        if number_of_keys < 1 {
436            return Err(CryptrError::Keys("number_of_keys must be greater than 1"));
437        }
438
439        let mut enc_keys = Vec::with_capacity(number_of_keys as usize);
440        for _ in 0..number_of_keys {
441            let id = secure_random_alnum(12);
442            let key = secure_random_vec(32)?;
443            enc_keys.push((id, key))
444        }
445
446        Ok(Self {
447            enc_key_active: enc_keys.first().unwrap().0.clone(),
448            enc_keys,
449        })
450    }
451
452    /// Generates a new random encryption key with a specific ID
453    pub fn generate_with_id(id: String) -> Result<Self, CryptrError> {
454        Self::validate_id(&id, None)?;
455        let key = secure_random_vec(32)?;
456
457        Ok(Self {
458            enc_key_active: id.clone(),
459            enc_keys: vec![(id, key)],
460        })
461    }
462
463    /// Used for compatibility with the older system
464    ///
465    /// This will convert the old encryption key format into the new one
466    pub fn try_convert_legacy_keys(keys: &str) -> Result<String, CryptrError> {
467        let mut keys_map: HashMap<String, Vec<u8>> = HashMap::new();
468
469        // we need to validate the key ids, since otherwise the parsing might fail from a webauthn cookie
470        let re = Self::re_key_id();
471
472        for k in keys.split(' ') {
473            if !k.is_empty() {
474                let t: (&str, &str) = k
475                    .split_once('/')
476                    .ok_or(CryptrError::Keys("Incorrect format for ENC_KEYS"))?;
477                let id = t.0.trim();
478                let key = t.1.trim();
479
480                if id.is_empty() || key.is_empty() {
481                    return Err(CryptrError::Keys(
482                        "ENC_KEYS must not be empty. Format: \"<id>/<key> <id>/<key>\"",
483                    ));
484                }
485
486                if key.len() != 32 {
487                    error!("Encryption Key for Enc Key Id '{id}' is not 32 bytes long");
488                    return Err(CryptrError::Keys("Encryption Key is not 32 bytes long"));
489                }
490
491                if !re.is_match(id) {
492                    return Err(CryptrError::Keys(
493                        "The IDs for ENC_KEYS must match '^[a-zA-Z0-9:_-]{2,20}$'",
494                    ));
495                }
496
497                keys_map.insert(String::from(id), Vec::from(key));
498            }
499        }
500
501        let mut res = String::with_capacity(keys_map.len() * 48);
502        for (id, key) in keys_map {
503            let key_b64 = b64_encode(&key);
504            writeln!(res, "{id}/{key_b64}")?;
505        }
506
507        Ok(res)
508    }
509
510    fn validate_id(id: &str, current: Option<&EncKeys>) -> Result<(), CryptrError> {
511        if let Some(curr) = current {
512            for (key_id, _) in &curr.enc_keys {
513                if key_id == id {
514                    return Err(CryptrError::Keys("Key ID exists already"));
515                }
516            }
517        }
518
519        if Self::re_key_id().is_match(id) {
520            Ok(())
521        } else {
522            Err(CryptrError::Keys(
523                "An encryption key ID must match: ^[a-zA-Z0-9_-]{2,20}$",
524            ))
525        }
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use argon2::Params;
532
533    use super::*;
534
535    #[tokio::test]
536    #[ignore] // Overlaps with the from file test case
537    async fn test_enc_from_env() {
538        unsafe {
539            env::set_var("ENC_KEY_ACTIVE", "zQac11NaE0Nn");
540            env::set_var(
541                "ENC_KEYS",
542                r#"
543    zQac11NaE0Nn/UZFxllgmmnA5KzBr7A6uS+p/ccLe2/L4M4Vs3CMhwQg=
544    nlL1mQjkQH58/lPfvTp7RojBOU8aNzZrfYQ44ykm0SR/DaZmvMZMmXkY=
545    26VvcHiaJP26/Cu8I2NEzD2tjKV+2Tl6Dwx2tkPOMyolYP1ydTcN+hik=
546    "#,
547            );
548        }
549
550        let keys = EncKeys::from_env().unwrap();
551        assert_eq!(keys.enc_key_active.as_str(), "zQac11NaE0Nn");
552        assert_eq!(keys.enc_keys.len(), 3);
553    }
554
555    #[tokio::test]
556    async fn test_enc_from_file() {
557        let keys_len = 3;
558        let keys = EncKeys::generate_multiple(keys_len).unwrap();
559
560        let path = "./test_files";
561        let file_name = "keys";
562        keys.save_to_file_with_path(path, file_name).await.unwrap();
563
564        let path_full = format!("{}/{}", path, file_name);
565
566        let keys_from = EncKeys::read_from_file(&path_full).unwrap();
567        assert_eq!(keys, keys_from);
568        assert_eq!(keys.enc_keys.len(), keys_len as usize);
569    }
570
571    #[tokio::test]
572    async fn test_append_delete() {
573        let keys = EncKeys::generate_multiple(3).unwrap();
574        assert_eq!(keys.enc_keys.len(), 3);
575
576        let curr_active = keys.enc_key_active.clone();
577        let (id, _key) = keys.enc_keys.get(2).unwrap().clone();
578
579        let mut keys = keys;
580        let res = keys.delete(&curr_active);
581        assert!(res.is_err());
582
583        keys.delete(&id).unwrap();
584        assert_eq!(keys.enc_keys.len(), 2);
585
586        keys.append_new_random().unwrap();
587        assert_ne!(keys.enc_key_active, curr_active);
588        assert_eq!(keys.enc_keys.len(), 3);
589    }
590
591    #[test]
592    fn test_fmt_config_str() {
593        let legacy_str = "bVCyTsGaggVy5yqQ/S9n7oCen53xSJLzcsmfdnBDvNrqQ63r4 q6u26onRvXVG4427/3CEC8RJWBcMkrBMkRXgx65AmJsNTghSA";
594        let converted = EncKeys::try_convert_legacy_keys(&legacy_str)
595            .expect("legacy key conversion to be successful");
596
597        let (cfg_value, _secrets_value) = EncKeys::fmt_enc_keys_str_for_config(&converted);
598
599        println!("\n{}\n", cfg_value);
600        assert!(
601            cfg_value.contains("q6u26onRvXVG4427/M0NFQzhSSldCY01rckJNa1JYZ3g2NUFtSnNOVGdoU0E=\n")
602        );
603        assert!(
604            cfg_value.contains("bVCyTsGaggVy5yqQ/UzluN29DZW41M3hTSkx6Y3NtZmRuQkR2TnJxUTYzcjQ=\n")
605        );
606    }
607
608    #[test]
609    fn test_from_kdf_value() {
610        let kdf_value = KdfValue::new("123");
611        let id = kdf_value.enc_key_value();
612        let enc_keys = EncKeys::from(kdf_value);
613
614        assert_eq!(enc_keys.enc_key_active, id);
615        assert_eq!(enc_keys.enc_keys.len(), 1);
616    }
617
618    #[test]
619    fn test_from_kdf_value_with_params() {
620        let params = Params::new(Params::MIN_M_COST, 5, 1, Some(32)).unwrap();
621        let kdf_value = KdfValue::new_with_params("123", params);
622        let id1 = kdf_value.enc_key_value();
623        let enc_keys1 = EncKeys::from(kdf_value);
624        assert_eq!(enc_keys1.enc_key_active, id1);
625        assert_eq!(enc_keys1.enc_keys.len(), 1);
626
627        // Check that it's not the same as the default params with the same password
628        let kdf_value = KdfValue::new("123");
629        let id2 = kdf_value.enc_key_value();
630        let enc_keys2 = EncKeys::from(kdf_value);
631        assert_eq!(enc_keys2.enc_key_active, id2);
632        assert_eq!(enc_keys2.enc_keys.len(), 1);
633
634        assert_ne!(enc_keys1.enc_key_active, enc_keys2.enc_key_active);
635        assert_ne!(enc_keys1.into_bytes(), enc_keys2.into_bytes());
636    }
637}