Skip to main content

secrets_core/
crypto.rs

1use aes_gcm::aead::array::Array;
2use aes_gcm::aead::{Aead as _, Generate, KeyInit, Nonce};
3use aes_gcm::Aes256Gcm;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum CryptoError {
8    #[error("encryption failed")]
9    Seal,
10    #[error("decryption failed (tampered or wrong key)")]
11    Open,
12}
13
14pub trait Aead: Send + Sync {
15    fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError>;
16    fn open(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError>;
17}
18
19/// AES-256-GCM, storing `nonce || ciphertext || tag` as a single blob.
20pub struct Aes256GcmAead {
21    cipher: Aes256Gcm,
22}
23
24impl Aes256GcmAead {
25    pub fn new(key: &[u8; 32]) -> Self {
26        Self {
27            cipher: Aes256Gcm::new(key.into()),
28        }
29    }
30}
31
32impl Aead for Aes256GcmAead {
33    fn seal(&self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
34        let nonce = Nonce::<Aes256Gcm>::generate();
35        let ciphertext = self
36            .cipher
37            .encrypt(&nonce, plaintext)
38            .map_err(|_| CryptoError::Seal)?;
39        let mut out = Vec::with_capacity(nonce.len() + ciphertext.len());
40        out.extend_from_slice(&nonce);
41        out.extend_from_slice(&ciphertext);
42        Ok(out)
43    }
44
45    fn open(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
46        if blob.len() < 12 {
47            return Err(CryptoError::Open);
48        }
49        let (nonce, ciphertext) = blob.split_at(12);
50        let nonce = Array::try_from(nonce).map_err(|_| CryptoError::Open)?;
51        self.cipher
52            .decrypt(&nonce, ciphertext)
53            .map_err(|_| CryptoError::Open)
54    }
55}
56
57pub trait MasterKeyProvider: Send + Sync {
58    fn current_key(&self) -> [u8; 32];
59}
60
61/// v1 master key source: a hex-encoded 32-byte key from an env var, or (if
62/// the env var holds a path instead) read from a file. Swappable later for
63/// a KMS-backed provider without touching `Barrier` or its callers.
64pub struct StaticMasterKeyProvider {
65    key: [u8; 32],
66}
67
68impl StaticMasterKeyProvider {
69    pub fn from_hex(hex_key: &str) -> Result<Self, CryptoError> {
70        let bytes = hex::decode(hex_key.trim()).map_err(|_| CryptoError::Seal)?;
71        let key: [u8; 32] = bytes.try_into().map_err(|_| CryptoError::Seal)?;
72        Ok(Self { key })
73    }
74
75    pub fn from_env(var: &str) -> Result<Self, CryptoError> {
76        let value = std::env::var(var).map_err(|_| CryptoError::Seal)?;
77        if let Ok(contents) = std::fs::read_to_string(&value) {
78            Self::from_hex(&contents)
79        } else {
80            Self::from_hex(&value)
81        }
82    }
83}
84
85impl MasterKeyProvider for StaticMasterKeyProvider {
86    fn current_key(&self) -> [u8; 32] {
87        self.key
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    fn aead() -> Aes256GcmAead {
96        Aes256GcmAead::new(&[7u8; 32])
97    }
98
99    #[test]
100    fn round_trip() {
101        let aead = aead();
102        let plaintext = b"super secret value";
103        let sealed = aead.seal(plaintext).unwrap();
104        assert_eq!(aead.open(&sealed).unwrap(), plaintext);
105    }
106
107    #[test]
108    fn tamper_detection() {
109        let aead = aead();
110        let mut sealed = aead.seal(b"super secret value").unwrap();
111        let last = sealed.len() - 1;
112        sealed[last] ^= 0xFF;
113        assert!(aead.open(&sealed).is_err());
114    }
115
116    #[test]
117    fn wrong_key_fails() {
118        let sealed = aead().seal(b"super secret value").unwrap();
119        let other = Aes256GcmAead::new(&[9u8; 32]);
120        assert!(other.open(&sealed).is_err());
121    }
122}