segment_buffer/cipher.rs
1//! Pluggable encryption for segment files at rest.
2//!
3//! The [`SegmentCipher`] trait abstracts the encrypt/decrypt operations so
4//! callers can bring any AEAD (AES-GCM, ChaCha20-Poly1305, etc.). When the
5//! `encryption` feature is enabled, a ready-made [`AesGcmCipher`] is provided.
6
7use crate::error::Result;
8
9/// Encrypts and decrypts segment file payloads.
10///
11/// Implementations must be [`Send`] + [`Sync`] because the buffer is shared
12/// across threads via `Arc<SegmentBuffer>`.
13///
14/// The ciphertext format is implementation-defined but must be self-describing:
15/// [`decrypt`](Self::decrypt) must be able to recover the plaintext from the
16/// exact bytes returned by [`encrypt`](Self::encrypt) without external state.
17///
18/// # Example
19///
20/// ```
21/// use segment_buffer::SegmentCipher;
22/// use segment_buffer::Result;
23///
24/// struct Rot13;
25///
26/// impl SegmentCipher for Rot13 {
27/// fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
28/// Ok(plaintext.iter().map(|b| b.wrapping_add(13)).collect())
29/// }
30/// fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
31/// Ok(ciphertext.iter().map(|b| b.wrapping_sub(13)).collect())
32/// }
33/// }
34/// ```
35pub trait SegmentCipher: Send + Sync {
36 /// Encrypt `plaintext`, returning self-describing ciphertext.
37 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>>;
38
39 /// Decrypt previously-produced ciphertext back to the original plaintext.
40 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>>;
41}
42
43// ---------------------------------------------------------------------------
44// AES-256-GCM implementation (behind the `encryption` feature)
45// ---------------------------------------------------------------------------
46
47#[cfg(feature = "encryption")]
48mod private {
49 use crate::error::{Result, SegmentError};
50
51 /// AES-256-GCM cipher with a random 12-byte nonce prepended to each ciphertext.
52 ///
53 /// The on-disk format is: `[12-byte nonce][ciphertext + 16-byte GCM tag]`.
54 /// This is byte-compatible with monitor365's `EncryptionKey` segment format,
55 /// so existing encrypted segments can be read without migration.
56 pub struct AesGcmCipher {
57 cipher: aes_gcm::Aes256Gcm,
58 }
59
60 impl AesGcmCipher {
61 /// Create a new cipher from a 32-byte AES-256 key.
62 ///
63 /// # Errors
64 ///
65 /// Returns [`SegmentError::Cipher`] if the key length is not 32 bytes.
66 pub fn from_slice(key_bytes: &[u8]) -> Result<Self> {
67 use aes_gcm::KeyInit;
68 let cipher = aes_gcm::Aes256Gcm::new_from_slice(key_bytes)
69 .map_err(|e| SegmentError::Cipher(format!("invalid AES-256 key: {e}")))?;
70 Ok(Self { cipher })
71 }
72
73 /// Create a new cipher from a 32-byte AES-256 key (const-sized input).
74 pub fn new(key_bytes: &[u8; 32]) -> Self {
75 use aes_gcm::KeyInit;
76 Self {
77 cipher: aes_gcm::Aes256Gcm::new_from_slice(key_bytes)
78 .expect("32-byte key is always valid for AES-256"),
79 }
80 }
81 }
82
83 impl super::SegmentCipher for AesGcmCipher {
84 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
85 use aes_gcm::aead::Aead;
86 use rand::RngCore;
87
88 let mut nonce_bytes = [0u8; 12];
89 rand::thread_rng().fill_bytes(&mut nonce_bytes);
90 let nonce = aes_gcm::Nonce::from_slice(&nonce_bytes);
91
92 let ciphertext = self
93 .cipher
94 .encrypt(nonce, plaintext)
95 .map_err(|e| SegmentError::Cipher(format!("AES-GCM encryption: {e}")))?;
96
97 let mut out = Vec::with_capacity(12 + ciphertext.len());
98 out.extend_from_slice(&nonce_bytes);
99 out.extend_from_slice(&ciphertext);
100 Ok(out)
101 }
102
103 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
104 use aes_gcm::aead::Aead;
105
106 if ciphertext.len() < 12 {
107 return Err(SegmentError::Integrity(
108 "ciphertext too small for nonce prefix".into(),
109 ));
110 }
111 let (nonce_bytes, encrypted) = ciphertext.split_at(12);
112 let nonce = aes_gcm::Nonce::from_slice(nonce_bytes);
113
114 self.cipher
115 .decrypt(nonce, encrypted)
116 .map_err(|e| SegmentError::Cipher(format!("AES-GCM decryption: {e}")))
117 }
118 }
119}
120
121#[cfg(feature = "encryption")]
122pub use private::AesGcmCipher;