1use aes_gcm::{
2 Aes256Gcm,
3 aead::{Aead, Generate, KeyInit, Nonce, Payload},
4};
5use base64::{
6 Engine as _,
7 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
8};
9
10const ENVELOPE_PREFIX: &str = "RULLST";
11const ENVELOPE_VERSION: &str = "v2";
12const DEFAULT_KEY_ID: &str = "default";
13const KEY_LENGTH: usize = 32;
14const NONCE_LENGTH: usize = 12;
15const TAG_LENGTH: usize = 16;
16const MAX_KEY_ID_LENGTH: usize = 128;
17const AAD_DOMAIN: &[u8] = b"rullst-orm:field-encryption:aes-256-gcm";
18const KEY_ENV: &str = "RULLST_ENCRYPTION_KEY";
19const KEY_ID_ENV: &str = "RULLST_ENCRYPTION_KEY_ID";
20const KEYRING_ENV: &str = "RULLST_ENCRYPTION_KEYRING";
21
22mod secret_serde;
23#[doc(hidden)]
24pub use secret_serde::with_redacted_secrets;
25pub use secret_serde::{deserialize_optional_plaintext_secret, deserialize_plaintext_secret};
26mod settings;
27mod sqlx_codec;
28
29#[derive(Clone, PartialEq, Eq)]
42pub struct SecretString(String);
43
44impl SecretString {
45 pub fn new(val: impl Into<String>) -> Self {
46 SecretString(val.into())
47 }
48
49 pub fn reveal_audited(&self) -> &str {
52 &self.0
53 }
54}
55
56impl std::fmt::Debug for SecretString {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.write_str("[ENCRYPTED_SECRET]")
60 }
61}
62
63pub struct PrivacyReport {
65 pub table_name: String,
68 pub has_encrypted_data: bool,
70 pub encrypted_fields: Vec<&'static str>,
73}
74
75pub trait ComplianceModel {
76 fn compliance_schema() -> PrivacyReport;
77
78 fn personal_fields() -> Vec<&'static str> {
82 Vec::new()
83 }
84}
85
86#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
88#[non_exhaustive]
89pub enum PrivacyError {
90 #[error("RULLST_ENCRYPTION_KEY must be exactly 32 bytes long")]
92 InvalidKeyLength,
93
94 #[error("RULLST_ENCRYPTION_KEY has invalid {0} encoding")]
96 InvalidKeyEncoding(&'static str),
97
98 #[error("Invalid field-encryption key identifier")]
100 InvalidKeyId,
101
102 #[error("Invalid field-encryption envelope")]
104 InvalidEnvelope,
105
106 #[error("Unsupported field-encryption envelope version: {0}")]
108 UnsupportedEnvelopeVersion(String),
109
110 #[error("Field-encryption key `{0}` was not found in the configured keyring")]
112 KeyNotFound(String),
113
114 #[error("Operating-system randomness is unavailable")]
116 RandomnessUnavailable,
117
118 #[error("Encryption failed: {0}")]
120 EncryptionFailed(String),
121
122 #[error("Decryption failed: {0}")]
124 DecryptionFailed(String),
125
126 #[error("Invalid encrypted payload (too short)")]
128 PayloadTooShort,
129
130 #[error("Base64 decode error: {0}")]
132 Base64Error(String),
133
134 #[error("UTF-8 decoding error: {0}")]
136 Utf8Error(String),
137
138 #[error("Environment variable error: {0}")]
140 EnvError(String),
141}
142
143fn validate_key(key: &[u8]) -> Result<(), PrivacyError> {
144 if key.len() == KEY_LENGTH {
145 Ok(())
146 } else {
147 Err(PrivacyError::InvalidKeyLength)
148 }
149}
150
151fn validate_key_id(key_id: &str) -> Result<(), PrivacyError> {
152 if key_id.is_empty()
153 || key_id.len() > MAX_KEY_ID_LENGTH
154 || !key_id
155 .bytes()
156 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
157 {
158 Err(PrivacyError::InvalidKeyId)
159 } else {
160 Ok(())
161 }
162}
163
164fn authenticated_data(key_id: &str, context: &[u8]) -> Vec<u8> {
165 let mut aad = Vec::with_capacity(AAD_DOMAIN.len() + key_id.len() + context.len() + 4);
166 aad.extend_from_slice(AAD_DOMAIN);
167 aad.push(0);
168 aad.extend_from_slice(ENVELOPE_VERSION.as_bytes());
169 aad.push(0);
170 aad.extend_from_slice(key_id.as_bytes());
171 aad.push(0);
172 aad.extend_from_slice(context);
173 aad
174}
175
176fn model_context(table: &str, column: &str) -> Vec<u8> {
177 let mut context = Vec::with_capacity(table.len() + column.len() + 1);
178 context.extend_from_slice(table.as_bytes());
179 context.push(0);
180 context.extend_from_slice(column.as_bytes());
181 context
182}
183
184fn encrypt_with_context(
185 plaintext: &str,
186 key: &[u8],
187 key_id: &str,
188 context: &[u8],
189) -> Result<String, PrivacyError> {
190 validate_key(key)?;
191 validate_key_id(key_id)?;
192 let cipher = Aes256Gcm::new_from_slice(key)
193 .map_err(|error| PrivacyError::EncryptionFailed(error.to_string()))?;
194 let nonce =
195 Nonce::<Aes256Gcm>::try_generate().map_err(|_| PrivacyError::RandomnessUnavailable)?;
196 let aad = authenticated_data(key_id, context);
197 let ciphertext = cipher
198 .encrypt(
199 &nonce,
200 Payload {
201 msg: plaintext.as_bytes(),
202 aad: &aad,
203 },
204 )
205 .map_err(|error| PrivacyError::EncryptionFailed(error.to_string()))?;
206
207 Ok(format!(
208 "{ENVELOPE_PREFIX}:{ENVELOPE_VERSION}:{key_id}:{}:{}",
209 URL_SAFE_NO_PAD.encode(nonce),
210 URL_SAFE_NO_PAD.encode(ciphertext)
211 ))
212}
213
214struct ParsedEnvelope<'a> {
215 key_id: &'a str,
216 nonce: Vec<u8>,
217 ciphertext: Vec<u8>,
218}
219
220fn parse_envelope(encrypted: &str) -> Result<ParsedEnvelope<'_>, PrivacyError> {
221 let mut fields = encrypted.split(':');
222 if fields.next() != Some(ENVELOPE_PREFIX) {
223 return Err(PrivacyError::InvalidEnvelope);
224 }
225 let version = fields.next().ok_or(PrivacyError::InvalidEnvelope)?;
226 if version != ENVELOPE_VERSION {
227 return Err(PrivacyError::UnsupportedEnvelopeVersion(
228 version.to_string(),
229 ));
230 }
231 let key_id = fields.next().ok_or(PrivacyError::InvalidEnvelope)?;
232 validate_key_id(key_id)?;
233 let encoded_nonce = fields.next().ok_or(PrivacyError::InvalidEnvelope)?;
234 let encoded_ciphertext = fields.next().ok_or(PrivacyError::InvalidEnvelope)?;
235 if fields.next().is_some() {
236 return Err(PrivacyError::InvalidEnvelope);
237 }
238
239 let nonce = URL_SAFE_NO_PAD
240 .decode(encoded_nonce)
241 .map_err(|error| PrivacyError::Base64Error(error.to_string()))?;
242 if nonce.len() != NONCE_LENGTH {
243 return Err(PrivacyError::PayloadTooShort);
244 }
245 let ciphertext = URL_SAFE_NO_PAD
246 .decode(encoded_ciphertext)
247 .map_err(|error| PrivacyError::Base64Error(error.to_string()))?;
248 if ciphertext.len() < TAG_LENGTH {
249 return Err(PrivacyError::PayloadTooShort);
250 }
251
252 Ok(ParsedEnvelope {
253 key_id,
254 nonce,
255 ciphertext,
256 })
257}
258
259fn decrypt_envelope(
260 envelope: &ParsedEnvelope<'_>,
261 key: &[u8],
262 context: &[u8],
263) -> Result<String, PrivacyError> {
264 validate_key(key)?;
265 let cipher = Aes256Gcm::new_from_slice(key)
266 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
267 let nonce = Nonce::<Aes256Gcm>::try_from(envelope.nonce.as_slice())
268 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
269 let aad = authenticated_data(envelope.key_id, context);
270 let plaintext = cipher
271 .decrypt(
272 &nonce,
273 Payload {
274 msg: &envelope.ciphertext,
275 aad: &aad,
276 },
277 )
278 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
279 String::from_utf8(plaintext).map_err(|error| PrivacyError::Utf8Error(error.to_string()))
280}
281
282fn current_key_id() -> Result<String, PrivacyError> {
283 match settings::setting(KEY_ID_ENV)? {
284 Some(key_id) => {
285 validate_key_id(&key_id)?;
286 Ok(key_id)
287 }
288 None => Ok(DEFAULT_KEY_ID.to_string()),
289 }
290}
291
292fn decode_configured_key(value: &str) -> Result<Vec<u8>, PrivacyError> {
293 let key = if let Some(encoded) = value.strip_prefix("base64:") {
294 STANDARD
295 .decode(encoded)
296 .map_err(|_| PrivacyError::InvalidKeyEncoding("base64"))?
297 } else if let Some(encoded) = value.strip_prefix("hex:") {
298 let (pairs, remainder) = encoded.as_bytes().as_chunks::<2>();
299 if !remainder.is_empty() {
300 return Err(PrivacyError::InvalidKeyEncoding("hex"));
301 }
302 pairs
303 .iter()
304 .map(|pair| {
305 let pair = std::str::from_utf8(pair)
306 .map_err(|_| PrivacyError::InvalidKeyEncoding("hex"))?;
307 u8::from_str_radix(pair, 16).map_err(|_| PrivacyError::InvalidKeyEncoding("hex"))
308 })
309 .collect::<Result<Vec<_>, _>>()?
310 } else {
311 value.as_bytes().to_vec()
312 };
313 validate_key(&key)?;
314 Ok(key)
315}
316
317fn current_key() -> Result<Vec<u8>, PrivacyError> {
318 decode_configured_key(&settings::required_setting(KEY_ENV)?)
319}
320
321fn parse_keyring(
322 keyring: &str,
323) -> Result<serde_json::Map<String, serde_json::Value>, PrivacyError> {
324 serde_json::from_str(keyring).map_err(|error| {
325 PrivacyError::EnvError(format!(
326 "{KEYRING_ENV} must be a JSON object of key IDs to 32-byte keys: {error}"
327 ))
328 })
329}
330
331fn configured_key(key_id: &str) -> Result<Vec<u8>, PrivacyError> {
332 if current_key_id()? == key_id {
333 return current_key();
334 }
335
336 let values = parse_keyring(&settings::required_setting(KEYRING_ENV)?)?;
337 let key = values
338 .get(key_id)
339 .and_then(serde_json::Value::as_str)
340 .ok_or_else(|| PrivacyError::KeyNotFound(key_id.to_string()))?
341 .to_string();
342 decode_configured_key(&key)
343}
344
345fn decrypt_legacy_configured(encrypted: &str) -> Result<String, PrivacyError> {
350 let rejected = match decrypt_legacy_aes_gcm(encrypted, ¤t_key()?) {
351 Err(error @ PrivacyError::DecryptionFailed(_)) => error,
352 other => return other,
353 };
354 let Some(keyring) = settings::setting(KEYRING_ENV)? else {
355 return Err(rejected);
356 };
357 let values = parse_keyring(&keyring)?;
358 let candidates = values.get(DEFAULT_KEY_ID).into_iter().chain(
359 values
360 .iter()
361 .filter(|(key_id, _)| key_id.as_str() != DEFAULT_KEY_ID)
362 .map(|(_, key)| key),
363 );
364 for candidate in candidates {
365 let key = candidate.as_str().ok_or_else(|| {
366 PrivacyError::EnvError(format!("{KEYRING_ENV} values must be key strings"))
367 })?;
368 match decrypt_legacy_aes_gcm(encrypted, &decode_configured_key(key)?) {
369 Err(PrivacyError::DecryptionFailed(_)) => continue,
370 other => return other,
371 }
372 }
373 Err(rejected)
374}
375
376pub fn encrypt_model_field(
383 plaintext: &str,
384 table: &str,
385 column: &str,
386) -> Result<String, PrivacyError> {
387 let key_id = current_key_id()?;
388 let key = current_key()?;
389 encrypt_with_context(plaintext, &key, &key_id, &model_context(table, column))
390}
391
392pub fn decrypt_model_field(
395 encrypted: &str,
396 table: &str,
397 column: &str,
398) -> Result<String, PrivacyError> {
399 let envelope = parse_envelope(encrypted)?;
400 let key = configured_key(envelope.key_id)?;
401 decrypt_envelope(&envelope, &key, &model_context(table, column))
402}
403
404pub fn encrypt_aes_gcm(plaintext: &str, key: &str) -> Result<String, PrivacyError> {
405 encrypt_with_context(plaintext, key.as_bytes(), DEFAULT_KEY_ID, b"")
406}
407
408pub fn decrypt_aes_gcm(encrypted: &str, key: &str) -> Result<String, PrivacyError> {
409 let key_bytes = key.as_bytes();
410 validate_key(key_bytes)?;
411 if encrypted.starts_with(&format!("{ENVELOPE_PREFIX}:")) {
412 let envelope = parse_envelope(encrypted)?;
413 return decrypt_envelope(&envelope, key_bytes, b"");
414 }
415
416 decrypt_legacy_aes_gcm(encrypted, key_bytes)
417}
418
419fn decrypt_legacy_aes_gcm(encrypted: &str, key: &[u8]) -> Result<String, PrivacyError> {
420 let payload = STANDARD
423 .decode(encrypted)
424 .map_err(|error| PrivacyError::Base64Error(error.to_string()))?;
425 if payload.len() < NONCE_LENGTH + TAG_LENGTH {
426 return Err(PrivacyError::PayloadTooShort);
427 }
428
429 let cipher = Aes256Gcm::new_from_slice(key)
430 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
431 let nonce = Nonce::<Aes256Gcm>::try_from(&payload[..NONCE_LENGTH])
432 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
433 let ciphertext = &payload[NONCE_LENGTH..];
434
435 let plaintext = cipher
436 .decrypt(&nonce, ciphertext)
437 .map_err(|error| PrivacyError::DecryptionFailed(error.to_string()))?;
438
439 String::from_utf8(plaintext).map_err(|error| PrivacyError::Utf8Error(error.to_string()))
440}
441
442fn encrypt_configured_secret(plaintext: &str) -> Result<String, PrivacyError> {
443 let key_id = current_key_id()?;
444 let key = current_key()?;
445 encrypt_with_context(plaintext, &key, &key_id, b"")
446}
447
448fn decrypt_configured_secret(encrypted: &str) -> Result<String, PrivacyError> {
449 if encrypted.starts_with(&format!("{ENVELOPE_PREFIX}:")) {
450 let envelope = parse_envelope(encrypted)?;
451 let key = configured_key(envelope.key_id)?;
452 decrypt_envelope(&envelope, &key, b"")
453 } else {
454 decrypt_legacy_configured(encrypted)
455 }
456}
457
458#[cfg(test)]
459mod tests;