diff --git a/Cargo.lock b/Cargo.lock index 301f673d..2210a7f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1502,6 +1502,7 @@ dependencies = [ "sha2 0.11.0-pre.3", "strum", "strum_macros", + "tempfile", "thiserror", "tokio", "tokio-stream", diff --git a/Cargo.toml b/Cargo.toml index 6ad75a48..de70710e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ num-format = "0.4.4" ring = "0.17.8" hex = "0.4.3" rand_chacha = "0.3.1" +tempfile = "3.10.1" [package.metadata.aur] depends = ["fuse3"] diff --git a/README.md b/README.md index cc36d0fe..25a89ff9 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,13 @@ cargo install rencfs A basic example of how to use the encrypted file system is shown below ``` -rencfs --mount-point MOUNT_POINT --data-dir DATA_DIR +rencfs mount --mount-point MOUNT_POINT --data-dir DATA_DIR --tmp-dir TMP_DIR + ``` -Where `MOUNT_POINT` is the directory where the encrypted file system will be mounted and `DATA_DIR` is the directory where the encrypted data will be stored.\ +- `MOUNT_POINT` act as a client, and mount FUSE at given path +- `DATA_DIR` where to store the encrypted data +- `TMP_DIR` where keep temp data. This should be in a different directory than `DATA_DIR` as you don't want to sync this with the sync provider. But it needs to be on the same filesystem as the data-dir + It will prompt you to enter a password to encrypt/decrypt the data. ### Change Password @@ -83,9 +87,10 @@ This is done by decrypting the key with the old password and re-encrypting it wi To change the password, you can run the following command ```bash -rencfs --change-password --data-dir DATA_DIR +rencfs change-password --data-dir DATA_DIR ``` -Where `DATA_DIR` is the directory where the encrypted data is stored.\ +`DATA_DIR` where the encrypted data is stored + It will prompt you to enter the old password and then the new password. ### Encryption info diff --git a/src/crypto.rs b/src/crypto.rs index 0112029e..e2897c6c 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -19,7 +19,7 @@ use strum_macros::{Display, EnumIter, EnumString}; use thiserror::Error; use tracing::{debug, error, instrument}; -use crate::{crypto, stream_util}; +use crate::stream_util; use crate::crypto::reader::{CryptoReader, RingCryptoReader}; use crate::crypto::writer::{CryptoWriter, RingCryptoWriter}; use crate::encryptedfs::FsResult; @@ -147,7 +147,7 @@ pub fn encrypt_string_with_nonce_seed(s: &SecretString, cipher: &Cipher, key: Ar let mut writer = create_writer(cursor, cipher, key, nonce_seed); writer.write_all(s.expose_secret().as_bytes())?; writer.flush()?; - cursor = writer.finish()?.unwrap(); + cursor = writer.finish()?; let v = cursor.into_inner(); if include_nonce_seed { Ok(format!("{}.{}", base64::encode(v), nonce_seed)) @@ -163,7 +163,7 @@ pub fn encrypt_string(s: &SecretString, cipher: &Cipher, key: Arc> let mut writer = create_writer(cursor, cipher, key, nonce_seed); writer.write_all(s.expose_secret().as_bytes())?; writer.flush()?; - cursor = writer.finish()?.unwrap(); + cursor = writer.finish()?; let v = cursor.into_inner(); Ok(format!("{}.{}", base64::encode(v), nonce_seed)) } @@ -217,6 +217,9 @@ pub fn derive_key(password: &SecretString, cipher: &Cipher, salt: [u8; 32]) -> R /// Encrypt a file name with provided nonce seed. It will **INCLUDE** the nonce seed in the result so that it can be used when decrypting. pub fn encrypt_file_name(name: &SecretString, cipher: &Cipher, key: Arc>, nonce_seed: u64) -> FsResult { + // in order not to add too much to filename length we keep just 3 digits from nonce seed + let nonce_seed = nonce_seed % 1000; + if name.expose_secret() != "$." && name.expose_secret() != "$.." { let normalized_name = SecretString::new(name.expose_secret().replace("/", " ").replace("\\", " ")); let mut encrypted = encrypt_string_with_nonce_seed(&normalized_name, cipher, key, nonce_seed, true)?; diff --git a/src/crypto/writer.rs b/src/crypto/writer.rs index 7a9d6808..97701063 100644 --- a/src/crypto/writer.rs +++ b/src/crypto/writer.rs @@ -12,7 +12,7 @@ use tracing::{error, instrument}; use crate::crypto::buf_mut::BufMut; pub trait CryptoWriter: Write + Sync + Send { - fn finish(&mut self) -> io::Result>; + fn finish(&mut self) -> io::Result; } /// cryptostream @@ -112,12 +112,12 @@ impl RingCryptoWriter { } impl CryptoWriter for RingCryptoWriter { - fn finish(&mut self) -> io::Result> { + fn finish(&mut self) -> io::Result { if self.buf.available() > 0 { // encrypt and write last block, use as many bytes we have self.encrypt_and_write()?; } - Ok(Some(self.out.take().unwrap().into_inner()?)) + Ok(self.out.take().unwrap().into_inner()?) } } diff --git a/src/encryptedfs.rs b/src/encryptedfs.rs index fc276141..b0b0ba9c 100644 --- a/src/encryptedfs.rs +++ b/src/encryptedfs.rs @@ -19,6 +19,7 @@ use rand::thread_rng; use ring::aead::{AES_256_GCM, CHACHA20_POLY1305}; use secrecy::{ExposeSecret, SecretString, SecretVec}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{Mutex, MutexGuard, RwLock}; use tokio_stream::wrappers::ReadDirStream; @@ -630,6 +631,7 @@ pub trait PasswordProvider: Send + Sync + 'static { /// 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, + tmp_dir: PathBuf, write_handles: RwLock>>, read_handles: RwLock>>, current_handle: AtomicU64, @@ -649,19 +651,18 @@ pub struct EncryptedFs { } impl EncryptedFs { - pub async fn new(data_dir: &str, password_provider: Box, cipher: Cipher) -> FsResult { - let path = PathBuf::from(&data_dir); - + pub async fn new(data_dir: PathBuf, tmp_dir: PathBuf, password_provider: Box, cipher: Cipher) -> FsResult { let key_provider = KeyProvider { - path: path.join(SECURITY_DIR).join(KEY_ENC_FILENAME), + path: data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME), password_provider, cipher: cipher.clone(), }; - ensure_structure_created(&path.clone(), &key_provider).await?; + ensure_structure_created(&data_dir.clone(), &tmp_dir.clone(), &key_provider).await?; let fs = EncryptedFs { - data_dir: path.clone(), + data_dir, + tmp_dir, write_handles: RwLock::new(HashMap::new()), read_handles: RwLock::new(HashMap::new()), current_handle: AtomicU64::new(1), @@ -787,9 +788,12 @@ impl EncryptedFs { SecretString::new(name.expose_secret().to_owned()) } }; - let name = crypto::encrypt_file_name(&name, &self.cipher, self.key.get().await?, parent)?; + // in order not to add too much to filename length we keep just 3 digits from nonce seed + let nonce_seed = parent % 1000; + let name = crypto::encrypt_file_name(&name, &self.cipher, self.key.get().await?, nonce_seed)?; let file = File::open(self.data_dir.join(CONTENTS_DIR).join(parent.to_string()).join(name))?; - let (inode, _): (u64, FileType) = bincode::deserialize_from(self.create_crypto_reader(file, parent).await?)?; + // attr are encrypted with same nonce seed as filename + let (inode, _): (u64, FileType) = bincode::deserialize_from(self.create_crypto_reader(file, nonce_seed).await?)?; Ok(Some(self.get_inode(inode).await?)) } @@ -887,6 +891,7 @@ impl EncryptedFs { } }; let name = crypto::encrypt_file_name(&name, &self.cipher, self.key.get().await?, parent)?; + info!(parent = parent, name = name); Ok(self.data_dir.join(CONTENTS_DIR).join(parent.to_string()).join(name).exists()) } @@ -976,16 +981,12 @@ impl EncryptedFs { let _guard = lock.write().unwrap(); let path = self.data_dir.join(INODES_DIR).join(attr.ino.to_string()); - let file = OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(true) - .open(&path)?; - let mut writer = crypto::create_writer(file, &self.cipher, key, attr.ino); + let tmp = NamedTempFile::new_in(self.tmp_dir.clone())?; + let mut writer = crypto::create_writer(tmp, &self.cipher, key, attr.ino); bincode::serialize_into(&mut writer, &attr)?; writer.flush()?; - writer.finish()?; + let tmp = writer.finish()?; + fs::rename(tmp.into_temp_path(), path)?; Ok(()) } @@ -1604,9 +1605,7 @@ impl EncryptedFs { } /// Change the password of the filesystem used to access the encryption key. - pub async fn change_password(data_dir: &str, old_password: SecretString, new_password: SecretString, cipher: Cipher) -> FsResult<()> { - let data_dir = PathBuf::from(data_dir); - + pub async fn change_password(data_dir: PathBuf, old_password: SecretString, new_password: SecretString, cipher: Cipher) -> FsResult<()> { check_structure(&data_dir, false).await?; // decrypt key @@ -1623,13 +1622,12 @@ impl EncryptedFs { // encrypt it with new key derived from new password let salt = crypto::hash_secret_string(&new_password); let new_key = crypto::derive_key(&new_password, &cipher, salt)?; - tokio::fs::remove_file(enc_file.clone()).await?; - let mut writer = crypto::create_writer(OpenOptions::new().read(true).write(true).create(true).truncate(true).open(enc_file.clone())?, - &cipher, Arc::new(new_key), 42_u64); + let tmp = NamedTempFile::new_in(data_dir.join(SECURITY_DIR).join(KEY_ENC_FILENAME))?; + let mut writer = crypto::create_writer(tmp, &cipher, Arc::new(new_key), 42_u64); bincode::serialize_into(&mut writer, &key_store)?; writer.flush()?; - writer.finish()?; - + let tmp = writer.finish()?; + fs::rename(tmp.into_temp_path(), enc_file)?; Ok(()) } @@ -1788,19 +1786,14 @@ impl EncryptedFs { let lock = map.get_or_insert_with(file_path.to_str().unwrap().to_string(), || std::sync::RwLock::new(false)); let _guard = lock.write().unwrap(); - let file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&file_path)?; - // write inode and file type let entry = (entry.ino, entry.kind); - let mut writer = crypto::create_writer(file, &self.cipher, key, nonce_seed); + let tmp = NamedTempFile::new_in(self.tmp_dir.clone())?; + let mut writer = crypto::create_writer(tmp, &self.cipher, key, nonce_seed); bincode::serialize_into(&mut writer, &entry)?; writer.flush()?; - writer.finish()?; - + let tmp = writer.finish()?; + fs::rename(tmp.into_temp_path(), file_path)?; Ok(()) } @@ -1868,7 +1861,7 @@ impl EncryptedFs { } } -async fn ensure_structure_created(data_dir: &PathBuf, key_provider: &KeyProvider) -> FsResult<()> { +async fn ensure_structure_created(data_dir: &PathBuf, tmp_dir: &PathBuf, key_provider: &KeyProvider) -> FsResult<()> { if data_dir.exists() { check_structure(data_dir, true).await?; } else { @@ -1883,6 +1876,7 @@ async fn ensure_structure_created(data_dir: &PathBuf, key_provider: &KeyProvider tokio::fs::create_dir_all(path).await?; } } + tokio::fs::create_dir_all(tmp_dir).await?; // create encryption key key_provider.provide()?; diff --git a/src/encryptedfs/moved_test.rs b/src/encryptedfs/moved_test.rs index 1ffcf883..41e3bae5 100644 --- a/src/encryptedfs/moved_test.rs +++ b/src/encryptedfs/moved_test.rs @@ -3,7 +3,7 @@ use std::{fs, io}; use std::fs::OpenOptions; use std::io::Read; use std::ops::DerefMut; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::Arc; @@ -26,11 +26,11 @@ struct SetupResult { } async fn setup(setup: TestSetup) -> SetupResult { - let path = setup.data_path.as_str(); - if fs::metadata(path).is_ok() { - fs::remove_dir_all(path).unwrap(); + let data_dir_str = setup.data_path.as_str(); + if fs::metadata(data_dir_str).is_ok() { + fs::remove_dir_all(data_dir_str).unwrap(); } - fs::create_dir_all(path).unwrap(); + let tmp = Path::new(data_dir_str).join("tmp"); struct PasswordProviderImpl {} impl PasswordProvider for PasswordProviderImpl { @@ -39,7 +39,7 @@ async fn setup(setup: TestSetup) -> SetupResult { } } - let fs = EncryptedFs::new(path, Box::new(PasswordProviderImpl {}), Cipher::ChaCha20).await.unwrap(); + let fs = EncryptedFs::new(Path::new(data_dir_str).to_path_buf(), tmp, Box::new(PasswordProviderImpl {}), Cipher::ChaCha20).await.unwrap(); SetupResult { fs: Some(fs), diff --git a/src/encryptedfs_fuse3.rs b/src/encryptedfs_fuse3.rs index 5b8661c9..32948b52 100644 --- a/src/encryptedfs_fuse3.rs +++ b/src/encryptedfs_fuse3.rs @@ -5,6 +5,7 @@ use std::io::{BufRead, BufReader}; use std::iter::Skip; use std::num::NonZeroU32; use std::os::raw::c_int; +use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -121,18 +122,18 @@ pub struct EncryptedFsFuse3 { } impl EncryptedFsFuse3 { - pub async fn new(data_dir: &str, password_provider: Box, cipher: Cipher, + pub async fn new(data_dir: PathBuf, tmp_dir: PathBuf, password_provider: Box, cipher: Cipher, direct_io: bool, _suid_support: bool) -> FsResult { #[cfg(feature = "abi-7-26")] { Ok(Self { - fs: Arc::new(EncryptedFs::new(data_dir, password_provider, cipher).await?), + fs: Arc::new(EncryptedFs::new(data_dir, tmp_dir, password_provider, cipher).await?), direct_io, suid_support: _suid_support, }) } #[cfg(not(feature = "abi-7-26"))] { Ok(Self { - fs: Arc::new(EncryptedFs::new(data_dir, password_provider, cipher).await?), + fs: Arc::new(EncryptedFs::new(data_dir, tmp_dir, password_provider, cipher).await?), direct_io, suid_support: false, }) diff --git a/src/lib.rs b/src/lib.rs index cca065d8..434805d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,17 +22,19 @@ //! use secrecy::SecretString; //! use rencfs::encryptedfs::PasswordProvider; //! use std::str::FromStr; +//! use std::path::Path; //! //! #[tokio::main] //! async fn main() { -//! struct PasswordProviderImpl {} +//! 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(); +//! run_fuse(Path::new(&"/tmp/rencfs").to_path_buf(), Path::new(&"/tmp/rencfs_data").to_path_buf(), Path::new(&"/tmp/rencfs_tmp").to_path_buf(), +//! Box::new(PasswordProviderImpl{}), Cipher::ChaCha20, false, false, false, false).await.unwrap(); //! } //! ``` //! @@ -42,6 +44,7 @@ //! //! ```no_run //! use std::ffi::OsStr; +//! use std::path::{Path, PathBuf}; //! use fuse3::MountOptions; //! use fuse3::raw::Session; //! use secrecy::SecretString; @@ -49,7 +52,7 @@ //! use rencfs::encryptedfs::{ PasswordProvider}; //! use rencfs::encryptedfs_fuse3::EncryptedFsFuse3; //! -//! 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<()> { +//! async fn run_fuse(mountpoint: PathBuf, data_dir: PathBuf, tmp_dir: PathBuf, 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() }; //! @@ -59,10 +62,10 @@ //! .allow_root(allow_root) //! .allow_other(allow_other) //! .clone(); -//! let mount_path = OsStr::new(mountpoint); +//! let mount_path = OsStr::new(mountpoint.to_str().unwrap()); //! //! Session::new(mount_options) -//! .mount_with_unprivileged(EncryptedFsFuse3::new(&data_dir, password_provider, cipher, direct_io, suid_support).await.unwrap(), mount_path) +//! .mount_with_unprivileged(EncryptedFsFuse3::new(data_dir, tmp_dir, password_provider, cipher, direct_io, suid_support).await.unwrap(), mount_path) //! .await? //! .await?; //! Ok(()) @@ -93,6 +96,8 @@ //! use rencfs::encryptedfs::{EncryptedFs, FileAttr, FileType, PasswordProvider, CreateFileAttr}; //! use rencfs::crypto::Cipher; //! use anyhow::Result; +//! use std::path::Path; +//! use rencfs::stream_util::write_all_string_to_fs; //! //! const ROOT_INODE: u64 = 1; //! @@ -106,12 +111,13 @@ //! //! #[tokio::main] //! async fn main() -> Result<()> { -//! use rencfs::stream_util::write_all_string_to_fs; -//! let data_dir = "/tmp/rencfs_data_test"; -//! let _ = fs::remove_dir_all(data_dir); +//! let data_dir = Path::new("/tmp/rencfs_data_test").to_path_buf(); +//! let tmp_dir = Path::new("/tmp/rencfs_data_test_tmp").to_path_buf(); +//! let _ = fs::remove_dir_all(data_dir.to_str().unwrap()); +//! let _ = fs::remove_dir_all(tmp_dir.to_str().unwrap()); //! let password = SecretString::from_str("password").unwrap(); //! let cipher = Cipher::ChaCha20; -//! let mut fs = EncryptedFs::new(data_dir, Box::new(PasswordProviderImpl{}), cipher ).await?; +//! let mut fs = EncryptedFs::new(data_dir, tmp_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?; @@ -151,7 +157,8 @@ //! //! #[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 { +//! use std::path::Path; +//! match EncryptedFs::change_password(Path::new(&"/tmp/rencfs_data").to_path_buf(), 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"), @@ -173,7 +180,8 @@ //! //! #[tokio::main] //! async fn main() { -//! // read password from stdin +//! use std::path::Path; +//! // read password from stdin //! use rencfs::crypto::Cipher; //! print!("Enter old password: "); //! io::stdout().flush().unwrap(); @@ -189,7 +197,7 @@ //! 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 { +//! match EncryptedFs::change_password(Path::new(&"/tmp/rencfs_data").to_path_buf(), 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"), @@ -201,6 +209,7 @@ use tracing::{info, instrument}; use fuse3::MountOptions; use std::ffi::OsStr; +use std::path::PathBuf; use fuse3::raw::Session; use crate::crypto::Cipher; use crate::encryptedfs::PasswordProvider; @@ -222,7 +231,8 @@ pub fn is_debug() -> bool { } #[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<()> { +pub async fn run_fuse(mountpoint: PathBuf, data_dir: PathBuf, tmp_dir: PathBuf, 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(); #[cfg(target_os = "linux")] { unsafe { @@ -236,11 +246,11 @@ pub async fn run_fuse(mountpoint: &str, data_dir: &str, password_provider: Box Result<()> { let log_level = if is_debug() { Level::DEBUG } else { - let log_level_str = matches.get_one::("log-level").unwrap().as_str(); - let log_level = Level::from_str(log_level_str); + let str = match matches.subcommand() { + Some(("mount", matches)) => Some(matches.get_one::("log-level").unwrap().as_str()), + Some(("change-password", matches)) => Some(matches.get_one::("log-level").unwrap().as_str()), + _ => None, + }; + let log_level = Level::from_str(str.unwrap()); if log_level.is_err() { error!("Invalid log level"); return Err(ExitStatusError::Failure(1).into()); @@ -46,9 +50,10 @@ async fn main() -> Result<()> { }; let guard = log_init(log_level); - let mountpoint: String = matches.get_one::("mount-point") - .unwrap() - .to_string(); + let mount_point = match matches.subcommand() { + Some(("mount", matches)) => Some(matches.get_one::("mount-point").unwrap().as_str()), + _ => None, + }; let res = task::spawn_blocking(|| { panic::catch_unwind(|| { @@ -66,19 +71,25 @@ async fn main() -> Result<()> { process::exit(*code); } error!("{err}"); - umount(mountpoint.as_str(), false)?; + if let Some(mount_point) = mount_point { + umount(mount_point)?; + } drop(guard); Err(err) } Ok(Err(err)) => { error!("{err:#?}"); - umount(mountpoint.as_str(), false)?; + if let Some(mount_point) = mount_point { + umount(mount_point)?; + } drop(guard); panic!("{err:#?}"); } Err(err) => { error!("{err}"); - umount(mountpoint.as_str(), false)?; + if let Some(mount_point) = mount_point { + umount(mount_point)?; + } drop(guard); panic!("{err}"); } @@ -89,93 +100,133 @@ fn get_cli_args() -> ArgMatches { 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!("Cipher used for encryption, 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()), + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("mount") + .about("Mount the filesystem exposing decrypted content from data dir") + .arg( + Arg::new("mount-point") + .long("mount-point") + .short('m') + .required(true) + .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("tmp-dir") + .long("tmp-dir") + .short('t') + .required(true) + .value_name("TMP_DIR") + .help("Where keep temp data. This should be in a different directory than data-dir as you don't want to sync this with the sync provider. But it needs to be on the same filesystem as the data-dir."), + ) + .arg( + Arg::new("umount-on-start") + .long("umount-on-start") + .short('u') + .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('x') + .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("Use direct I/O (bypass page cache for an open file)"), ) - ) - .arg( - Arg::new("umount-on-start") - .long("umount-on-start") - .short('u') - .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('x') - .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("Use direct I/O (bypass page cache for an open file)"), - ) - .arg( - Arg::new("suid") - .long("suid") - .short('s') - .action(ArgAction::SetTrue) - .help("If it should allow setting SUID and SGID when files are created. Default is false and it will unset those flags when creating files"), - ) - .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"), - ) + .arg( + Arg::new("suid") + .long("suid") + .short('s') + .action(ArgAction::SetTrue) + .help("If it should allow setting SUID and SGID when files are created. Default is false and it will unset those flags when creating files"), + ) + .arg( + Arg::new("cipher") + .long("cipher") + .short('c') + .value_name("cipher") + .default_value("ChaCha20") + .help(format!("Cipher used for encryption, 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("log-level") + .long("log-level") + .short('l') + .value_name("log-level") + .default_value("INFO") + .help("Log level, possible values: TRACE, DEBUG, INFO, WARN, ERROR"), + ) + ).subcommand( + Command::new("change-password") + .about("Change password for the encrypted data") + .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!("Cipher used for encryption, 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("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(); matches } @@ -183,6 +234,23 @@ fn get_cli_args() -> ArgMatches { async fn async_main() -> Result<()> { let matches = get_cli_args(); + match matches.subcommand() { + Some(("change-password", matches)) => run_change_password(&matches).await?, + Some(("mount", matches)) => run_mount(&matches).await?, + None => { + error!("No subcommand provided"); + return Err(ExitStatusError::Failure(1).into()); + } + _ => { + error!("Invalid subcommand"); + return Err(ExitStatusError::Failure(1).into()); + } + } + + Ok(()) +} + +async fn run_change_password(matches: &ArgMatches) -> Result<()> { let data_dir: String = matches .get_one::("data-dir") .unwrap() @@ -199,18 +267,6 @@ async fn async_main() -> Result<()> { } let cipher = cipher.unwrap(); - if matches.get_flag("change-password") { - // change password - run_change_password(&data_dir, cipher).await?; - } else { - //normal run - run_normal(matches, &data_dir, cipher).await?; - } - - Ok(()) -} - -async fn run_change_password(data_dir: &String, cipher: Cipher) -> Result<()> { // read password from stdin print!("Enter old password: "); io::stdout().flush().unwrap(); @@ -226,7 +282,7 @@ async fn run_change_password(data_dir: &String, cipher: Cipher) -> Result<()> { return Err(ExitStatusError::Failure(1).into()); } println!("Changing password..."); - EncryptedFs::change_password(&data_dir, password, new_password, cipher).await.map_err(|err| { + EncryptedFs::change_password(Path::new(&data_dir).to_path_buf(), password, new_password, cipher).await.map_err(|err| { match err { FsError::InvalidPassword => { println!("Invalid old password"); @@ -245,15 +301,32 @@ async fn run_change_password(data_dir: &String, cipher: Cipher) -> Result<()> { Ok(()) } -async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher) -> Result<()> { - if !matches.contains_id("mount-point") { - error!("--mount-point is required"); - return Err(ExitStatusError::Failure(1).into()); - } +async fn run_mount(matches: &ArgMatches) -> Result<()> { let mountpoint: String = matches.get_one::("mount-point") .unwrap() .to_string(); + let data_dir: String = matches + .get_one::("data-dir") + .unwrap() + .to_string(); + + let tmp_dir: String = matches + .get_one::("tmp-dir") + .unwrap() + .to_string(); + + 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 Err(ExitStatusError::Failure(1).into()); + } + let cipher = cipher.unwrap(); + // when running from IDE we can't read from stdin with rpassword, get it from env var let mut password = SecretString::new(env::var("RENCFS_PASSWORD").unwrap_or_else(|_| "".to_string())); if password.expose_secret().is_empty() { @@ -262,7 +335,7 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher) -> R io::stdout().flush().unwrap(); password = SecretString::new(read_password().unwrap()); - if !PathBuf::new().join(data_dir).is_dir() || fs::read_dir(&data_dir).await.unwrap().next_entry().await.unwrap().is_none() { + if !PathBuf::new().join(data_dir.clone()).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(); @@ -280,7 +353,7 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher) -> R })?; if matches.get_flag("umount-on-start") { - umount(mountpoint.as_str(), false)?; + umount(mountpoint.as_str())?; } let auto_unmount = matches.get_flag("auto_unmount"); @@ -293,7 +366,7 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher) -> R if auto_unmount { info!("Unmounting {}", mountpoint_kill); } - umount(mountpoint_kill.as_str(), true).map_err(|err| { + umount(mountpoint_kill.as_str()).map_err(|err| { error!(err = %err); status.replace(ExitStatusError::Failure(1)); }).ok(); @@ -318,17 +391,17 @@ async fn run_normal(matches: ArgMatches, data_dir: &String, cipher: Cipher) -> R } } - rencfs::run_fuse(&mountpoint, &data_dir, Box::new(PasswordProviderImpl {}), cipher, + rencfs::run_fuse(Path::new(&mountpoint).to_path_buf(), Path::new(&data_dir).to_path_buf(), Path::new(&tmp_dir).to_path_buf(), Box::new(PasswordProviderImpl {}), cipher, 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) -> Result<()> { +fn umount(mountpoint: &str) -> Result<()> { let output = process::Command::new("umount") .arg(mountpoint) .output()?; - if print_fail_status && !output.status.success() { + if !output.status.success() { warn!("Cannot umount, maybe it was not mounted"); }