oxify_storage/
encryption.rs1use anyhow::{Context, Result};
6use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use oxify_model::EncryptionMetadata;
8use rand::Rng;
9
10pub struct EncryptionService {
12 master_key: Vec<u8>,
13 key_version: u32,
14}
15
16impl EncryptionService {
17 pub fn new(master_key: Vec<u8>) -> Self {
19 Self {
20 master_key,
21 key_version: 1,
22 }
23 }
24
25 pub fn from_env() -> Result<Self> {
27 let key_b64 = std::env::var("OXIFY_MASTER_KEY")
28 .context("OXIFY_MASTER_KEY environment variable not set")?;
29
30 let master_key = BASE64
31 .decode(key_b64.as_bytes())
32 .context("Failed to decode master key from base64")?;
33
34 if master_key.len() != 32 {
35 anyhow::bail!("Master key must be 32 bytes (256 bits)");
36 }
37
38 Ok(Self {
39 master_key,
40 key_version: 1,
41 })
42 }
43
44 pub fn generate_master_key() -> Vec<u8> {
46 let mut key = vec![0u8; 32];
47 rand::rng().fill(&mut key[..]);
48 key
49 }
50
51 pub fn encrypt(&self, plaintext: &str) -> Result<(Vec<u8>, EncryptionMetadata)> {
53 use aes_gcm::{
54 aead::{Aead, KeyInit},
55 Aes256Gcm, Nonce,
56 };
57
58 let mut iv = vec![0u8; 12];
60 rand::rng().fill(&mut iv[..]);
61
62 let mut salt = vec![0u8; 32];
64 rand::rng().fill(&mut salt[..]);
65
66 let mut derived_key = [0u8; 32];
68 pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&self.master_key, &salt, 100_000, &mut derived_key);
69
70 let cipher = Aes256Gcm::new(&derived_key.into());
72 let nonce = Nonce::from_slice(&iv);
73
74 let ciphertext = cipher
76 .encrypt(nonce, plaintext.as_bytes())
77 .map_err(|e| anyhow::anyhow!("Encryption failed: {e}"))?;
78
79 let metadata = EncryptionMetadata {
81 algorithm: "AES-256-GCM".to_string(),
82 kdf: "PBKDF2-HMAC-SHA256".to_string(),
83 salt: BASE64.encode(&salt),
84 iv: BASE64.encode(&iv),
85 key_version: self.key_version,
86 };
87
88 Ok((ciphertext, metadata))
89 }
90
91 pub fn decrypt(&self, ciphertext: &[u8], metadata: &EncryptionMetadata) -> Result<String> {
93 use aes_gcm::{
94 aead::{Aead, KeyInit},
95 Aes256Gcm, Nonce,
96 };
97
98 if metadata.algorithm != "AES-256-GCM" {
100 anyhow::bail!("Unsupported encryption algorithm: {}", metadata.algorithm);
101 }
102
103 let salt = BASE64
105 .decode(metadata.salt.as_bytes())
106 .context("Failed to decode salt")?;
107 let iv = BASE64
108 .decode(metadata.iv.as_bytes())
109 .context("Failed to decode IV")?;
110
111 let mut derived_key = [0u8; 32];
113 pbkdf2::pbkdf2_hmac::<sha2::Sha256>(&self.master_key, &salt, 100_000, &mut derived_key);
114
115 let cipher = Aes256Gcm::new(&derived_key.into());
117 let nonce = Nonce::from_slice(&iv);
118
119 let plaintext = cipher
121 .decrypt(nonce, ciphertext)
122 .map_err(|e| anyhow::anyhow!("Decryption failed: {e}"))?;
123
124 String::from_utf8(plaintext).context("Decrypted value is not valid UTF-8")
125 }
126
127 pub fn rotate_key(&mut self, new_master_key: Vec<u8>) {
129 self.master_key = new_master_key;
130 self.key_version += 1;
131 }
132
133 pub fn key_version(&self) -> u32 {
135 self.key_version
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn test_encrypt_decrypt() {
145 let master_key = EncryptionService::generate_master_key();
146 let service = EncryptionService::new(master_key);
147
148 let plaintext = "my-secret-api-key";
149 let (ciphertext, metadata) = service.encrypt(plaintext).unwrap();
150
151 assert_ne!(ciphertext, plaintext.as_bytes());
153
154 let decrypted = service.decrypt(&ciphertext, &metadata).unwrap();
156 assert_eq!(decrypted, plaintext);
157 }
158
159 #[test]
160 fn test_different_ivs() {
161 let master_key = EncryptionService::generate_master_key();
162 let service = EncryptionService::new(master_key);
163
164 let plaintext = "same-plaintext";
165
166 let (ciphertext1, metadata1) = service.encrypt(plaintext).unwrap();
167 let (ciphertext2, metadata2) = service.encrypt(plaintext).unwrap();
168
169 assert_ne!(metadata1.iv, metadata2.iv);
171 assert_ne!(ciphertext1, ciphertext2);
172
173 let decrypted1 = service.decrypt(&ciphertext1, &metadata1).unwrap();
175 let decrypted2 = service.decrypt(&ciphertext2, &metadata2).unwrap();
176
177 assert_eq!(decrypted1, plaintext);
178 assert_eq!(decrypted2, plaintext);
179 }
180
181 #[test]
182 fn test_wrong_key_fails() {
183 let master_key1 = EncryptionService::generate_master_key();
184 let master_key2 = EncryptionService::generate_master_key();
185
186 let service1 = EncryptionService::new(master_key1);
187 let service2 = EncryptionService::new(master_key2);
188
189 let plaintext = "secret-data";
190 let (ciphertext, metadata) = service1.encrypt(plaintext).unwrap();
191
192 let result = service2.decrypt(&ciphertext, &metadata);
194 assert!(result.is_err());
195 }
196}