1use std::sync::Arc;
28
29use base64::Engine as _;
30use zeroize::Zeroizing;
31
32use crate::error::{Error, Result};
33use crate::manager::EncryptionManager;
34
35const MASTER_KEY_ENV: &str = "ENCRYPTION_MASTER_KEY";
37const ALGORITHM_ENV: &str = "ENCRYPTION_ALGORITHM";
39const DEFAULT_ALGORITHM: &str = "aes-256-gcm";
41
42#[derive(Debug, Clone)]
47pub struct Guard {
48 manager: Arc<EncryptionManager>,
49}
50
51impl Guard {
52 pub fn new(manager: EncryptionManager) -> Self {
54 Self {
55 manager: Arc::new(manager),
56 }
57 }
58
59 pub fn from_master_key(master_key: &[u8], default_identifier: &str) -> Result<Self> {
63 Ok(Self::new(
64 crate::factory::EncryptionManagerFactory::from_master_key(
65 master_key,
66 default_identifier,
67 )?,
68 ))
69 }
70
71 pub fn from_env() -> Result<Self> {
76 Self::from_env_with(|key| std::env::var(key).ok())
77 }
78
79 pub fn from_env_with<F: Fn(&str) -> Option<String>>(lookup: F) -> Result<Self> {
104 let raw = Zeroizing::new(lookup(MASTER_KEY_ENV).ok_or(Error::MissingConfig {
106 key: MASTER_KEY_ENV,
107 })?);
108 let algorithm = lookup(ALGORITHM_ENV).unwrap_or_else(|| DEFAULT_ALGORITHM.to_owned());
109 let master_key = Zeroizing::new(decode_master_key(raw.as_str())?);
111 Self::from_master_key(master_key.as_slice(), &algorithm)
112 }
113
114 pub fn manager(&self) -> &EncryptionManager {
116 &self.manager
117 }
118
119 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
121 self.manager.encrypt(plaintext)
122 }
123
124 pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
126 self.manager.decrypt(ciphertext)
127 }
128
129 pub fn encrypt_with(&self, identifier: &str, plaintext: &[u8]) -> Result<Vec<u8>> {
131 self.manager.encrypt_with(identifier, plaintext)
132 }
133
134 pub fn decrypt_with(&self, identifier: &str, ciphertext: &[u8]) -> Result<Vec<u8>> {
136 self.manager.decrypt_with(identifier, ciphertext)
137 }
138
139 pub fn identifiers(&self) -> Vec<&str> {
141 self.manager.registry().identifiers()
142 }
143}
144
145fn decode_master_key(raw: &str) -> Result<[u8; 32]> {
148 let value = raw.trim();
149 let (is_hex, payload) = if let Some(hex) = value.strip_prefix("hex:") {
150 (true, hex)
151 } else if let Some(b64) = value.strip_prefix("base64:") {
152 (false, b64)
153 } else if value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit()) {
154 (true, value)
155 } else {
156 (false, value)
157 };
158
159 let decoded = Zeroizing::new(
162 if is_hex {
163 crate::internal::hex_decode(payload).ok()
164 } else {
165 base64::engine::general_purpose::STANDARD
166 .decode(payload)
167 .ok()
168 }
169 .ok_or(Error::InvalidMasterKey { got: 0 })?,
171 );
172
173 decoded
174 .as_slice()
175 .try_into()
176 .map_err(|_| Error::InvalidMasterKey { got: decoded.len() })
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn guard() -> Guard {
184 Guard::from_master_key(&[7u8; 32], "aes-256-gcm").unwrap()
185 }
186
187 #[test]
188 fn wraps_the_factory_and_forwards() {
189 let guard = guard();
190 assert_eq!(guard.identifiers().len(), 5);
191 assert_eq!(guard.manager().default_identifier(), "aes-256-gcm");
192
193 let blob = guard.encrypt(b"phone").unwrap();
194 assert_eq!(guard.decrypt(&blob).unwrap(), b"phone");
195
196 let blob = guard.encrypt_with("sm4-cbc", b"phone").unwrap();
197 assert_eq!(guard.decrypt_with("sm4-cbc", &blob).unwrap(), b"phone");
198 }
199
200 #[test]
201 fn clones_share_one_manager() {
202 let guard = guard();
203 let clone = guard.clone();
204 let blob = guard.encrypt(b"shared").unwrap();
205 assert_eq!(clone.decrypt(&blob).unwrap(), b"shared");
206 assert_eq!(Arc::strong_count(&guard.manager), 2);
207 }
208
209 #[test]
210 fn invalid_master_key_fails_at_construction() {
211 assert!(Guard::from_master_key(&[0u8; 16], "aes-256-gcm").is_err());
212 assert!(Guard::from_master_key(&[0u8; 32], "no-such-algorithm").is_err());
213 }
214
215 fn fake_env(entries: Vec<(&str, String)>) -> impl Fn(&str) -> Option<String> {
217 move |key| {
218 entries
219 .iter()
220 .find(|(k, _)| *k == key)
221 .map(|(_, v)| v.clone())
222 }
223 }
224
225 fn roundtrip(guard: &Guard) {
226 let blob = guard.encrypt(b"phone").unwrap();
227 assert_eq!(guard.decrypt(&blob).unwrap(), b"phone");
228 }
229
230 #[test]
231 fn from_env_hex_prefix() {
232 let hex = "07".repeat(32);
233 let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("hex:{hex}"))]);
234 let guard = Guard::from_env_with(lookup).unwrap();
235 assert_eq!(guard.manager().default_identifier(), "aes-256-gcm");
236 roundtrip(&guard);
237 }
238
239 #[test]
240 fn from_env_base64_prefix() {
241 let b64 = "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=";
243 let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("base64:{b64}"))]);
244 let guard = Guard::from_env_with(lookup).unwrap();
245 roundtrip(&guard);
246 }
247
248 #[test]
249 fn from_env_bare_hex_auto_detected() {
250 let hex = "00".repeat(32);
251 let lookup = fake_env(vec![(MASTER_KEY_ENV, hex)]);
252 let guard = Guard::from_env_with(lookup).unwrap();
253 roundtrip(&guard);
254 }
255
256 #[test]
257 fn from_env_missing_master_key() {
258 let err = Guard::from_env_with(fake_env(vec![])).unwrap_err();
259 assert_eq!(
260 err,
261 Error::MissingConfig {
262 key: "ENCRYPTION_MASTER_KEY"
263 }
264 );
265 }
266
267 #[test]
268 fn from_env_wrong_length() {
269 let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("hex:{}", "00".repeat(16)))]);
271 assert_eq!(
272 Guard::from_env_with(lookup).unwrap_err(),
273 Error::InvalidMasterKey { got: 16 }
274 );
275
276 let lookup = fake_env(vec![(MASTER_KEY_ENV, format!("base64:{}", "A".repeat(32)))]);
278 assert_eq!(
279 Guard::from_env_with(lookup).unwrap_err(),
280 Error::InvalidMasterKey { got: 24 }
281 );
282
283 let lookup = fake_env(vec![(MASTER_KEY_ENV, "hex:zz".to_owned())]);
285 assert_eq!(
286 Guard::from_env_with(lookup).unwrap_err(),
287 Error::InvalidMasterKey { got: 0 }
288 );
289 }
290
291 #[test]
292 fn from_env_respects_algorithm() {
293 let lookup = fake_env(vec![
294 (MASTER_KEY_ENV, "07".repeat(32)),
295 (ALGORITHM_ENV, "sm4-cbc".to_owned()),
296 ]);
297 let guard = Guard::from_env_with(lookup).unwrap();
298 assert_eq!(guard.manager().default_identifier(), "sm4-cbc");
299 roundtrip(&guard);
300 }
301
302 #[test]
303 fn from_env_unknown_algorithm_bubbles() {
304 let lookup = fake_env(vec![
305 (MASTER_KEY_ENV, "07".repeat(32)),
306 (ALGORITHM_ENV, "no-such-algorithm".to_owned()),
307 ]);
308 assert!(matches!(
309 Guard::from_env_with(lookup).unwrap_err(),
310 Error::UnknownIdentifier { .. }
311 ));
312 }
313}