Skip to main content

confex_core/
crypto.rs

1use std::path::Path;
2
3use aes_gcm::{
4    Aes256Gcm, Nonce,
5    aead::{Aead, AeadCore, KeyInit},
6};
7use rand::rngs::OsRng;
8
9use crate::{
10    error::{CryptoError, SourceError},
11    format::Format,
12    key::SecretKey,
13    value::ConfigValue,
14};
15
16/// Symmetric encrypt/decrypt over raw bytes.
17pub trait Encryptor: Send + Sync {
18    /// # Errors
19    /// Returns [`CryptoError::Encrypt`] on failure.
20    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError>;
21
22    /// # Errors
23    /// Returns [`CryptoError::Decrypt`] or [`CryptoError::InvalidFormat`] on failure.
24    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError>;
25}
26
27/// AES-256-GCM encryptor. Prepends the 12-byte nonce to the ciphertext.
28pub struct Aes256GcmEncryptor {
29    cipher: Aes256Gcm,
30}
31
32impl From<&SecretKey> for Aes256GcmEncryptor {
33    fn from(key: &SecretKey) -> Self {
34        Self {
35            cipher: Aes256Gcm::new(key.as_bytes().into()),
36        }
37    }
38}
39
40const NONCE_LEN: usize = 12;
41
42impl Encryptor for Aes256GcmEncryptor {
43    fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
44        let nonce = Aes256Gcm::generate_nonce(OsRng);
45        let ct = self
46            .cipher
47            .encrypt(&nonce, plaintext)
48            .map_err(|_| CryptoError::Encrypt)?;
49
50        let mut out = Vec::with_capacity(NONCE_LEN + ct.len());
51        out.extend_from_slice(&nonce);
52        out.extend_from_slice(&ct);
53        Ok(out)
54    }
55
56    fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
57        if ciphertext.len() < NONCE_LEN {
58            return Err(CryptoError::InvalidFormat);
59        }
60        let (nonce_bytes, ct) = ciphertext.split_at(NONCE_LEN);
61        let nonce = Nonce::from_slice(nonce_bytes);
62        self.cipher
63            .decrypt(nonce, ct)
64            .map_err(|_| CryptoError::Decrypt)
65    }
66}
67
68/// Encrypt the string values of named fields in a `ConfigValue::Table`.
69///
70/// Encrypted bytes are hex-encoded and stored back as strings.
71/// Recurses into nested tables.
72///
73/// # Errors
74/// Returns [`CryptoError::NotAString`] if a targeted field is not a string value.
75pub fn encrypt_fields(
76    value: &mut ConfigValue,
77    field_names: &[&str],
78    encryptor: &dyn Encryptor,
79) -> Result<(), CryptoError> {
80    let ConfigValue::Table(table) = value else {
81        return Ok(());
82    };
83    for (key, val) in table.iter_mut() {
84        if field_names.contains(&key.as_str()) {
85            let ConfigValue::String(s) = val else {
86                return Err(CryptoError::NotAString(key.clone()));
87            };
88            let encrypted = encryptor.encrypt(s.as_bytes())?;
89            *val = ConfigValue::String(hex::encode(encrypted));
90        } else if matches!(val, ConfigValue::Table(_)) {
91            encrypt_fields(val, field_names, encryptor)?;
92        }
93    }
94    Ok(())
95}
96
97/// Decrypt hex-encoded string values of named fields in a `ConfigValue::Table`.
98///
99/// # Errors
100/// Returns an error if hex decoding, decryption, or UTF-8 conversion fails.
101pub fn decrypt_fields(
102    value: &mut ConfigValue,
103    field_names: &[&str],
104    encryptor: &dyn Encryptor,
105) -> Result<(), CryptoError> {
106    let ConfigValue::Table(table) = value else {
107        return Ok(());
108    };
109    for (key, val) in table.iter_mut() {
110        if field_names.contains(&key.as_str()) {
111            let ConfigValue::String(s) = val else {
112                return Err(CryptoError::NotAString(key.clone()));
113            };
114            let bytes = hex::decode(s.as_str()).map_err(|_| CryptoError::InvalidFormat)?;
115            let plain = encryptor.decrypt(&bytes)?;
116            *val = ConfigValue::String(String::from_utf8(plain).map_err(|_| CryptoError::Decrypt)?);
117        } else if matches!(val, ConfigValue::Table(_)) {
118            decrypt_fields(val, field_names, encryptor)?;
119        }
120    }
121    Ok(())
122}
123
124/// Wraps a [`Format`] to transparently decrypt on parse and encrypt on serialize.
125///
126/// Use this for "app data" files that should be opaque on disk (e.g. session tokens).
127pub struct EncryptedFormat<'a, F: Format> {
128    inner: F,
129    encryptor: &'a dyn Encryptor,
130}
131
132impl<'a, F: Format> EncryptedFormat<'a, F> {
133    #[must_use]
134    pub fn new(inner: F, encryptor: &'a dyn Encryptor) -> Self {
135        Self { inner, encryptor }
136    }
137}
138
139impl<F: Format> Format for EncryptedFormat<'_, F> {
140    fn name(&self) -> &'static str {
141        self.inner.name()
142    }
143
144    fn parse(&self, path: &Path, bytes: &[u8]) -> Result<ConfigValue, SourceError> {
145        let decrypted = self.encryptor.decrypt(bytes)?;
146        self.inner.parse(path, &decrypted)
147    }
148
149    fn serialize(&self, value: &ConfigValue) -> Result<Vec<u8>, SourceError> {
150        let bytes = self.inner.serialize(value)?;
151        Ok(self.encryptor.encrypt(&bytes)?)
152    }
153}