diff --git a/Cargo.lock b/Cargo.lock index a175ca4..bd0d6c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,7 +293,7 @@ checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encryptedfs" -version = "0.1.23" +version = "0.1.24" dependencies = [ "base64", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 68bac19..c271c5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "encryptedfs" description = "An encrypted file system that mounts with FUSE on Linux. It can be used to create encrypted directories." -version = "0.1.23" +version = "0.1.24" edition = "2021" license = "Apache-2.0" authors = ["Radu Marias "] diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index ba0500d..e3b4b15 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -119,6 +119,9 @@ pub enum FsError { #[error("encryption error: {0}")] Encryption(#[from] ErrorStack), + + #[error("invalid password")] + InvalidPassword, } #[derive(Debug, Clone, EnumIter, EnumString, Display)] @@ -297,7 +300,7 @@ impl EncryptedFs { opened_files_for_write: HashMap::new(), }; let _ = fs.ensure_root_exists(); - fs.check_password(); + fs.check_password()?; Ok(fs) } @@ -993,13 +996,13 @@ impl EncryptedFs { encryptedfs::normalize_end_encrypt_file_name(name, &self.cipher, &self.key) } /// Change the password of the filesystem used to access the encryption key. - pub fn change_password(data_dir: &str, old_password: &str, new_password: &str, cipher: &Cipher, derive_key_hash_rounds: u32) -> FsResult<()> { + pub fn change_password(data_dir: &str, old_password: &str, new_password: &str, cipher: Cipher, derive_key_hash_rounds: u32) -> FsResult<()> { let data_dir = PathBuf::from(data_dir); // decrypt key let initial_key = encryptedfs::derive_key(old_password, &cipher, derive_key_hash_rounds, "salt-42"); let enc_file = data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME); - let mut decryptor = encryptedfs::create_decryptor(File::open(enc_file.clone())?, cipher, &initial_key); + let mut decryptor = encryptedfs::create_decryptor(File::open(enc_file.clone())?, &cipher, &initial_key); let mut key: Vec = vec![]; decryptor.read_to_end(&mut key)?; decryptor.finish(); @@ -1008,7 +1011,7 @@ impl EncryptedFs { let new_key = encryptedfs::derive_key(new_password, &cipher, derive_key_hash_rounds, "salt-42"); fs::remove_file(enc_file.clone())?; let mut encryptor = encryptedfs::create_encryptor(OpenOptions::new().read(true).write(true).create(true).truncate(true).open(enc_file.clone())?, - cipher, &new_key); + &cipher, &new_key); encryptor.write_all(&key)?; encryptor.finish()?; @@ -1027,14 +1030,11 @@ impl EncryptedFs { } } - fn check_password(&mut self) { + fn check_password(&mut self) -> FsResult<()> { match self.get_inode(ROOT_INODE) { - Err(FsError::SerializeError(_)) => { - println!("Cannot decrypt data, maybe password is wrong"); - process::exit(2); - } - Err(err) => { panic!("Error while checking password: {:?}", err); } - _ => {} + Ok(_) => Ok(()), + Err(FsError::SerializeError(_)) => return Err(FsError::InvalidPassword), + Err(err) => return Err(err), } } diff --git a/src/encryptedfs_fuse3.rs b/src/encryptedfs_fuse3.rs index 231a482..0ea080a 100644 --- a/src/encryptedfs_fuse3.rs +++ b/src/encryptedfs_fuse3.rs @@ -124,14 +124,14 @@ impl EncryptedFsFuse3 { direct_io: bool, _suid_support: bool) -> FsResult { #[cfg(feature = "abi-7-26")] { Ok(Self { - fs: const_reentrant_mutex(RefCell::new(EncryptedFs::new(data_dir, password, cipher, derive_key_hash_rounds).unwrap())), + fs: const_reentrant_mutex(RefCell::new(EncryptedFs::new(data_dir, password, cipher, derive_key_hash_rounds)?)), direct_io, suid_support: _suid_support, }) } #[cfg(not(feature = "abi-7-26"))] { Ok(Self { - fs: const_reentrant_mutex(RefCell::new(EncryptedFs::new(data_dir, password, cipher, derive_key_hash_rounds).unwrap())), + fs: const_reentrant_mutex(RefCell::new(EncryptedFs::new(data_dir, password, cipher, derive_key_hash_rounds)?)), direct_io, suid_support: false, }) diff --git a/src/main.rs b/src/main.rs index b886db6..3463e2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,18 +2,20 @@ use std::{env, io, panic, process}; use std::backtrace::Backtrace; use std::ffi::OsStr; use std::io::Write; +use std::path::PathBuf; use std::str::FromStr; -use clap::{Arg, ArgAction, Command, crate_version}; +use clap::{Arg, ArgAction, ArgMatches, Command, crate_version}; use ctrlc::set_handler; use fuse3::MountOptions; use fuse3::raw::prelude::*; +use libc::system; use rpassword::read_password; use strum::IntoEnumIterator; -use tokio::task; +use tokio::{fs, task}; use tracing::{error, info, Level}; -use encryptedfs::encryptedfs::{Cipher, EncryptedFs}; +use encryptedfs::encryptedfs::{Cipher, EncryptedFs, FsError, FsResult}; use encryptedfs::encryptedfs_fuse3::EncryptedFsFuse3; #[tokio::main] @@ -25,7 +27,7 @@ async fn main() { }).await; match result { - Ok(Ok(_)) => println!("Program terminated successfully"), + Ok(Ok(_)) => {} Ok(Err(err)) => { error!("Error panic: {:#?}", err); error!("Backtrace: {}", Backtrace::force_capture()); @@ -48,12 +50,14 @@ fn async_main() { .arg( Arg::new("mount-point") .long("mount-point") + .short('m') .value_name("MOUNT_POINT") .help("Act as a client, and mount FUSE at given path"), ) .arg( Arg::new("data-dir") .long("data-dir") + .short('d') .required(true) .value_name("DATA_DIR") .help("Where to store the encrypted data"), @@ -61,6 +65,7 @@ fn async_main() { .arg( Arg::new("cipher") .long("cipher") + .short('c') .value_name("cipher") .default_value("ChaCha20") .help(format!("Encryption type, possible values: {}", @@ -73,31 +78,44 @@ fn async_main() { .arg( Arg::new("derive-key-hash-rounds") .long("derive-key-hash-rounds") + .short('k') .value_name("derive-key-hash-rounds") .default_value("600000") .help("How many times to hash the password to derive the key"), ) + .arg( + Arg::new("umount-on-start") + .long("umount-on-start") + .short('x') + .action(ArgAction::SetTrue) + .help("If we should try to umount the mountpoint before starting the FUSE server. This can be useful when the previous run crashed or was forced kll and the mountpoint is still mounted."), + ) .arg( Arg::new("auto_unmount") .long("auto_unmount") + .short('u') + .default_value("true") .action(ArgAction::SetTrue) .help("Automatically unmount on process exit"), ) .arg( Arg::new("allow-root") .long("allow-root") + .short('r') .action(ArgAction::SetTrue) .help("Allow root user to access filesystem"), ) .arg( Arg::new("allow-other") .long("allow-other") + .short('o') .action(ArgAction::SetTrue) .help("Allow other user to access filesystem"), ) .arg( Arg::new("direct-io") .long("direct-io") + .short('i') .action(ArgAction::SetTrue) .requires("mount-point") .help("Mount FUSE with direct IO"), @@ -105,31 +123,33 @@ fn async_main() { .arg( Arg::new("suid") .long("suid") + .short('s') .action(ArgAction::SetTrue) .help("Enable setuid support when run as root"), ) .arg( Arg::new("change-password") .long("change-password") + .short('p') .action(ArgAction::SetTrue) - .help("Change password for the encrypted data. Old password and new password with be read from stdin"), - ) - .arg( - Arg::new("umount-on-start") - .long("umount-on-start") - .action(ArgAction::SetTrue) - .help("If we should try to umount the mountpoint before starting the FUSE server. This can be useful when the previous run crashed or was forced kll and the mountpoint is still mounted."), + .help("Change password for the encrypted data. Old password and new password will be read from the stdin"), ) .arg( Arg::new("log-level") .long("log-level") + .short('l') .value_name("log-level") .default_value("INFO") .help("Log level, possible values: TRACE, DEBUG, INFO, WARN, ERROR"), ) .get_matches(); - log_init(matches.get_one::("log-level").unwrap().as_str()); + let log_level = matches.get_one::("log-level").unwrap().as_str(); + if Level::from_str(log_level).is_err() { + println!("Invalid log level"); + return; + } + log_init(log_level); let data_dir: String = matches .get_one::("data-dir") @@ -142,7 +162,7 @@ fn async_main() { .to_string(); let cipher = Cipher::from_str(cipher.as_str()); if cipher.is_err() { - println!("Invalid encryption type"); + println!("Invalid cipher"); return; } let cipher = cipher.unwrap(); @@ -153,63 +173,93 @@ fn async_main() { .to_string(); let derive_key_hash_rounds = u32::from_str(derive_key_hash_rounds.as_str()); if derive_key_hash_rounds.is_err() { - println!("Invalid derive-key-hash-rounds"); + println!("Invalid derive key hash rounds"); return; } let derive_key_hash_rounds = derive_key_hash_rounds.unwrap(); if matches.get_flag("change-password") { // change password + run_change_password(&data_dir, cipher, derive_key_hash_rounds).await; + } else { + //normal run + run_normal(matches, &data_dir, cipher, derive_key_hash_rounds).await; + } + }); +} - // read password from stdin - print!("Enter old password: "); - io::stdout().flush().unwrap(); - let password = read_password().unwrap(); +async fn run_change_password(data_dir: &String, cipher: Cipher, derive_key_hash_rounds: u32) { + if !PathBuf::new().join(data_dir).is_dir() || fs::read_dir(&data_dir).await.unwrap().next_entry().await.unwrap().is_none() { + println!("Data dir is not set up yet, nothing to change password for"); + return; + } - print!("Enter new password: "); - io::stdout().flush().unwrap(); - let new_password = read_password().unwrap(); - EncryptedFs::change_password(&data_dir, &password, &new_password, &cipher, derive_key_hash_rounds).unwrap(); - println!("Password changed successfully"); + // read password from stdin + print!("Enter old password: "); + io::stdout().flush().unwrap(); + let password = read_password().unwrap(); - return; - } else { - //normal run + { + let fs = EncryptedFs::new(&data_dir, &password, cipher.clone(), derive_key_hash_rounds); + if let Err(FsError::InvalidPassword) = fs { + println!("Cannot decrypt data, maybe the password is wrong"); + process::exit(1); + } + } + + print!("Enter new password: "); + io::stdout().flush().unwrap(); + let new_password = read_password().unwrap(); + EncryptedFs::change_password(&data_dir, &password, &new_password, cipher, derive_key_hash_rounds).unwrap(); + println!("Password changed successfully"); +} + +async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher, derive_key_hash_rounds: u32) { + if !matches.contains_id("mount-point") { + println!("--mount-point is required"); + return; + } + let mountpoint: String = matches.get_one::("mount-point") + .unwrap() + .to_string(); + + // when running from IDE we can't read from stdin with rpassword, get it from env var + let mut password = env::var("ENCRYPTEDFS_PASSWORD").unwrap_or_else(|_| "".to_string()); + if password.is_empty() { + // read password from stdin + print!("Enter password: "); + io::stdout().flush().unwrap(); + password = read_password().unwrap(); - if !matches.contains_id("mount-point") { - println!("--mount-point is required"); + if !PathBuf::new().join(data_dir).is_dir() || fs::read_dir(&data_dir).await.unwrap().next_entry().await.unwrap().is_none() { + // first run, ask to confirm password + print!("Confirm password: "); + io::stdout().flush().unwrap(); + let confirm_password = read_password().unwrap(); + if password != confirm_password { + println!("Passwords do not match"); return; } - let mountpoint: String = matches.get_one::("mount-point") - .unwrap() - .to_string(); - - // when running from IDE we can't read from stdin with rpassword, get it from env var - let mut password = env::var("ENCRYPTEDFS_PASSWORD").unwrap_or_else(|_| "".to_string()); - if password.is_empty() { - // read password from stdin - print!("Enter password: "); - io::stdout().flush().unwrap(); - password = read_password().unwrap(); - } + } + } - if matches.get_flag("umount-on-start") { - umount(mountpoint.as_str(), false); - } + if matches.get_flag("umount-on-start") { + umount(mountpoint.as_str(), false); + } - // unmount on process kill - let mountpoint_kill = mountpoint.clone(); - set_handler(move || { - info!("Received signal to exit"); - umount(mountpoint_kill.as_str(), true); - process::exit(0); - }).unwrap(); - - run_fuse(mountpoint, &data_dir, &password, cipher, derive_key_hash_rounds, - matches.get_flag("allow-root"), matches.get_flag("allow-other"), - matches.get_flag("direct-io"), matches.get_flag("suid")).await; - } - }); + // unmount on process kill + if matches.get_flag("auto_unmount") { + let mountpoint_kill = mountpoint.clone(); + set_handler(move || { + info!("Received signal to exit"); + umount(mountpoint_kill.as_str(), true); + process::exit(0); + }).unwrap(); + } + + run_fuse(mountpoint, &data_dir, &password, cipher, derive_key_hash_rounds, + matches.get_flag("allow-root"), matches.get_flag("allow-other"), + matches.get_flag("direct-io"), matches.get_flag("suid")).await; } async fn run_fuse(mountpoint: String, data_dir: &str, password: &str, cipher: Cipher, derive_key_hash_rounds: u32, @@ -226,12 +276,23 @@ async fn run_fuse(mountpoint: String, data_dir: &str, password: &str, cipher: Ci .clone(); let mount_path = OsStr::new(mountpoint.as_str()); - Session::new(mount_options) - .mount_with_unprivileged(EncryptedFsFuse3::new(&data_dir, &password, cipher, derive_key_hash_rounds, direct_io, suid_support).unwrap(), mount_path) - .await - .unwrap() - .await - .unwrap(); + match EncryptedFsFuse3::new(&data_dir, &password, cipher, derive_key_hash_rounds, direct_io, suid_support) { + Err(FsError::InvalidPassword) => { + println!("Cannot decrypt data, maybe the password is wrong"); + process::exit(1); + } + Err(err) => { + error!("{}", err); + process::exit(1); + } + Ok(fs) => + Session::new(mount_options) + .mount_with_unprivileged(fs, mount_path) + .await + .unwrap() + .await + .unwrap() + } } fn umount(mountpoint: &str, print_fail_status: bool) {