Skip to main content

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;