1use crate::segment::error::{Result, SegmentError};
63
64use aes_gcm::aead::{Aead, KeyInit};
65use aes_gcm::{Aes256Gcm, Nonce};
66
67pub(crate) const NONCE_SIZE: usize = 12;
69
70fn fill_random(dest: &mut [u8]) {
76 use rand::TryRng;
77 rand::rngs::SysRng
78 .try_fill_bytes(dest)
79 .expect("the operating system random source must be available");
80}
81
82pub(crate) const TAG_SIZE: usize = 16;
84
85#[derive(Clone)]
87pub struct FieldEncryptionKey {
88 pub key_id: String,
90 key: [u8; 32],
92}
93
94impl FieldEncryptionKey {
95 #[must_use]
101 pub fn new(key_id: impl Into<String>, key_bytes: [u8; 32]) -> Self {
102 Self {
103 key_id: key_id.into(),
104 key: key_bytes,
105 }
106 }
107
108 #[must_use]
110 pub fn key_bytes(&self) -> &[u8; 32] {
111 &self.key
112 }
113}
114
115impl std::fmt::Debug for FieldEncryptionKey {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 f.debug_struct("FieldEncryptionKey")
118 .field("key_id", &self.key_id)
119 .field("key", &"[REDACTED]")
120 .finish()
121 }
122}
123
124#[derive(Debug, Clone, Default)]
129pub struct FieldEncryptionConfig {
130 pub columns: std::collections::HashMap<String, FieldEncryptionKey>,
132}
133
134impl FieldEncryptionConfig {
135 #[must_use]
137 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn encrypt_column(&mut self, column_name: impl Into<String>, key: FieldEncryptionKey) {
143 self.columns.insert(column_name.into(), key);
144 }
145
146 #[must_use]
148 pub fn is_encrypted(&self, column_name: &str) -> bool {
149 self.columns.contains_key(column_name)
150 }
151
152 #[must_use]
154 pub fn key_for(&self, column_name: &str) -> Option<&FieldEncryptionKey> {
155 self.columns.get(column_name)
156 }
157}
158
159pub trait FieldKeyProvider: Send + Sync {
161 fn get_key(&self, key_id: &str) -> Option<[u8; 32]>;
166}
167
168#[derive(Debug, Clone, Default)]
170pub struct StaticKeyProvider {
171 keys: std::collections::HashMap<String, [u8; 32]>,
172}
173
174impl StaticKeyProvider {
175 #[must_use]
177 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub fn add_key(&mut self, key_id: impl Into<String>, key_bytes: [u8; 32]) {
183 self.keys.insert(key_id.into(), key_bytes);
184 }
185}
186
187impl FieldKeyProvider for StaticKeyProvider {
188 fn get_key(&self, key_id: &str) -> Option<[u8; 32]> {
189 self.keys.get(key_id).copied()
190 }
191}
192
193#[derive(Debug, Clone, Copy)]
213pub(crate) struct BlockContext<'a> {
214 pub column: &'a str,
216 pub segment_created_at: i64,
218}
219
220impl BlockContext<'_> {
221 fn associated_data(self) -> Vec<u8> {
224 let mut aad = Vec::with_capacity(self.column.len() + 32);
225 aad.extend_from_slice(b"chronix-field-v1");
226 aad.extend_from_slice(&(self.column.len() as u32).to_le_bytes());
227 aad.extend_from_slice(self.column.as_bytes());
228 aad.extend_from_slice(&self.segment_created_at.to_le_bytes());
229 aad
230 }
231}
232
233pub(crate) fn encrypt_block(
237 plaintext: &[u8],
238 key: &[u8; 32],
239 context: BlockContext<'_>,
240) -> Result<Vec<u8>> {
241 let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| SegmentError::CorruptFile {
242 detail: format!("invalid encryption key: {e}"),
243 })?;
244
245 let mut nonce_bytes = [0u8; NONCE_SIZE];
248 fill_random(&mut nonce_bytes);
249 let nonce = Nonce::try_from(&nonce_bytes[..]).map_err(|_| SegmentError::CorruptFile {
250 detail: "nonce must be 12 bytes".into(),
251 })?;
252 let aad = context.associated_data();
253 let ciphertext = cipher
254 .encrypt(
255 &nonce,
256 aes_gcm::aead::Payload {
257 msg: plaintext,
258 aad: &aad,
259 },
260 )
261 .map_err(|e| SegmentError::CorruptFile {
262 detail: format!("field encryption failed: {e}"),
263 })?;
264
265 let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
266 result.extend_from_slice(&nonce);
267 result.extend_from_slice(&ciphertext);
268 Ok(result)
269}
270
271pub(crate) fn decrypt_block(
277 encrypted: &[u8],
278 key: &[u8; 32],
279 context: BlockContext<'_>,
280) -> Result<Vec<u8>> {
281 if encrypted.len() < NONCE_SIZE + TAG_SIZE {
282 return Err(SegmentError::CorruptFile {
283 detail: format!(
284 "encrypted block too short: {} bytes (need at least {})",
285 encrypted.len(),
286 NONCE_SIZE + TAG_SIZE,
287 ),
288 });
289 }
290
291 let cipher = Aes256Gcm::new_from_slice(key).map_err(|e| SegmentError::CorruptFile {
292 detail: format!("invalid decryption key: {e}"),
293 })?;
294
295 let nonce =
296 Nonce::try_from(&encrypted[..NONCE_SIZE]).map_err(|_| SegmentError::CorruptFile {
297 detail: "nonce must be 12 bytes".into(),
298 })?;
299 let ciphertext = &encrypted[NONCE_SIZE..];
300 let aad = context.associated_data();
301
302 cipher
303 .decrypt(
304 &nonce,
305 aes_gcm::aead::Payload {
306 msg: ciphertext,
307 aad: &aad,
308 },
309 )
310 .map_err(|e| SegmentError::CorruptFile {
311 detail: format!(
312 "field decryption failed (wrong key, moved block, or tampered data): {e}"
313 ),
314 })
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 fn ctx() -> BlockContext<'static> {
323 BlockContext {
324 column: "value",
325 segment_created_at: 1_700_000_000_000_000_000,
326 }
327 }
328
329 fn test_key() -> [u8; 32] {
330 let mut key = [0u8; 32];
331 for (i, b) in key.iter_mut().enumerate() {
332 *b = i as u8;
333 }
334 key
335 }
336
337 #[test]
338 fn encrypt_decrypt_roundtrip() {
339 let key = test_key();
340 let plaintext = b"Hello, field-level encryption!";
341 let encrypted = encrypt_block(plaintext, &key, ctx()).unwrap();
342 assert_ne!(encrypted, plaintext);
343 assert!(encrypted.len() > plaintext.len()); let decrypted = decrypt_block(&encrypted, &key, ctx()).unwrap();
345 assert_eq!(decrypted, plaintext);
346 }
347
348 #[test]
349 fn different_nonces_produce_different_ciphertext() {
350 let key = test_key();
351 let plaintext = b"same data";
352 let enc1 = encrypt_block(plaintext, &key, ctx()).unwrap();
353 let enc2 = encrypt_block(plaintext, &key, ctx()).unwrap();
354 assert_ne!(&enc1[..NONCE_SIZE], &enc2[..NONCE_SIZE]);
356 }
357
358 #[test]
359 fn wrong_key_fails_decryption() {
360 let key1 = test_key();
361 let mut key2 = test_key();
362 key2[0] ^= 0xFF; let encrypted = encrypt_block(b"secret", &key1, ctx()).unwrap();
364 let result = decrypt_block(&encrypted, &key2, ctx());
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn tampered_ciphertext_fails() {
370 let key = test_key();
371 let mut encrypted = encrypt_block(b"important data", &key, ctx()).unwrap();
372 let last = encrypted.len() - 1;
374 encrypted[last] ^= 0x01;
375 let result = decrypt_block(&encrypted, &key, ctx());
376 assert!(result.is_err());
377 }
378
379 #[test]
380 fn too_short_encrypted_block_fails() {
381 let key = test_key();
382 let result = decrypt_block(&[0u8; 10], &key, ctx());
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn field_encryption_config_basic() {
388 let mut config = FieldEncryptionConfig::new();
389 assert!(!config.is_encrypted("temperature"));
390
391 let key = FieldEncryptionKey::new("key-1", test_key());
392 config.encrypt_column("temperature", key);
393
394 assert!(config.is_encrypted("temperature"));
395 assert!(!config.is_encrypted("humidity"));
396 assert_eq!(config.key_for("temperature").unwrap().key_id, "key-1");
397 }
398
399 #[test]
400 fn static_key_provider_lookup() {
401 let mut provider = StaticKeyProvider::new();
402 let key = test_key();
403 provider.add_key("key-1", key);
404
405 assert_eq!(provider.get_key("key-1"), Some(key));
406 assert_eq!(provider.get_key("key-2"), None);
407 }
408
409 #[test]
410 fn field_encryption_key_debug_redacts() {
411 let key = FieldEncryptionKey::new("key-1", test_key());
412 let debug = format!("{key:?}");
413 assert!(debug.contains("REDACTED"));
414 assert!(!debug.contains(&format!("{:?}", test_key())));
415 }
416
417 #[test]
418 fn encrypt_empty_block() {
419 let key = test_key();
420 let encrypted = encrypt_block(b"", &key, ctx()).unwrap();
421 assert_eq!(encrypted.len(), NONCE_SIZE + TAG_SIZE); let decrypted = decrypt_block(&encrypted, &key, ctx()).unwrap();
423 assert!(decrypted.is_empty());
424 }
425
426 #[test]
427 fn encrypt_large_block() {
428 let key = test_key();
429 let plaintext = vec![0xABu8; 1024 * 1024]; let encrypted = encrypt_block(&plaintext, &key, ctx()).unwrap();
431 let decrypted = decrypt_block(&encrypted, &key, ctx()).unwrap();
432 assert_eq!(decrypted, plaintext);
433 }
434
435 #[test]
441 fn a_block_moved_to_another_column_fails_to_authenticate() {
442 let key = test_key();
443 let salary = encrypt_block(
444 b"180000",
445 &key,
446 BlockContext {
447 column: "salary",
448 segment_created_at: 42,
449 },
450 )
451 .unwrap();
452
453 let err = decrypt_block(
454 &salary,
455 &key,
456 BlockContext {
457 column: "public_note",
458 segment_created_at: 42,
459 },
460 )
461 .expect_err("a block must not decrypt under another column's name");
462 assert!(err.to_string().contains("decryption failed"), "{err}");
463 }
464
465 #[test]
468 fn a_block_moved_to_another_segment_fails_to_authenticate() {
469 let key = test_key();
470 let block = encrypt_block(
471 b"readings",
472 &key,
473 BlockContext {
474 column: "value",
475 segment_created_at: 1_000,
476 },
477 )
478 .unwrap();
479
480 assert!(
481 decrypt_block(
482 &block,
483 &key,
484 BlockContext {
485 column: "value",
486 segment_created_at: 2_000,
487 },
488 )
489 .is_err(),
490 "a block must not decrypt inside a different segment"
491 );
492
493 let plain = decrypt_block(
495 &block,
496 &key,
497 BlockContext {
498 column: "value",
499 segment_created_at: 1_000,
500 },
501 )
502 .expect("the block reads in its own place");
503 assert_eq!(plain, b"readings");
504 }
505}