diff --git a/Cargo.lock b/Cargo.lock index 3df21c8d..beebe726 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1544,7 +1544,7 @@ checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rencfs" -version = "0.3.1" +version = "0.3.2" dependencies = [ "anyhow", "argon2", @@ -1575,6 +1575,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "tracing-test", ] [[package]] @@ -2079,6 +2080,29 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-test" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a2c0ff408fe918a94c428a3f2ad04e4afd5c95bbc08fcf868eff750c15728a4" +dependencies = [ + "lazy_static", + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bc1c4f8e2e73a977812ab339d503e6feeb92700f6d07a6de4d321522d5c08" +dependencies = [ + "lazy_static", + "quote", + "syn 1.0.109", +] + [[package]] name = "trait-make" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index f97f19ca..c7cb87b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "rencfs" description = "An encrypted file system that mounts with FUSE on Linux. It can be used to create encrypted directories." -version = "0.3.1" +version = "0.3.2" edition = "2021" license = "Apache-2.0" authors = ["Radu Marias "] @@ -33,6 +33,7 @@ bytes = "1.5" tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_info"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2.3" +tracing-test = "0.2.4" ctrlc = { version = "3.1.9", features = ["termination"] } sha2 = "0.11.0-pre.3" strum = "0.26.2" diff --git a/examples/change_password.rs b/examples/change_password.rs new file mode 100644 index 00000000..d2766bf3 --- /dev/null +++ b/examples/change_password.rs @@ -0,0 +1,13 @@ +use std::str::FromStr; +use secrecy::SecretString; +use rencfs::encryptedfs::{Cipher, EncryptedFs, FsError}; + +#[tokio::main] +async fn main() { + match EncryptedFs::change_password("/tmp/rencfs_data", SecretString::from_str("old-pass").unwrap(), SecretString::from_str("new-pass").unwrap(), Cipher::ChaCha20).await { + Ok(_) => println!("Password changed successfully"), + Err(FsError::InvalidPassword) => println!("Invalid old password"), + Err(FsError::InvalidDataDirStructure) => println!("Invalid structure of data directory"), + Err(err) => println!("Error: {err}"), + } +} \ No newline at end of file diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index 2f20ac3e..ecd64854 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -1066,7 +1066,7 @@ impl EncryptedFs { Self::copy_remaining_of_file(&mut ctx, file_size, &self.cipher, &*self.key.get().await?, self.data_dir.join(CONTENTS_DIR).join(ino_str))?; } - debug!("finishing encryptwarnor"); + debug!("finishing encryptor"); ctx.encryptor.take().unwrap().finish()?; // if we are in tmp file move it to actual file let mut recreate_readers = false; @@ -1221,8 +1221,7 @@ impl EncryptedFs { let tmp_path = self.data_dir.join(CONTENTS_DIR).join(tmp_path_str); let tmp_file = OpenOptions::new().write(true).create(true).truncate(true).open(tmp_path.clone())?; - let encryptor = - crypto_util::create_encryptor(tmp_file, &self.cipher, &*self.key.get().await?); + let encryptor = crypto_util::create_encryptor(tmp_file, &self.cipher, &*self.key.get().await?); debug!("recreating encryptor"); ctx.encryptor.replace(encryptor); ctx.pos = 0; @@ -1270,15 +1269,10 @@ impl EncryptedFs { } #[instrument(skip(ctx, key))] - fn copy_remaining_of_file(ctx: &mut MutexGuard, mut end_offset: u64, cipher: &Cipher, key: &SecretVec, file: PathBuf) -> Result<(), FsError> { + fn copy_remaining_of_file(ctx: &mut MutexGuard, end_offset: u64, cipher: &Cipher, key: &SecretVec, file: PathBuf) -> Result<(), FsError> { debug!("copy_remaining_of_file from file {}", file.to_str().unwrap()); - let actual_file_size = fs::metadata(ctx.path.clone())?.len(); - debug!("copy_remaining_of_file from {} to {}, file size {} actual file size {}", ctx.pos.to_formatted_string(&Locale::en), end_offset.to_formatted_string(&Locale::en), ctx.attr.size.to_formatted_string(&Locale::en), actual_file_size.to_formatted_string(&Locale::en)); - // keep offset in file size bounds - if end_offset > ctx.attr.size { - debug!("end offset {} is bigger than file size {}", end_offset.to_formatted_string(&Locale::en), ctx.attr.size.to_formatted_string(&Locale::en)); - end_offset = ctx.attr.size; - } + let actual_file_size = fs::metadata(file.clone())?.len(); + debug!("copy_remaining_of_file from {} to {}, ctx file size {} actual file size {}", ctx.pos.to_formatted_string(&Locale::en), end_offset.to_formatted_string(&Locale::en), ctx.attr.size.to_formatted_string(&Locale::en), actual_file_size.to_formatted_string(&Locale::en)); if ctx.pos == end_offset { debug!("no need to copy, pos {} end_offset {}", ctx.pos.to_formatted_string(&Locale::en), end_offset.to_formatted_string(&Locale::en)); // no-op @@ -1289,7 +1283,7 @@ impl EncryptedFs { // move read position to the write position if ctx.pos > 0 { let mut buffer = vec![0; BUF_SIZE]; - let mut read_pos = 0u64; + let mut read_pos = 0_u64; loop { let len = min(buffer.len(), (ctx.pos - read_pos) as usize); decryptor.read_exact(&mut buffer[..len]).map_err(|err| { @@ -1307,6 +1301,7 @@ impl EncryptedFs { // copy the rest of the file let mut buffer = vec![0; BUF_SIZE]; loop { + debug!("reading from file pos {} end_offset {}", ctx.pos.to_formatted_string(&Locale::en), end_offset.to_formatted_string(&Locale::en)); let len = min(buffer.len(), (end_offset - ctx.pos) as usize); decryptor.read_exact(&mut buffer[..len]).map_err(|err| { debug!("error reading from file pos {} len {} {end_offset} file size {} actual file size {}", @@ -1970,17 +1965,3 @@ fn merge_attr(attr: &mut FileAttr, set_attr: SetFileAttr) { attr.flags = flags; } } - -fn check_password(data_dir: &PathBuf, password: &SecretString, cipher: &Cipher) -> FsResult<()> { - let salt = crypto_util::hash_secret(password); - let initial_key = crypto_util::derive_key(password, cipher, salt)?; - let enc_file = data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME); - let decryptor = crypto_util::create_decryptor(File::open(enc_file.clone())?, cipher, &initial_key); - let key_store: KeyStore = bincode::deserialize_from(decryptor).map_err(|_| FsError::InvalidPassword)?; - // check hash - if key_store.hash != crypto_util::hash(key_store.key.expose_secret()) { - return Err(FsError::InvalidPassword); - } - - Ok(()) -} diff --git a/src/encryptedfs/moved_test.rs b/src/encryptedfs/moved_test.rs index ab2d8d06..29605a9f 100644 --- a/src/encryptedfs/moved_test.rs +++ b/src/encryptedfs/moved_test.rs @@ -1,3 +1,4 @@ +use tracing_test::traced_test; use std::{fs, io}; use std::fs::OpenOptions; use std::io::Read; @@ -9,7 +10,7 @@ use std::sync::Arc; use secrecy::{ExposeSecret, SecretString}; use tokio::sync::Mutex; -use crate::encryptedfs::{Cipher, CONTENTS_DIR, DirectoryEntry, DirectoryEntryPlus, EncryptedFs, FileAttr, FileType, FsError, FsResult, ROOT_INODE}; +use crate::encryptedfs::{Cipher, CONTENTS_DIR, CreateFileAttr, DirectoryEntry, DirectoryEntryPlus, EncryptedFs, FileType, FsError, FsResult, PasswordProvider, ROOT_INODE}; const TESTS_DATA_DIR: &str = "/tmp/rencfs-test-data/"; @@ -29,7 +30,15 @@ async fn setup(setup: TestSetup) -> FsResult { fs::remove_dir_all(path)?; } fs::create_dir_all(path)?; - let fs = EncryptedFs::new(path, SecretString::from_str("pass-42").unwrap(), Cipher::ChaCha20).await?; + + struct PasswordProviderImpl {} + impl PasswordProvider for PasswordProviderImpl { + fn get_password(&self) -> Option { + Some(SecretString::from_str("password").unwrap()) + } + } + + let fs = EncryptedFs::new(path, Box::new(PasswordProviderImpl {}), Cipher::ChaCha20).await?; Ok(SetupResult { fs: Some(fs), @@ -71,30 +80,17 @@ async fn run_test(init: TestSetup, t: T) -> FsResult<()> thread_local!(static SETUP_RESULT: Arc>> = Arc::new(Mutex::new(None))); -fn create_attr(ino: u64, file_type: FileType) -> FileAttr { - FileAttr { - ino, - size: 0, - blocks: 0, - atime: std::time::SystemTime::now(), - mtime: std::time::SystemTime::now(), - ctime: std::time::SystemTime::now(), - crtime: std::time::SystemTime::now(), - kind: file_type, - perm: if file_type == FileType::Directory { 0o755 } else { 0o644 }, - nlink: if file_type == FileType::Directory { 2 } else { 1 }, +fn create_attr_from_type(kind: FileType) -> CreateFileAttr { + CreateFileAttr { + kind, + perm: 0, uid: 0, gid: 0, rdev: 0, - blksize: 0, flags: 0, } } -fn create_attr_from_type(file_type: FileType) -> FileAttr { - create_attr(0, file_type) -} - async fn read_to_string(path: PathBuf, fs: &EncryptedFs) -> String { let mut buf: Vec = vec![]; fs.create_decryptor(OpenOptions::new().read(true).write(true).open(path).unwrap()).await.unwrap().read_to_end(&mut buf).unwrap(); @@ -325,6 +321,7 @@ async fn test_truncate() -> FsResult<()> { } #[tokio::test] +#[traced_test] async fn test_copy_file_range() -> FsResult<()> { run_test(TestSetup { data_path: format!("{TESTS_DATA_DIR}test_copy_file_range") }, async { let fs = SETUP_RESULT.with(|s| Arc::clone(s)); diff --git a/src/lib.rs b/src/lib.rs index a79cdc9e..e2e28626 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,11 +20,19 @@ //! use rencfs::run_fuse; //! use rencfs::encryptedfs::Cipher; //! use secrecy::SecretString; +//! use rencfs::encryptedfs::PasswordProvider; +//! use std::str::FromStr; //! //! #[tokio::main] //! async fn main() { -//! use std::str::FromStr; -//! run_fuse("/tmp/rencfs", "/tmp/rencfs_data", SecretString::from_str("password").unwrap(), Cipher::ChaCha20, false, false, false, false).await.unwrap(); +//! struct PasswordProviderImpl {} +//! impl PasswordProvider for PasswordProviderImpl { +//! fn get_password(&self) -> Option { +//! /// dummy password, better use some secure way to get the password like with [keyring](https://crates.io/crates/keyring) crate +//! Some(SecretString::from_str("password").unwrap()) +//! } +//! } +//! run_fuse("/tmp/rencfs", "/tmp/rencfs_data", Box::new(PasswordProviderImpl{}), Cipher::ChaCha20, false, false, false, false).await.unwrap(); //! } //! ``` //! @@ -37,10 +45,10 @@ //! use fuse3::MountOptions; //! use fuse3::raw::Session; //! use secrecy::SecretString; -//! use rencfs::encryptedfs::Cipher; +//! use rencfs::encryptedfs::{Cipher, PasswordProvider}; //! use rencfs::encryptedfs_fuse3::EncryptedFsFuse3; //! -//! async fn run_fuse(mountpoint: &str, data_dir: &str, password: SecretString, cipher: Cipher, allow_root: bool, allow_other: bool, direct_io: bool, suid_support: bool) { +//! async fn run_fuse(mountpoint: &str, data_dir: &str, password_provider: Box, cipher: Cipher, allow_root: bool, allow_other: bool, direct_io: bool, suid_support: bool) -> anyhow::Result<()> { //! let uid = unsafe { libc::getuid() }; //! let gid = unsafe { libc::getgid() }; //! @@ -53,11 +61,10 @@ //! let mount_path = OsStr::new(mountpoint); //! //! Session::new(mount_options) -//! .mount_with_unprivileged(EncryptedFsFuse3::new(&data_dir, password, cipher, direct_io, suid_support).unwrap(), mount_path) -//! .await -//! .unwrap() -//! .await -//! .unwrap(); +//! .mount_with_unprivileged(EncryptedFsFuse3::new(&data_dir, password_provider, cipher, direct_io, suid_support).await.unwrap(), mount_path) +//! .await? +//! .await?; +//! Ok(()) //! } //! ``` //! Parameters: @@ -82,44 +89,49 @@ //! use std::fs; //! use std::str::FromStr; //! use secrecy::SecretString; -//! use rencfs::encryptedfs::{EncryptedFs, FileAttr, FileType}; +//! use rencfs::encryptedfs::{EncryptedFs, FileAttr, FileType, PasswordProvider, CreateFileAttr}; //! //! const ROOT_INODE: u64 = 1; -//! let data_dir = "/tmp/rencfs_data_test"; -//! let _ = fs::remove_dir_all(data_dir); -//! let password = SecretString::from_str("password").unwrap(); -//! let cipher = rencfs::encryptedfs::Cipher::ChaCha20; -//! let mut fs = EncryptedFs::new(data_dir, password, cipher ).unwrap(); -//! -//! let file1 = SecretString::from_str("file1").unwrap(); -//! let (fh, attr) = fs.create_nod(ROOT_INODE, &file1, create_attr(FileType::RegularFile), false, true).unwrap(); -//! let data = "Hello, world!"; -//! fs.write_all(attr.ino, 0, data.as_bytes(), fh).unwrap(); -//! fs.flush(fh).unwrap(); -//! fs.release(fh).unwrap(); -//! let fh = fs.open(attr.ino, true, false).unwrap(); -//! let mut buf = vec![0; data.len()]; -//! fs.read(attr.ino, 0, &mut buf, fh).unwrap(); -//! fs.release(fh).unwrap(); -//! assert_eq!(data, String::from_utf8(buf).unwrap()); -//! fs::remove_dir_all(data_dir).unwrap(); -//! -//! fn create_attr(file_type: FileType) -> FileAttr { -//! FileAttr { -//! ino: 0, -//! size: 0, -//! blocks: 0, -//! atime: std::time::SystemTime::now(), -//! mtime: std::time::SystemTime::now(), -//! ctime: std::time::SystemTime::now(), -//! crtime: std::time::SystemTime::now(), -//! kind: file_type, -//! perm: if file_type == FileType::Directory { 0o755 } else { 0o644 }, -//! nlink: if file_type == FileType::Directory { 2 } else { 1 }, +//! +//! struct PasswordProviderImpl {} +//! impl PasswordProvider for PasswordProviderImpl { +//! fn get_password(&self) -> Option { +//! /// dummy password, better use some secure way to get the password like with [keyring](https://crates.io/crates/keyring) crate +//! Some(SecretString::from_str("password").unwrap()) +//! } +//! } +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! let data_dir = "/tmp/rencfs_data_test"; +//! let _ = fs::remove_dir_all(data_dir); +//! let password = SecretString::from_str("password").unwrap(); +//! let cipher = rencfs::encryptedfs::Cipher::ChaCha20; +//! let mut fs = EncryptedFs::new(data_dir, Box::new(PasswordProviderImpl{}), cipher ).await?; +//! +//! let file1 = SecretString::from_str("file1").unwrap(); +//! let (fh, attr) = fs.create_nod(ROOT_INODE, &file1, file_attr(), false, true).await?; +//! let data = "Hello, world!"; +//! fs.write_all(attr.ino, 0, data.as_bytes(), fh).await?; +//! fs.flush(fh).await?; +//! fs.release(fh).await?; +//! let fh = fs.open(attr.ino, true, false).await?; +//! let mut buf = vec![0; data.len()]; +//! fs.read(attr.ino, 0, &mut buf, fh).await?; +//! fs.release(fh).await?; +//! assert_eq!(data, String::from_utf8(buf)?); +//! fs::remove_dir_all(data_dir)?; +//! +//! Ok(()) +//! } +//! +//! fn file_attr() -> CreateFileAttr { +//! CreateFileAttr { +//! kind: FileType::RegularFile, +//! perm: 0o644, //! uid: 0, //! gid: 0, //! rdev: 0, -//! blksize: 0, //! flags: 0, //! } //! } @@ -133,11 +145,14 @@ //! use rencfs::encryptedfs::{EncryptedFs, FsError, FsResult}; //! use rencfs::encryptedfs::Cipher; //! -//! match EncryptedFs::change_password("/tmp/rencfs_data", SecretString::from_str("old-pass").unwrap(), SecretString::from_str("new-pass").unwrap(), Cipher::ChaCha20 ) { -//! Ok(_) => println!("Password changed successfully"), -//! Err(FsError::InvalidPassword) => println!("Invalid old password"), -//! Err(FsError::InvalidDataDirStructure) => println!("Invalid structure of data directory"), -//! Err(err) => println!("Error: {err}"), +//! #[tokio::main] +//! async fn main() { +//! match EncryptedFs::change_password("/tmp/rencfs_data", SecretString::from_str("old-pass").unwrap(), SecretString::from_str("new-pass").unwrap(), Cipher::ChaCha20).await { +//! Ok(_) => println!("Password changed successfully"), +//! Err(FsError::InvalidPassword) => println!("Invalid old password"), +//! Err(FsError::InvalidDataDirStructure) => println!("Invalid structure of data directory"), +//! Err(err) => println!("Error: {err}"), +//! } //! } //! ``` //! ## Change password from CLI with `rpassword` crate @@ -152,36 +167,36 @@ //! use secrecy::{ExposeSecret, Secret, SecretString}; //! use rencfs::encryptedfs::{Cipher, EncryptedFs, FsError}; //! -//! // read password from stdin -//! print!("Enter old password: "); -//! io::stdout().flush().unwrap(); -//! let password = SecretString::new(read_password().unwrap()); -//! print!("Enter new password: "); -//! io::stdout().flush().unwrap(); -//! let new_password = SecretString::new(read_password().unwrap()); -//! print!("Confirm new password: "); -//! io::stdout().flush().unwrap(); -//! let new_password2 = SecretString::new(read_password().unwrap()); -//! if new_password.expose_secret() != new_password2.expose_secret() { -//! println!("Passwords do not match"); -//! return; -//! } -//! println!("Changing password..."); -//! match EncryptedFs::change_password("/tmp/rencfs_data", SecretString::from_str("old-pass").unwrap(), SecretString::from_str("new-pass").unwrap(), Cipher::ChaCha20 ) { -//! Ok(_) => println!("Password changed successfully"), -//! Err(FsError::InvalidPassword) => println!("Invalid old password"), -//! Err(FsError::InvalidDataDirStructure) => println!("Invalid structure of data directory"), -//! Err(err) => println!("Error: {err}"), +//! #[tokio::main] +//! async fn main() { +//! // read password from stdin +//! print!("Enter old password: "); +//! io::stdout().flush().unwrap(); +//! let password = SecretString::new(read_password().unwrap()); +//! print!("Enter new password: "); +//! io::stdout().flush().unwrap(); +//! let new_password = SecretString::new(read_password().unwrap()); +//! print!("Confirm new password: "); +//! io::stdout().flush().unwrap(); +//! let new_password2 = SecretString::new(read_password().unwrap()); +//! if new_password.expose_secret() != new_password2.expose_secret() { +//! println!("Passwords do not match"); +//! return; +//! } +//! println!("Changing password..."); +//! match EncryptedFs::change_password("/tmp/rencfs_data", SecretString::from_str("old-pass").unwrap(), SecretString::from_str("new-pass").unwrap(), Cipher::ChaCha20).await { +//! Ok(_) => println!("Password changed successfully"), +//! Err(FsError::InvalidPassword) => println!("Invalid old password"), +//! Err(FsError::InvalidDataDirStructure) => println!("Invalid structure of data directory"), +//! Err(err) => println!("Error: {err}"), +//! } +//! println!("Password changed successfully"); //! } -//! println!("Password changed successfully"); //! ``` -use tracing::{info, instrument, Level}; -use tracing_appender::non_blocking::WorkerGuard; +use tracing::{info, instrument}; use fuse3::MountOptions; use std::ffi::OsStr; use fuse3::raw::Session; -use tracing::level_filters::LevelFilter; -use tracing_subscriber::EnvFilter; use crate::encryptedfs::{Cipher, PasswordProvider}; use crate::encryptedfs_fuse3::EncryptedFsFuse3; @@ -198,29 +213,6 @@ pub fn is_debug() -> bool { return false; } -pub fn log_init(level: Level) -> WorkerGuard { - let directive = format!("rencfs={}", level.as_str()).parse().unwrap(); - let filter = EnvFilter::builder() - .with_default_directive(LevelFilter::INFO.into()) - .from_env().unwrap() - .add_directive(directive); - - let (writer, guard) = tracing_appender::non_blocking(std::io::stdout()); - let builder = tracing_subscriber::fmt() - .with_writer(writer) - .with_env_filter(filter); - // .with_max_level(level); - if is_debug() { - builder - .pretty() - .init() - } else { - builder.init(); - } - - guard -} - #[instrument(skip(password_provider))] pub async fn run_fuse(mountpoint: &str, data_dir: &str, password_provider: Box, cipher: Cipher, allow_root: bool, allow_other: bool, direct_io: bool, suid_support: bool) -> anyhow::Result<()> { let mut mount_options = &mut MountOptions::default(); diff --git a/src/main.rs b/src/main.rs index 412c694f..5b2d3404 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,9 @@ use tracing::{error, info, Level, warn}; use anyhow::Result; use secrecy::{ExposeSecret, SecretString}; use thiserror::Error; +use tracing::level_filters::LevelFilter; +use tracing_appender::non_blocking::WorkerGuard; +use tracing_subscriber::EnvFilter; use rencfs::encryptedfs::{Cipher, EncryptedFs, FsError, PasswordProvider}; use rencfs::{is_debug, log_init}; @@ -330,3 +333,26 @@ fn umount(mountpoint: &str, print_fail_status: bool) -> Result<()> { Ok(()) } + +pub fn log_init(level: Level) -> WorkerGuard { + let directive = format!("rencfs={}", level.as_str()).parse().unwrap(); + let filter = EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env().unwrap() + .add_directive(directive); + + let (writer, guard) = tracing_appender::non_blocking(io::stdout()); + let builder = tracing_subscriber::fmt() + .with_writer(writer) + .with_env_filter(filter); + // .with_max_level(level); + if is_debug() { + builder + .pretty() + .init() + } else { + builder.init(); + } + + guard +}