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