From 389124e4e28de671eabb20e267e945abec61a312 Mon Sep 17 00:00:00 2001 From: Radu Marias Date: Fri, 26 Apr 2024 21:03:46 +0300 Subject: [PATCH] use anyhow crate save encryption key with a hash so we can correctly validate if pass is ok on fs init or on chang epass added examples for changing pass check dir/key structure if data-dir is an existing non-empty dir --- Cargo.lock | 9 +- Cargo.toml | 3 +- src/encryptedfs.rs | 104 +++++--- src/encryptedfs/crypto_util.rs | 7 +- src/lib.rs | 115 +++++++-- src/main.rs | 433 ++++++++++++++++----------------- 6 files changed, 402 insertions(+), 269 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e120e562..cf72a03e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,6 +65,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" + [[package]] name = "async-notify" version = "0.3.0" @@ -811,8 +817,9 @@ dependencies = [ [[package]] name = "rencfs" -version = "0.1.35" +version = "0.1.36" dependencies = [ + "anyhow", "base64", "bincode", "bytes", diff --git a/Cargo.toml b/Cargo.toml index b1153b2c..639b1b56 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.1.35" +version = "0.1.36" edition = "2021" license = "Apache-2.0" authors = ["Radu Marias "] @@ -40,6 +40,7 @@ strum_macros = "0.26.2" rpassword = "7.3.1" cryptostream = "0.3.2" weak-table = "0.3.2" +anyhow = "1.0.82" [package.metadata.aur] depends = ["fuse3"] diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index a681f8f2..770f49ee 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex, RwLock, Weak}; use std::sync::atomic::AtomicU64; use std::time::SystemTime; -use cryptostream::{read, write}; +use cryptostream::read; use cryptostream::read::Decryptor; use cryptostream::write::Encryptor; use openssl::error::ErrorStack; @@ -126,9 +126,12 @@ pub enum FsError { #[error("invalid password")] InvalidPassword, + + #[error("invalid structure of data directory")] + InvalidDataDirStructure, } -#[derive(Debug, Clone, EnumIter, EnumString, Display)] +#[derive(Debug, Clone, EnumIter, EnumString, Display, Serialize, Deserialize, PartialEq)] pub enum Cipher { ChaCha20, Aes256Gcm, @@ -278,6 +281,12 @@ impl Iterator for DirectoryEntryPlusIterator { } } +#[derive(Debug, Serialize, Deserialize)] +struct KeyStore { + key: Vec, + hash: [u8; 32], +} + /// Encrypted FS that stores encrypted files in a dedicated directory with a specific structure based on `inode`. pub struct EncryptedFs { pub(crate) data_dir: PathBuf, @@ -1073,12 +1082,12 @@ impl EncryptedFs { })?; let mut parent_attr = self.get_inode(parent)?; - parent_attr.mtime = std::time::SystemTime::now(); - parent_attr.ctime = std::time::SystemTime::now(); + parent_attr.mtime = SystemTime::now(); + parent_attr.ctime = SystemTime::now(); let mut new_parent_attr = self.get_inode(new_parent)?; - new_parent_attr.mtime = std::time::SystemTime::now(); - new_parent_attr.ctime = std::time::SystemTime::now(); + new_parent_attr.mtime = SystemTime::now(); + new_parent_attr.ctime = SystemTime::now(); attr.ctime = std::time::SystemTime::now(); @@ -1106,7 +1115,7 @@ impl EncryptedFs { } /// Create an encryptor using internal encryption info. - pub fn create_encryptor(&self, file: File) -> write::Encryptor { + pub fn create_encryptor(&self, file: File) -> Encryptor { crypto_util::create_encryptor(file, &self.cipher, &self.key) } @@ -1133,21 +1142,24 @@ impl EncryptedFs { 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); + check_structure(&data_dir, false)?; + // decrypt key let initial_key = crypto_util::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 = crypto_util::create_decryptor(File::open(enc_file.clone())?, &cipher, &initial_key); - let mut key: Vec = vec![]; - decryptor.read_to_end(&mut key)?; - decryptor.finish(); + 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.as_slice()) { + return Err(FsError::InvalidPassword); + } // encrypt it with new key derived from new password let new_key = crypto_util::derive_key(new_password, &cipher, derive_key_hash_rounds, "salt-42"); fs::remove_file(enc_file.clone())?; let mut encryptor = crypto_util::create_encryptor(OpenOptions::new().read(true).write(true).create(true).truncate(true).open(enc_file.clone())?, &cipher, &new_key); - encryptor.write_all(&key)?; - encryptor.finish()?; + bincode::serialize_into(&mut encryptor, &key_store)?; Ok(()) } @@ -1190,11 +1202,12 @@ impl EncryptedFs { } fn check_password(&mut self) -> FsResult<()> { - match self.get_inode(ROOT_INODE) { - Ok(_) => Ok(()), - Err(FsError::SerializeError(_)) => Err(FsError::InvalidPassword), - Err(err) => Err(err), - } + self.get_inode(ROOT_INODE).map_err(|err| match err { + FsError::SerializeError(_) => FsError::InvalidPassword, + _ => err + })?; + + Ok(()) } fn create_read_handle(&mut self, ino: u64, attr: Option, handle: u64, lock: Arc>) -> FsResult { @@ -1308,7 +1321,9 @@ impl EncryptedFs { } fn ensure_structure_created(data_dir: &PathBuf) -> FsResult<()> { - if !data_dir.exists() { + if data_dir.exists() { + check_structure(data_dir, true)?; + } else { fs::create_dir_all(&data_dir)?; } @@ -1324,6 +1339,29 @@ fn ensure_structure_created(data_dir: &PathBuf) -> FsResult<()> { Ok(()) } +fn check_structure(data_dir: &PathBuf, ignore_empty: bool) -> FsResult<()> { + if !data_dir.exists() || !data_dir.is_dir() { + return Err(FsError::InvalidDataDirStructure); + } + let mut vec1 = fs::read_dir(data_dir)?.into_iter().map(|dir| dir.unwrap().file_name().to_string_lossy().to_string()).collect::>(); + if vec1.len() == 0 && ignore_empty { + return Ok(()); + } + if vec1.len() != 3 { + return Err(FsError::InvalidDataDirStructure); + } + // make sure existing structure is ok + vec1.sort(); + let mut vec2 = vec![INODES_DIR, CONTENTS_DIR, SECURITY_DIR]; + vec2.sort(); + if vec1 != vec2 || + !data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME).is_file() { + return Err(FsError::InvalidDataDirStructure); + } + + Ok(()) +} + fn merge_attr(attr: &mut FileAttr, perm: u16, atime: SystemTime, mtime: SystemTime, ctime: SystemTime, crtime: SystemTime, uid: u32, gid: u32, size: u64, nlink: u32, flags: u32) { attr.perm = perm; merge_attr_times_and_set_size(attr, atime, mtime, ctime, crtime, size); @@ -1351,8 +1389,19 @@ fn merge_attr_time_and_set_size_obj(attr: &mut FileAttr, from: &TimeAndSizeFileA fn read_or_create_key(path: PathBuf, password: &str, cipher: &Cipher, rounds: u32) -> FsResult> { let derived_key = crypto_util::derive_key(password, cipher, rounds, "salt-42"); - if !path.exists() { + if path.exists() { + // read key + + let decryptor = crypto_util::create_decryptor(File::open(path)?, cipher, &derived_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.as_slice()) { + return Err(FsError::InvalidPassword); + } + Ok(key_store.key) + } else { // first time, create a random key and encrypt it with the derived key from password + let mut key: Vec = vec![]; let key_len = match cipher { Cipher::ChaCha20 => 32, @@ -1360,16 +1409,13 @@ fn read_or_create_key(path: PathBuf, password: &str, cipher: &Cipher, rounds: u3 }; key.resize(key_len, 0); OsRng::new()?.fill_bytes(&mut key); + let key_store = KeyStore { + key: key.clone(), + hash: crypto_util::hash(key.as_slice()), + }; let mut encryptor = crypto_util::create_encryptor(OpenOptions::new().read(true).write(true).create(true).open(path.clone())?, cipher, &derived_key); - encryptor.write_all(&key)?; - encryptor.finish()?; - - return Ok(key); + bincode::serialize_into(&mut encryptor, &key_store)?; + Ok(key) } - let mut decryptor = crypto_util::create_decryptor(File::open(path)?, cipher, &derived_key); - let mut key: Vec = vec![]; - decryptor.read_to_end(&mut key)?; - decryptor.finish(); - Ok(key) } diff --git a/src/encryptedfs/crypto_util.rs b/src/encryptedfs/crypto_util.rs index ffd44630..00f5aed5 100644 --- a/src/encryptedfs/crypto_util.rs +++ b/src/encryptedfs/crypto_util.rs @@ -5,6 +5,7 @@ use rand::Rng; use std::io::{Read, Write}; use base64::decode; use std::io; +use openssl::sha::sha256; use crate::encryptedfs::Cipher; pub fn create_encryptor(mut file: File, cipher: &Cipher, key: &Vec) -> write::Encryptor { @@ -87,9 +88,13 @@ pub fn normalize_end_encrypt_file_name(name: &str, cipher: &Cipher, key: &Vec [u8; 32] { + sha256(data) +} + fn get_cipher(cipher: &Cipher) -> openssl::symm::Cipher { match cipher { Cipher::ChaCha20 => openssl::symm::Cipher::chacha20(), Cipher::Aes256Gcm => openssl::symm::Cipher::aes_256_gcm(), } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 89e2e09a..212cecde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,11 +11,23 @@ //! //! In the following example, we will see how we can use the library. //! -//! ## EncryptedFsFuse3 +//! ## Calling (run_fuse)[run_fuse] //! -//! You can use the [EncryptedFsFuse3](encryptedfs_fuse3::EncryptedFsFuse3) to mount the file system. +//! ### Example //! -//! # Example +//! ```no_run +//! use rencfs::run_fuse; +//! use rencfs::encryptedfs::Cipher; +//! +//! #[tokio::main] +//! async fn main() { +//! run_fuse("/tmp/rencfs", "/tmp/rencfs_data", "password", Cipher::ChaCha20, 1000, false, false, false, false).await.unwrap(); +//! } +//! ``` +//! +//! ## Using [EncryptedFsFuse3](EncryptedFsFuse3) +//! +//! ### Example //! //! ```no_run //! use std::ffi::OsStr; @@ -48,35 +60,35 @@ //! Parameters: //! - `data_dir`: The directory where the file system will be mounted. //! - `password`: The password to encrypt/decrypt the data. -//! - `cipher`: The encryption algorithm to use. Currently, it supports these ciphers [Cipher](encryptedfs::Cipher). +//! - `cipher`: The encryption algorithm to use. Currently, it supports these ciphers [Cipher](Cipher). //! - `derive_key_hash_rounds`: The number of rounds to derive the key hash. //! - `allow_root`: Allow root to access the file system. //! - `allow_other`: Allow other users to access the file system. //! - `direct_io`: Use direct I/O. //! - `suid_support`: Enable suid support. //! -//! ## EncryptedFs +//! ## Or directly work with [EncryptedFs](EncryptedFs) //! -//! Or directly work with [EncryptedFs](encryptedfs::EncryptedFs). You need to specify several parameters to create an encrypted file system: +//! You need to specify several parameters to create an encrypted file system: //! - `data_dir`: The directory where the file system will be mounted. //! - `password`: The password to encrypt/decrypt the data. -//! - `cipher`: The encryption algorithm to use. Currently, it supports these ciphers [Cipher](encryptedfs::Cipher). +//! - `cipher`: The encryption algorithm to use. Currently, it supports these ciphers [Cipher](Cipher). //! - `derive_key_hash_rounds`: The number of rounds to derive the key hash. //! -//! # Example +//! ### Example //! //! ``` //! use std::fs; //! use rencfs::encryptedfs::{EncryptedFs, FileAttr, FileType}; //! const ROOT_INODE: u64 = 1; //! let data_dir = "/tmp/rencfs_data_test"; -//! fs::remove_dir_all(data_dir); +//! let _ = fs::remove_dir_all(data_dir); //! let password = "password"; //! let cipher = rencfs::encryptedfs::Cipher::ChaCha20; //! let derive_key_hash_rounds = 1000; //! let mut fs = EncryptedFs::new(data_dir, password, cipher, derive_key_hash_rounds).unwrap(); //! -//! let (fh, attr) = fs.create_nod(ROOT_INODE, "file1", create_attr_from_type(FileType::RegularFile), false, true).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(); @@ -88,9 +100,9 @@ //! assert_eq!(data, String::from_utf8(buf).unwrap()); //! fs::remove_dir_all(data_dir).unwrap(); //! -//! fn create_attr(ino: u64, file_type: FileType) -> FileAttr { +//! fn create_attr(file_type: FileType) -> FileAttr { //! FileAttr { -//! ino, +//! ino: 0, //! size: 0, //! blocks: 0, //! atime: std::time::SystemTime::now(), @@ -107,13 +119,62 @@ //! flags: 0, //! } //! } +//! ``` +//! ## Change password from code +//! +//! ### Example +//! ```no_run +//! use rencfs::encryptedfs::{EncryptedFs, FsError, FsResult}; +//! use rencfs::encryptedfs::Cipher; +//! +//! match EncryptedFs::change_password("/tmp/rencfs_data", "old-pass", "new-pass", Cipher::ChaCha20, 1000) { +//! 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 //! -//! fn create_attr_from_type(file_type: FileType) -> FileAttr { -//! create_attr(0, file_type) +//! ### Example +//! +//! ```no_run +//! use std::io; +//! use std::io::Write; +//! use rpassword::read_password; +//! use rencfs::encryptedfs::{Cipher, EncryptedFs, FsError}; +//! +//! // read password from stdin +//! print!("Enter old password: "); +//! io::stdout().flush().unwrap(); +//! let password = read_password().unwrap(); +//! print!("Enter new password: "); +//! io::stdout().flush().unwrap(); +//! let new_password = read_password().unwrap(); +//! print!("Confirm new password: "); +//! io::stdout().flush().unwrap(); +//! let new_password2 = read_password().unwrap(); +//! if new_password != new_password2 { +//! println!("Passwords do not match"); +//! return; +//! } +//! println!("Changing password..."); +//! match EncryptedFs::change_password("/tmp/rencfs_data", "old-pass", "new-pass", Cipher::ChaCha20, 1000) { +//! 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"); //! ``` -use tracing::Level; +use tracing::{info, instrument, Level}; use tracing_appender::non_blocking::WorkerGuard; +use fuse3::MountOptions; +use std::ffi::OsStr; +use fuse3::raw::Session; +use encryptedfs::EncryptedFs; +use crate::encryptedfs::Cipher; +use crate::encryptedfs_fuse3::EncryptedFsFuse3; pub mod encryptedfs; pub mod encryptedfs_fuse3; @@ -141,3 +202,27 @@ pub fn log_init(level: Level) -> WorkerGuard { guard } + +#[instrument(skip(password))] +pub async fn run_fuse(mountpoint: &str, data_dir: &str, password: &str, cipher: Cipher, derive_key_hash_rounds: u32, + allow_root: bool, allow_other: bool, direct_io: bool, suid_support: bool) -> anyhow::Result<()> { + let uid = unsafe { libc::getuid() }; + let gid = unsafe { libc::getgid() }; + + let mount_options = MountOptions::default() + .uid(uid) + .gid(gid) + .read_only(false). + allow_root(allow_root). + allow_other(allow_other) + .clone(); + let mount_path = OsStr::new(mountpoint); + + info!("Checking password and mounting FUSE filesystem"); + Session::new(mount_options) + .mount_with_unprivileged(EncryptedFsFuse3::new(data_dir, password, cipher, derive_key_hash_rounds, direct_io, suid_support)?, mount_path) + .await? + .await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 64875258..81f5449b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ use std::{env, io, panic, process}; -use std::backtrace::Backtrace; use std::ffi::OsStr; use std::io::Write; use std::path::PathBuf; @@ -12,219 +11,242 @@ use fuse3::raw::prelude::*; use rpassword::read_password; use strum::IntoEnumIterator; use tokio::{fs, task}; -use tracing::{error, info, instrument, Level}; +use tracing::{error, info, instrument, Level, warn}; +use anyhow::Result; +use thiserror::Error; use rencfs::encryptedfs::{Cipher, EncryptedFs, FsError}; use rencfs::encryptedfs_fuse3::EncryptedFsFuse3; use rencfs::{is_debug, log_init}; +#[derive(Debug, Error)] +enum ExitStatusError { + #[error("exit with status {0}")] + Failure(i32), +} + #[tokio::main] -async fn main() { - let result = task::spawn_blocking(|| { +async fn main() -> Result<()> { + let res = task::spawn_blocking(|| { panic::catch_unwind(|| { - async_main() + let handle = tokio::runtime::Handle::current(); + handle.block_on(async { + async_main().await + }) }) }).await; - - match result { - Ok(Ok(_)) => {} + match res { + Ok(Ok(Ok(_))) => Ok(()), + Ok(Ok(Err(err))) => { + let err2 = err.downcast_ref::(); + if let Some(ExitStatusError::Failure(code)) = err2 { + process::exit(*code); + } + Err(err) + } Ok(Err(err)) => { - error!("panic {err:#?}"); - error!(backtrace = %Backtrace::force_capture()); + eprintln!("{err:#?}"); panic!("{err:#?}"); } Err(err) => { - error!(err = %err, "panic"); - error!(backtrace = %Backtrace::force_capture()); + eprintln!("{err}"); panic!("{err}"); } } } -fn async_main() { - let handle = tokio::runtime::Handle::current(); - handle.block_on(async { - let matches = Command::new("RencFs") - .version(crate_version!()) - .author("Radu Marias") - .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"), - ) - .arg( - Arg::new("cipher") - .long("cipher") - .short('c') - .value_name("cipher") - .default_value("ChaCha20") - .help(format!("Encryption type, possible values: {}", - Cipher::iter().fold(String::new(), |mut acc, x| { - acc.push_str(format!("{acc}{}{x}", if acc.len() != 0 { ", " } else { "" }).as_str()); - acc - }).as_str()), - ) - ) - .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"), - ) - .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 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(); +async fn async_main() -> Result<()> { + let matches = Command::new("RencFs") + .version(crate_version!()) + .author("Radu Marias") + .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"), + ) + .arg( + Arg::new("cipher") + .long("cipher") + .short('c') + .value_name("cipher") + .default_value("ChaCha20") + .help(format!("Encryption type, possible values: {}", + Cipher::iter().fold(String::new(), |mut acc, x| { + acc.push_str(format!("{acc}{}{x}", if acc.len() != 0 { ", " } else { "" }).as_str()); + acc + }).as_str()), + ) + ) + .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"), + ) + .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 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(); - let log_level = if is_debug() { - Level::TRACE - } else { - let log_level_str = matches.get_one::("log-level").unwrap().as_str(); - let log_level = Level::from_str(log_level_str); - if log_level.is_err() { - println!("Invalid log level"); - return; - } - - log_level.unwrap() - }; - let _guard = log_init(log_level); + let log_level = if is_debug() { + Level::TRACE + } else { + let log_level_str = matches.get_one::("log-level").unwrap().as_str(); + let log_level = Level::from_str(log_level_str); + if log_level.is_err() { + error!("Invalid log level"); + return Ok(()); + } - let data_dir: String = matches - .get_one::("data-dir") - .unwrap() - .to_string(); + log_level.unwrap() + }; + let _guard = log_init(log_level); - let cipher: String = matches - .get_one::("cipher") - .unwrap() - .to_string(); - let cipher = Cipher::from_str(cipher.as_str()); - if cipher.is_err() { - println!("Invalid cipher"); - return; - } - let cipher = cipher.unwrap(); + let data_dir: String = matches + .get_one::("data-dir") + .unwrap() + .to_string(); - let derive_key_hash_rounds: String = matches - .get_one::("derive-key-hash-rounds") - .unwrap() - .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"); - return; - } - let derive_key_hash_rounds = derive_key_hash_rounds.unwrap(); + let cipher: String = matches + .get_one::("cipher") + .unwrap() + .to_string(); + let cipher = Cipher::from_str(cipher.as_str()); + if cipher.is_err() { + error!("Invalid cipher"); + return Ok(()); + } + let cipher = cipher.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; - } - }); -} + let derive_key_hash_rounds: String = matches + .get_one::("derive-key-hash-rounds") + .unwrap() + .to_string(); + let derive_key_hash_rounds = u32::from_str(derive_key_hash_rounds.as_str()); + if derive_key_hash_rounds.is_err() { + error!("Invalid derive key hash rounds"); + return Ok(()); + } + let derive_key_hash_rounds = derive_key_hash_rounds.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; + if matches.get_flag("change-password") { + // change password + run_change_password(&data_dir, cipher, derive_key_hash_rounds)?; + } else { + //normal run + run_normal(matches, &data_dir, cipher, derive_key_hash_rounds).await?; } + Ok(()) +} + +fn run_change_password(data_dir: &String, cipher: Cipher, derive_key_hash_rounds: u32) -> Result<()> { // read password from stdin print!("Enter old password: "); io::stdout().flush().unwrap(); let password = read_password().unwrap(); - - { - 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(); + print!("Confirm new password: "); + io::stdout().flush().unwrap(); + let new_password2 = read_password().unwrap(); + if new_password != new_password2 { + println!("Passwords do not match"); + return Err(ExitStatusError::Failure(1).into()); + } + println!("Changing password..."); + EncryptedFs::change_password(&data_dir, &password, &new_password, cipher, derive_key_hash_rounds).map_err(|err| { + match err { + FsError::InvalidPassword => { + println!("Invalid old password"); + } + FsError::InvalidDataDirStructure => { + println!("Invalid structure of data directory"); + } + _ => { + error!(err = %err); + } + } + ExitStatusError::Failure(1) + })?; println!("Password changed successfully"); + + Ok(()) } -async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher, derive_key_hash_rounds: u32) { +async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher, derive_key_hash_rounds: u32) -> Result<()> { if !matches.contains_id("mount-point") { - println!("--mount-point is required"); - return; + error!("--mount-point is required"); + return Ok(()); } let mountpoint: String = matches.get_one::("mount-point") .unwrap() @@ -244,14 +266,14 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher, deri io::stdout().flush().unwrap(); let confirm_password = read_password().unwrap(); if password != confirm_password { - println!("Passwords do not match"); - return; + error!("Passwords do not match"); + return Ok(()); } } } if matches.get_flag("umount-on-start") { - umount(mountpoint.as_str(), false); + umount(mountpoint.as_str(), false)?; } // unmount on process kill @@ -259,57 +281,24 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher, deri let mountpoint_kill = mountpoint.clone(); set_handler(move || { info!("Received signal to exit"); - umount(mountpoint_kill.as_str(), true); + let _ = umount(mountpoint_kill.as_str(), true).map_err(|err| error!(err = %err)); 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; -} - -#[instrument(skip(password))] -async fn run_fuse(mountpoint: &str, data_dir: &str, password: &str, cipher: Cipher, derive_key_hash_rounds: u32, - allow_root: bool, allow_other: bool, direct_io: bool, suid_support: bool) { - let uid = unsafe { libc::getuid() }; - let gid = unsafe { libc::getgid() }; - - let mount_options = MountOptions::default() - .uid(uid) - .gid(gid) - .read_only(false). - allow_root(allow_root). - allow_other(allow_other) - .clone(); - let mount_path = OsStr::new(mountpoint); - - info!("Mounting FUSE filesystem"); - match EncryptedFsFuse3::new(data_dir, password, cipher, derive_key_hash_rounds, direct_io, suid_support) { - Err(FsError::InvalidPassword) => { - error!("Cannot decrypt data, maybe the password is wrong"); - println!("Cannot decrypt data, maybe the password is wrong"); - } - Err(err) => { - error!("{err}"); - } - Ok(fs) => - Session::new(mount_options) - .mount_with_unprivileged(fs, mount_path) - .await - .unwrap() - .await - .unwrap() - } + rencfs::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 } -fn umount(mountpoint: &str, print_fail_status: bool) { +fn umount(mountpoint: &str, print_fail_status: bool) -> Result<()> { let output = process::Command::new("umount") .arg(mountpoint) - .output() - .expect("Failed to execute command"); + .output()?; if print_fail_status && !output.status.success() { - println!("Cannot umount, maybe it was not mounted"); + warn!("Cannot umount, maybe it was not mounted"); } + + Ok(()) }