1use std::sync::Arc;
23
24use tracing::debug;
25
26use chronix_core::{NamespaceId, ShardId};
27
28use crate::storage::backend::{SegmentPath, StorageBackend};
29use crate::storage::error::{Result, StorageError};
30
31#[derive(Debug, Clone)]
36pub struct EncryptingBackend<B> {
37 inner: B,
38 encryption: Arc<chronix_security::auth::EncryptionService>,
39}
40
41impl<B> EncryptingBackend<B> {
42 pub fn new(inner: B, encryption: Arc<chronix_security::auth::EncryptionService>) -> Self {
44 Self { inner, encryption }
45 }
46
47 #[must_use]
49 pub fn inner(&self) -> &B {
50 &self.inner
51 }
52}
53
54fn seal(
56 encryption: &chronix_security::auth::EncryptionService,
57 plaintext: &[u8],
58) -> Result<Vec<u8>> {
59 let (ciphertext, key_id) = encryption
60 .encrypt(plaintext)
61 .map_err(|e| StorageError::Io(std::io::Error::other(e.to_string())))?;
62
63 let key_id_bytes = key_id.as_bytes();
64 let key_id_len = u32::try_from(key_id_bytes.len()).map_err(|_| {
65 StorageError::Io(std::io::Error::new(
66 std::io::ErrorKind::InvalidData,
67 "key_id too long",
68 ))
69 })?;
70
71 let mut envelope = Vec::with_capacity(4 + key_id_bytes.len() + ciphertext.len());
72 envelope.extend_from_slice(&key_id_len.to_le_bytes());
73 envelope.extend_from_slice(key_id_bytes);
74 envelope.extend_from_slice(&ciphertext);
75 Ok(envelope)
76}
77
78fn unseal(
80 encryption: &chronix_security::auth::EncryptionService,
81 envelope: &[u8],
82) -> Result<Vec<u8>> {
83 if envelope.len() < 4 {
84 return Err(StorageError::Io(std::io::Error::new(
85 std::io::ErrorKind::InvalidData,
86 "encrypted envelope too short",
87 )));
88 }
89
90 let key_id_len = u32::from_le_bytes(envelope[..4].try_into().expect("4 bytes")) as usize;
91
92 if envelope.len() < 4 + key_id_len {
93 return Err(StorageError::Io(std::io::Error::new(
94 std::io::ErrorKind::InvalidData,
95 "encrypted envelope truncated at key_id",
96 )));
97 }
98
99 let key_id = std::str::from_utf8(&envelope[4..4 + key_id_len]).map_err(|_| {
100 StorageError::Io(std::io::Error::new(
101 std::io::ErrorKind::InvalidData,
102 "key_id is not valid UTF-8",
103 ))
104 })?;
105
106 let ciphertext = &envelope[4 + key_id_len..];
107
108 encryption.decrypt(ciphertext, key_id).map_err(|e| {
109 StorageError::Io(std::io::Error::new(
110 std::io::ErrorKind::InvalidData,
111 format!("decryption failed: {e}"),
112 ))
113 })
114}
115
116impl<B: StorageBackend> StorageBackend for EncryptingBackend<B> {
117 async fn put_segment(&self, path: &SegmentPath, data: &[u8]) -> Result<()> {
118 let envelope = seal(&self.encryption, data)?;
119 debug!(
120 path = %path,
121 plaintext_bytes = data.len(),
122 encrypted_bytes = envelope.len(),
123 "encrypting segment"
124 );
125 self.inner.put_segment(path, &envelope).await
126 }
127
128 async fn get_segment(&self, path: &SegmentPath) -> Result<Vec<u8>> {
129 let envelope = self.inner.get_segment(path).await?;
130 let plaintext = unseal(&self.encryption, &envelope)?;
131 debug!(
132 path = %path,
133 encrypted_bytes = envelope.len(),
134 plaintext_bytes = plaintext.len(),
135 "decrypting segment"
136 );
137 Ok(plaintext)
138 }
139
140 async fn get_range(&self, path: &SegmentPath, offset: u64, length: usize) -> Result<Vec<u8>> {
141 let plaintext = self.get_segment(path).await?;
145 let end = offset as usize + length;
146 if end > plaintext.len() {
147 return Err(StorageError::Io(std::io::Error::new(
148 std::io::ErrorKind::UnexpectedEof,
149 format!(
150 "range [{offset}..{end}) exceeds decrypted segment size {}",
151 plaintext.len()
152 ),
153 )));
154 }
155 Ok(plaintext[offset as usize..end].to_vec())
156 }
157
158 async fn delete_segment(&self, path: &SegmentPath) -> Result<()> {
159 self.inner.delete_segment(path).await
160 }
161
162 async fn list_segments(
163 &self,
164 namespace: &NamespaceId,
165 shard_id: &ShardId,
166 ) -> Result<Vec<SegmentPath>> {
167 self.inner.list_segments(namespace, shard_id).await
168 }
169
170 async fn exists(&self, path: &SegmentPath) -> Result<bool> {
171 self.inner.exists(path).await
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::storage::local::LocalFsBackend;
179 use chronix_security::auth::error::AuthError;
180 use chronix_security::auth::{EncryptionService, KeyProvider, SecretKey};
181
182 struct TestKeyProvider {
184 key: Vec<u8>,
185 key_id: String,
186 }
187
188 impl TestKeyProvider {
189 fn new() -> Self {
190 Self {
191 key: vec![0xAB; 32], key_id: "test-key-1".to_string(),
193 }
194 }
195 }
196
197 impl std::fmt::Debug for TestKeyProvider {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("TestKeyProvider")
200 .field("key_id", &self.key_id)
201 .finish()
202 }
203 }
204
205 impl KeyProvider for TestKeyProvider {
206 fn current_key(&self) -> std::result::Result<(SecretKey, String), AuthError> {
207 Ok((SecretKey::new(self.key.clone()), self.key_id.clone()))
208 }
209
210 fn key_by_id(&self, key_id: &str) -> std::result::Result<SecretKey, AuthError> {
211 if key_id == self.key_id {
212 Ok(SecretKey::new(self.key.clone()))
213 } else {
214 Err(AuthError::KeyNotFound(key_id.to_string()))
215 }
216 }
217
218 fn key_ids(&self) -> Vec<String> {
219 vec![self.key_id.clone()]
220 }
221 }
222
223 fn make_backend(dir: &std::path::Path) -> EncryptingBackend<LocalFsBackend> {
224 let local = LocalFsBackend::new(dir);
225 let enc = Arc::new(EncryptionService::new(Box::new(TestKeyProvider::new())));
226 EncryptingBackend::new(local, enc)
227 }
228
229 fn test_path() -> SegmentPath {
230 SegmentPath::new(
231 NamespaceId::default_namespace(),
232 ShardId(1),
233 "test_segment.csx",
234 )
235 .unwrap()
236 }
237
238 #[tokio::test]
239 async fn encrypt_decrypt_round_trip() {
240 let dir = tempfile::tempdir().unwrap();
241 let backend = make_backend(dir.path());
242 let path = test_path();
243
244 let data = b"Hello, encrypted segment!";
245 backend.put_segment(&path, data).await.unwrap();
246
247 let raw = LocalFsBackend::new(dir.path())
249 .get_segment(&path)
250 .await
251 .unwrap();
252 assert_ne!(&raw, data, "on-disk data must be encrypted");
253
254 let decrypted = backend.get_segment(&path).await.unwrap();
256 assert_eq!(decrypted, data);
257 }
258
259 #[tokio::test]
260 async fn range_read_after_encryption() {
261 let dir = tempfile::tempdir().unwrap();
262 let backend = make_backend(dir.path());
263 let path = test_path();
264
265 let data = b"0123456789ABCDEF";
266 backend.put_segment(&path, data).await.unwrap();
267
268 let range = backend.get_range(&path, 4, 8).await.unwrap();
269 assert_eq!(&range, &data[4..12]);
270 }
271
272 #[tokio::test]
273 async fn range_read_out_of_bounds() {
274 let dir = tempfile::tempdir().unwrap();
275 let backend = make_backend(dir.path());
276 let path = test_path();
277
278 backend.put_segment(&path, b"short").await.unwrap();
279
280 let err = backend.get_range(&path, 0, 100).await;
281 assert!(err.is_err());
282 }
283
284 #[tokio::test]
285 async fn delete_encrypted_segment() {
286 let dir = tempfile::tempdir().unwrap();
287 let backend = make_backend(dir.path());
288 let path = test_path();
289
290 backend.put_segment(&path, b"delete me").await.unwrap();
291 assert!(backend.exists(&path).await.unwrap());
292
293 backend.delete_segment(&path).await.unwrap();
294 assert!(!backend.exists(&path).await.unwrap());
295 }
296
297 #[tokio::test]
298 async fn list_encrypted_segments() {
299 let dir = tempfile::tempdir().unwrap();
300 let backend = make_backend(dir.path());
301 let path = test_path();
302
303 backend.put_segment(&path, b"data").await.unwrap();
304
305 let segments = backend
306 .list_segments(&NamespaceId::default_namespace(), &ShardId(1))
307 .await
308 .unwrap();
309 assert_eq!(segments.len(), 1);
310 assert_eq!(segments[0].segment_name(), "test_segment.csx");
311 }
312
313 #[tokio::test]
314 async fn tampered_data_fails_decrypt() {
315 let dir = tempfile::tempdir().unwrap();
316 let backend = make_backend(dir.path());
317 let path = test_path();
318
319 backend.put_segment(&path, b"sensitive data").await.unwrap();
320
321 let raw_backend = LocalFsBackend::new(dir.path());
323 let mut raw = raw_backend.get_segment(&path).await.unwrap();
324 if let Some(last) = raw.last_mut() {
325 *last ^= 0xFF; }
327 raw_backend.put_segment(&path, &raw).await.unwrap();
328
329 let result = backend.get_segment(&path).await;
331 assert!(result.is_err(), "tampered segment must fail decryption");
332 }
333
334 #[tokio::test]
335 async fn wrong_key_fails_decrypt() {
336 let dir = tempfile::tempdir().unwrap();
337 let backend = make_backend(dir.path());
338 let path = test_path();
339
340 backend.put_segment(&path, b"secret").await.unwrap();
341
342 struct OtherKey;
344 impl std::fmt::Debug for OtherKey {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 write!(f, "OtherKey")
347 }
348 }
349 impl KeyProvider for OtherKey {
350 fn current_key(&self) -> std::result::Result<(SecretKey, String), AuthError> {
351 Ok((SecretKey::new(vec![0xCD; 32]), "test-key-1".into()))
352 }
353 fn key_by_id(&self, _: &str) -> std::result::Result<SecretKey, AuthError> {
354 Ok(SecretKey::new(vec![0xCD; 32]))
355 }
356 fn key_ids(&self) -> Vec<String> {
357 vec!["test-key-1".into()]
358 }
359 }
360
361 let wrong_backend = EncryptingBackend::new(
362 LocalFsBackend::new(dir.path()),
363 Arc::new(EncryptionService::new(Box::new(OtherKey))),
364 );
365
366 let result = wrong_backend.get_segment(&path).await;
367 assert!(result.is_err(), "wrong key must fail decryption");
368 }
369
370 #[tokio::test]
371 async fn large_segment_round_trip() {
372 let dir = tempfile::tempdir().unwrap();
373 let backend = make_backend(dir.path());
374 let path = test_path();
375
376 let data: Vec<u8> = (0..1_048_576u32).map(|i| (i % 256) as u8).collect();
378 backend.put_segment(&path, &data).await.unwrap();
379
380 let decrypted = backend.get_segment(&path).await.unwrap();
381 assert_eq!(decrypted, data);
382 }
383
384 #[tokio::test]
387 async fn exists_nonexistent_encrypted() {
388 let dir = tempfile::tempdir().unwrap();
389 let backend = make_backend(dir.path());
390 let path = test_path();
391 assert!(!backend.exists(&path).await.unwrap());
392 }
393
394 #[tokio::test]
395 async fn empty_segment_round_trip() {
396 let dir = tempfile::tempdir().unwrap();
397 let backend = make_backend(dir.path());
398 let path = test_path();
399
400 backend.put_segment(&path, b"").await.unwrap();
401 let data = backend.get_segment(&path).await.unwrap();
402 assert!(data.is_empty());
403 }
404
405 #[tokio::test]
406 async fn range_read_full_segment() {
407 let dir = tempfile::tempdir().unwrap();
408 let backend = make_backend(dir.path());
409 let path = test_path();
410
411 let original = b"complete data";
412 backend.put_segment(&path, original).await.unwrap();
413
414 let range = backend.get_range(&path, 0, original.len()).await.unwrap();
415 assert_eq!(&range, original);
416 }
417}