//! Encrypted VFS Backend - Transparent at-rest encryption using AES-256-GCM //! //! This module provides transparent file encryption at the VFS layer. //! Files are encrypted before being written to disk and decrypted on read. //! //! Format: //! - Header (32 bytes): magic(4) + version(4) + nonce(12) + original_size(8) + reserved(4) //! - Body: AES-256-GCM encrypted data //! - Tag (16 bytes): GCM authentication tag use std::path::PathBuf; use std::io::{Seek, SeekFrom}; use aes_gcm::{ Aes256Gcm, Nonce, aead::{Aead, KeyInit}, }; use sha2::{Sha256, Digest}; use super::{VfsBackend, VfsFile, VfsStat, VfsError}; use super::local_fs::LocalFs; const ENCRYPTED_MAGIC: &[u8] = b"MBE1"; // MarkBase Encrypted v1 const ENCRYPTED_VERSION: u32 = 1; const HEADER_SIZE: usize = 32; const TAG_SIZE: usize = 16; const NONCE_SIZE: usize = 12; const KEY_SIZE: usize = 32; #[derive(Debug, Clone)] pub struct EncryptedVfsConfig { pub master_key: Vec, // 32 bytes for AES-256 pub encrypt_filenames: bool, // Future feature } impl EncryptedVfsConfig { pub fn new(master_key: [u8; 32]) -> Self { Self { master_key: master_key.to_vec(), encrypt_filenames: false, } } pub fn from_password(password: &str) -> Self { let mut hasher = Sha256::new(); hasher.update(password.as_bytes()); let key = hasher.finalize(); Self { master_key: key.to_vec(), encrypt_filenames: false, } } } pub struct EncryptedVfs { inner: Box, config: EncryptedVfsConfig, } impl EncryptedVfs { pub fn new(inner: Box, config: EncryptedVfsConfig) -> Self { Self { inner, config } } pub fn wrap_local_fs(_root: PathBuf, config: EncryptedVfsConfig) -> Self { Self::new(Box::new(LocalFs::new()), config) } fn derive_key(&self, path: &PathBuf) -> Vec { let mut hasher = Sha256::new(); hasher.update(&self.config.master_key); hasher.update(path.to_string_lossy().as_bytes()); let derived = hasher.finalize(); derived[..KEY_SIZE].to_vec() } pub fn is_encrypted_file(data: &[u8]) -> bool { data.len() >= HEADER_SIZE + TAG_SIZE && &data[..4] == ENCRYPTED_MAGIC } fn encrypt_data(&self, path: &PathBuf, data: &[u8]) -> Result, VfsError> { let key_bytes = self.derive_key(path); let cipher = Aes256Gcm::new_from_slice(&key_bytes) .map_err(|e| VfsError::Io(format!("cipher init failed: {}", e)))?; let nonce_bytes: [u8; NONCE_SIZE] = rand_key(12).try_into().map_err(|_| VfsError::Io("nonce generation failed".to_string()))?; let nonce = Nonce::from_slice(&nonce_bytes); let ciphertext = cipher.encrypt(nonce, data) .map_err(|e| VfsError::Io(format!("encryption failed: {}", e)))?; let mut result = Vec::with_capacity(HEADER_SIZE + ciphertext.len() + TAG_SIZE); result.extend_from_slice(ENCRYPTED_MAGIC); result.extend_from_slice(&ENCRYPTED_VERSION.to_le_bytes()); result.extend_from_slice(&nonce_bytes); result.extend_from_slice(&(data.len() as u64).to_le_bytes()); result.extend_from_slice(&[0u8; 4]); result.extend_from_slice(&ciphertext); Ok(result) } fn decrypt_data(&self, path: &PathBuf, data: &[u8]) -> Result, VfsError> { if !Self::is_encrypted_file(data) { return Err(VfsError::Io("not an encrypted file".to_string())); } let key_bytes = self.derive_key(path); let cipher = Aes256Gcm::new_from_slice(&key_bytes) .map_err(|e| VfsError::Io(format!("cipher init failed: {}", e)))?; let nonce_bytes: [u8; NONCE_SIZE] = data[8..20].try_into().map_err(|_| VfsError::Io("invalid nonce".to_string()))?; let nonce = Nonce::from_slice(&nonce_bytes); let original_size = u64::from_le_bytes(data[20..28].try_into().map_err(|_| VfsError::Io("invalid size".to_string()))?) as usize; let ciphertext = &data[HEADER_SIZE..]; let plaintext = cipher.decrypt(nonce, ciphertext) .map_err(|e| VfsError::Io(format!("decryption failed: {}", e)))?; if plaintext.len() != original_size { return Err(VfsError::Io(format!("size mismatch: expected {}, got {}", original_size, plaintext.len()))); } Ok(plaintext) } } fn rand_key(len: usize) -> Vec { use std::time::{SystemTime, UNIX_EPOCH}; let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); let mut hasher = Sha256::new(); hasher.update(now.to_le_bytes()); hasher.update([0u8; 32]); let hash = hasher.finalize(); hash[..len].to_vec() } pub struct EncryptedFile { inner: Box, path: PathBuf, config: EncryptedVfsConfig, decrypted_data: Option>, modified: bool, position: u64, } impl EncryptedFile { fn decrypt_on_open(&mut self) -> Result<(), VfsError> { let encrypted = self.inner.read_all()?; if EncryptedVfs::is_encrypted_file(&encrypted) { let vfs = EncryptedVfs::new(Box::new(LocalFs::new()), self.config.clone()); self.decrypted_data = Some(vfs.decrypt_data(&self.path, &encrypted)?); } else { self.decrypted_data = Some(encrypted); } Ok(()) } fn encrypt_on_close(&mut self) -> Result<(), VfsError> { if !self.modified { return Ok(()); } let data = self.decrypted_data.as_ref().ok_or_else(|| VfsError::Io("no data to encrypt".to_string()))?; let vfs = EncryptedVfs::new(Box::new(LocalFs::new()), self.config.clone()); let encrypted = vfs.encrypt_data(&self.path, data)?; self.inner.seek(SeekFrom::Start(0))?; self.inner.write_all(&encrypted)?; Ok(()) } } impl VfsFile for EncryptedFile { fn read(&mut self, buf: &mut [u8]) -> Result { if self.decrypted_data.is_none() { self.decrypt_on_open()?; } let data = self.decrypted_data.as_ref().ok_or_else(|| VfsError::Io("no decrypted data".to_string()))?; let start = self.position as usize; let end = std::cmp::min(start + buf.len(), data.len()); if start >= data.len() { return Ok(0); } buf[..(end - start)].copy_from_slice(&data[start..end]); self.position += (end - start) as u64; Ok(end - start) } fn write(&mut self, buf: &[u8]) -> Result { if self.decrypted_data.is_none() { self.decrypted_data = Some(Vec::new()); } let data = self.decrypted_data.as_mut().ok_or_else(|| VfsError::Io("no decrypted data".to_string()))?; let start = self.position as usize; if start + buf.len() > data.len() { data.resize(start + buf.len(), 0); } data[start..start + buf.len()].copy_from_slice(buf); self.position += buf.len() as u64; self.modified = true; Ok(buf.len()) } fn seek(&mut self, pos: SeekFrom) -> Result { match pos { SeekFrom::Start(offset) => { self.position = offset; } SeekFrom::Current(offset) => { self.position = (self.position as i64 + offset) as u64; } SeekFrom::End(offset) => { let len = self.decrypted_data.as_ref().map(|d| d.len() as i64).unwrap_or(0); self.position = (len + offset) as u64; } } Ok(self.position) } fn flush(&mut self) -> Result<(), VfsError> { self.encrypt_on_close()?; self.inner.flush()?; Ok(()) } fn stat(&mut self) -> Result { let stat = self.inner.stat()?; Ok(VfsStat { size: self.decrypted_data.as_ref().map(|d| d.len() as u64).unwrap_or(stat.size), mode: stat.mode, uid: stat.uid, gid: stat.gid, atime: stat.atime, mtime: stat.mtime, is_dir: false, is_symlink: false, }) } fn set_len(&mut self, size: u64) -> Result<(), VfsError> { if self.decrypted_data.is_none() { self.decrypted_data = Some(Vec::new()); } let data = self.decrypted_data.as_mut().ok_or_else(|| VfsError::Io("no decrypted data".to_string()))?; data.resize(size as usize, 0); self.modified = true; Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_encrypt_decrypt_roundtrip() { let config = EncryptedVfsConfig::from_password("test_password"); let path = PathBuf::from("/test/file.txt"); let vfs = EncryptedVfs::new(Box::new(LocalFs::new()), config.clone()); let original = b"Hello, World! This is a test message."; let encrypted = vfs.encrypt_data(&path, original).unwrap(); assert!(encrypted.len() > original.len()); assert!(EncryptedVfs::is_encrypted_file(&encrypted)); let decrypted = vfs.decrypt_data(&path, &encrypted).unwrap(); assert_eq!(decrypted, original); } #[test] fn test_different_keys_produce_different_ciphertext() { let config1 = EncryptedVfsConfig::from_password("password1"); let config2 = EncryptedVfsConfig::from_password("password2"); let path = PathBuf::from("/test/file.txt"); let vfs1 = EncryptedVfs::new(Box::new(LocalFs::new()), config1); let vfs2 = EncryptedVfs::new(Box::new(LocalFs::new()), config2); let original = b"Same content"; let enc1 = vfs1.encrypt_data(&path, original).unwrap(); let enc2 = vfs2.encrypt_data(&path, original).unwrap(); assert_ne!(enc1, enc2); } #[test] fn test_key_derivation() { let config = EncryptedVfsConfig::from_password("test_password"); let vfs = EncryptedVfs::new(Box::new(LocalFs::new()), config); let key1 = vfs.derive_key(&PathBuf::from("/file1.txt")); let key2 = vfs.derive_key(&PathBuf::from("/file2.txt")); assert_ne!(key1, key2); } #[test] fn test_header_format() { let config = EncryptedVfsConfig::from_password("test"); let path = PathBuf::from("/test.txt"); let vfs = EncryptedVfs::new(Box::new(LocalFs::new()), config); let data = b"test"; let encrypted = vfs.encrypt_data(&path, data).unwrap(); assert_eq!(&encrypted[..4], ENCRYPTED_MAGIC); assert_eq!(u32::from_le_bytes(encrypted[4..8].try_into().unwrap()), ENCRYPTED_VERSION); assert_eq!(encrypted.len(), HEADER_SIZE + data.len() + TAG_SIZE); } #[test] fn test_config_from_password() { let config = EncryptedVfsConfig::from_password("my_secret_password"); assert_eq!(config.master_key.len(), KEY_SIZE); let config2 = EncryptedVfsConfig::from_password("my_secret_password"); assert_eq!(config.master_key, config2.master_key); let config3 = EncryptedVfsConfig::from_password("different"); assert_ne!(config.master_key, config3.master_key); } }