feat: add crypt4ghfs-rust

This commit is contained in:
2023-01-08 11:48:40 +08:00
parent c8837b21f6
commit 8f6805f3e3
30 changed files with 2959 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
use std::path::PathBuf;
/// Fuse layer exposing Crypt4GH-encrypted files, as if they were decrypted.
#[derive(clap::Parser)]
#[clap(about, version, author)]
pub struct Args {
/// Display debug information
#[clap(short, long)]
pub verbose: bool,
/// Path to the config file
#[clap(short, long)]
pub conf: PathBuf,
/// Path to the mountpoint
#[clap()]
pub mountpoint: PathBuf,
}

View File

@@ -0,0 +1,338 @@
use std::collections::HashSet;
use std::convert::{TryFrom, TryInto};
use std::fs::File;
use std::io::Read;
use std::path::Path;
use crypt4gh::error::Crypt4GHError;
use crypt4gh::Keys;
use fuser::MountOption;
use itertools::Itertools;
use rpassword::prompt_password;
use serde::Deserialize;
use crate::error::Crypt4GHFSError;
const PASSPHRASE: &str = "C4GH_PASSPHRASE";
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum LogLevel {
#[serde(alias = "CRITICAL")]
Critical,
Warn,
Info,
Debug,
#[serde(alias = "NOTSET")]
Trace,
}
#[derive(Deserialize, Debug, Copy, Clone)]
#[serde(rename_all = "snake_case")]
pub enum Facility {
Kern,
User,
Mail,
Daemon,
Auth,
Syslog,
Lpr,
News,
Uucp,
Cron,
Authpriv,
Ftp,
Local0,
Local1,
Local2,
Local3,
Local4,
Local5,
Local6,
Local7,
}
#[derive(Deserialize, Debug)]
pub struct Default {
extensions: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct Fuse {
options: Option<Vec<String>>,
rootdir: String,
cache_directories: Option<bool>,
}
#[derive(Deserialize, Debug)]
pub struct Crypt4GH {
#[serde(rename = "seckey")]
seckey_path: Option<String>,
recipients: Option<Vec<String>>,
include_myself_as_recipient: Option<bool>,
}
#[derive(Deserialize, Debug)]
pub struct LoggerConfig {
pub log_level: LogLevel,
pub use_syslog: bool,
pub log_facility: Option<Facility>,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "UPPERCASE")]
pub struct Config {
default: Default,
pub logger: LoggerConfig,
fuse: Fuse,
crypt4gh: Crypt4GH,
}
impl Config {
pub fn new_with_defaults(rootdir: String, seckey_path: Option<String>) -> Self {
Self {
default: Default { extensions: vec![] },
fuse: Fuse {
rootdir,
options: Some(vec!["ro".into(), "default_permissions".into(), "auto_unmount".into()]),
cache_directories: Some(true),
},
crypt4gh: Crypt4GH {
seckey_path,
recipients: Some(vec![]),
include_myself_as_recipient: Some(true),
},
logger: LoggerConfig {
log_level: LogLevel::Info,
use_syslog: false,
log_facility: None,
},
}
}
#[must_use]
pub fn with_extensions(mut self, extensions: Vec<String>) -> Self {
self.default.extensions = extensions;
self
}
#[must_use]
pub const fn with_log_level(mut self, log_level: LogLevel) -> Self {
self.logger.log_level = log_level;
self
}
pub fn get_options(&self) -> Vec<MountOption> {
self.fuse.options.clone().map_or_else(
|| vec![MountOption::RW, MountOption::DefaultPermissions],
|options| {
options
.iter()
.map(String::as_str)
.map(str_to_mount_option)
.inspect(|option| {
log::info!("+ fuse option: {:?}", option);
})
.collect()
},
)
}
pub const fn get_cache(&self) -> bool {
if let Some(cache_directories) = self.fuse.cache_directories {
return cache_directories;
}
true
}
pub fn get_extensions(&self) -> Vec<String> {
self.default.extensions.clone()
}
pub fn get_secret_key(&self) -> Result<Option<Vec<u8>>, Crypt4GHFSError> {
match &self.crypt4gh.seckey_path {
Some(seckey_path_str) => {
let seckey_path = Path::new(&seckey_path_str);
log::info!("Loading secret key from {}", seckey_path.display());
if !seckey_path.is_file() {
return Err(Crypt4GHFSError::SecretNotFound(seckey_path.into()));
}
let callback: Box<dyn Fn() -> Result<String, Crypt4GHError>> = match std::env::var(PASSPHRASE) {
Ok(_) => {
log::warn!("Warning: Using a passphrase in an environment variable is insecure");
Box::new(|| std::env::var(PASSPHRASE).map_err(|e| Crypt4GHError::NoPassphrase(e.into())))
},
Err(_) => Box::new(|| {
prompt_password(format!("Passphrase for {}: ", seckey_path.display()))
.map_err(|e| Crypt4GHError::NoPassphrase(e.into()))
}),
};
let key = crypt4gh::keys::get_private_key(seckey_path, callback)
.map_err(|e| Crypt4GHFSError::SecretKeyError(e.to_string()))?;
Ok(Some(key))
},
None => Ok(None),
}
}
pub fn get_recipients(&self, seckey: &[u8]) -> HashSet<Keys> {
let recipient_paths = &self.crypt4gh.recipients.clone().unwrap_or_default();
let mut recipient_pubkeys: HashSet<_> = recipient_paths
.iter()
.map(Path::new)
.filter(|path| path.exists())
.filter_map(|path| {
log::debug!("Recipient pubkey path: {}", path.display());
crypt4gh::keys::get_public_key(path).ok()
})
.unique()
.map(|key| Keys {
method: 0,
privkey: seckey.to_vec(),
recipient_pubkey: key,
})
.collect();
if self.crypt4gh.include_myself_as_recipient.unwrap_or(true) {
let k = crypt4gh::keys::get_public_key_from_private_key(seckey)
.expect("Unable to extract public key from seckey");
recipient_pubkeys.insert(Keys {
method: 0,
privkey: seckey.to_vec(),
recipient_pubkey: k,
});
}
recipient_pubkeys
}
pub const fn get_log_level(&self) -> LogLevel {
self.logger.log_level
}
pub fn get_rootdir(&self) -> String {
self.fuse.rootdir.to_string()
}
pub fn from_file(mut config_file: File) -> Result<Self, Crypt4GHFSError> {
let mut config_string = String::new();
config_file
.read_to_string(&mut config_string)
.map_err(|e| Crypt4GHFSError::BadConfig(e.to_string()))?;
let config_toml = toml::from_str(config_string.as_str()).map_err(|e| Crypt4GHFSError::BadConfig(e.to_string()));
config_toml
}
pub fn get_facility(&self) -> syslog::Facility {
match self.logger.log_facility.unwrap_or(Facility::User) {
Facility::Kern => syslog::Facility::LOG_KERN,
Facility::User => syslog::Facility::LOG_USER,
Facility::Mail => syslog::Facility::LOG_MAIL,
Facility::Daemon => syslog::Facility::LOG_DAEMON,
Facility::Auth => syslog::Facility::LOG_AUTH,
Facility::Syslog => syslog::Facility::LOG_SYSLOG,
Facility::Lpr => syslog::Facility::LOG_LPR,
Facility::News => syslog::Facility::LOG_NEWS,
Facility::Uucp => syslog::Facility::LOG_UUCP,
Facility::Cron => syslog::Facility::LOG_CRON,
Facility::Authpriv => syslog::Facility::LOG_AUTHPRIV,
Facility::Ftp => syslog::Facility::LOG_FTP,
Facility::Local0 => syslog::Facility::LOG_LOCAL0,
Facility::Local1 => syslog::Facility::LOG_LOCAL1,
Facility::Local2 => syslog::Facility::LOG_LOCAL2,
Facility::Local3 => syslog::Facility::LOG_LOCAL3,
Facility::Local4 => syslog::Facility::LOG_LOCAL4,
Facility::Local5 => syslog::Facility::LOG_LOCAL5,
Facility::Local6 => syslog::Facility::LOG_LOCAL6,
Facility::Local7 => syslog::Facility::LOG_LOCAL7,
}
}
pub fn setup_logger(&self) -> Result<(), Crypt4GHFSError> {
let log_level: LogLevel = if let Ok(log_level_str) = std::env::var("RUST_LOG") {
log_level_str
.as_str()
.try_into()
.expect("Unable to parse RUST_LOG environment variable")
}
else {
let log_level = self.get_log_level();
let log_level_str = match log_level {
LogLevel::Critical => "error",
LogLevel::Warn => "warn",
LogLevel::Info => "info",
LogLevel::Debug => "debug",
LogLevel::Trace => "trace",
};
std::env::set_var("RUST_LOG", log_level_str);
log_level
};
// Choose logger
if self.logger.use_syslog {
syslog::init(self.get_facility(), log_level.into(), None)?;
}
else {
let _ = pretty_env_logger::try_init(); // Ignore error
}
Ok(())
}
}
impl TryFrom<&str> for LogLevel {
type Error = Crypt4GHFSError;
fn try_from(level: &str) -> Result<Self, Self::Error> {
match level {
"error" => Ok(Self::Critical),
"warn" => Ok(Self::Warn),
"info" => Ok(Self::Info),
"debug" => Ok(Self::Debug),
"trace" => Ok(Self::Trace),
_ => Err(Crypt4GHFSError::BadConfig("Wrong log level".into())),
}
}
}
impl From<LogLevel> for log::LevelFilter {
fn from(val: LogLevel) -> Self {
match val {
LogLevel::Critical => Self::Error,
LogLevel::Warn => Self::Warn,
LogLevel::Info => Self::Info,
LogLevel::Debug => Self::Debug,
LogLevel::Trace => Self::Trace,
}
}
}
fn str_to_mount_option(s: &str) -> MountOption {
match s {
"auto_unmount" => MountOption::AutoUnmount,
"allow_other" => MountOption::AllowOther,
"allow_root" => MountOption::AllowRoot,
"default_permissions" => MountOption::DefaultPermissions,
"dev" => MountOption::Dev,
"nodev" => MountOption::NoDev,
"suid" => MountOption::Suid,
"nosuid" => MountOption::NoSuid,
"ro" => MountOption::RO,
"rw" => MountOption::RW,
"exec" => MountOption::Exec,
"noexec" => MountOption::NoExec,
"atime" => MountOption::Atime,
"noatime" => MountOption::NoAtime,
"dirsync" => MountOption::DirSync,
"sync" => MountOption::Sync,
"async" => MountOption::Async,
x if x.starts_with("fsname=") => MountOption::FSName(x[7..].into()),
x if x.starts_with("subtype=") => MountOption::Subtype(x[8..].into()),
x => MountOption::CUSTOM(x.into()),
}
}

View File

@@ -0,0 +1,74 @@
use std::collections::HashMap;
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::path::Path;
use crate::egafile::EgaFile;
use crate::error::{Crypt4GHFSError, Result};
use crate::utils;
pub struct Directory {
pub opened_files: HashMap<u64, Box<File>>,
pub path: Box<Path>,
}
impl EgaFile for Directory {
fn fh(&self) -> Vec<u64> {
self.opened_files.iter().map(|(&fh, _)| fh).collect()
}
fn path(&self) -> Box<Path> {
self.path.clone()
}
fn open(&mut self, flags: i32) -> Result<i32> {
let path = self.path();
let directory = utils::open(&path, flags)?;
let fh = directory.as_raw_fd();
self.opened_files.insert(fh as u64, Box::new(directory));
Ok(fh)
}
fn read(&mut self, _fh: u64, _offset: i64, _size: u32) -> Result<Vec<u8>> {
unimplemented!()
}
fn flush(&mut self, _fh: u64) -> Result<()> {
unimplemented!()
}
fn write(&mut self, _fh: u64, _data: &[u8]) -> Result<usize> {
unimplemented!()
}
fn truncate(&mut self, _fh: Option<u64>, _size: u64) -> Result<()> {
unimplemented!()
}
fn close(&mut self, fh: u64) -> Result<()> {
let f = self.opened_files.get(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
assert_eq!(f.as_raw_fd(), fh as i32);
self.opened_files.remove(&fh);
Ok(())
}
fn rename(&mut self, new_path: &Path) {
self.path = new_path.into();
}
fn attrs(&self, uid: u32, gid: u32) -> Result<fuser::FileAttr> {
let stat = utils::lstat(&self.path)?;
Ok(utils::stat_to_fileatr(stat, uid, gid))
}
}
impl Directory {
pub fn new(file: Option<Box<File>>, path: Box<Path>) -> Self {
// Build open files
let mut opened_files = HashMap::new();
if let Some(f) = file {
opened_files.insert(f.as_raw_fd() as u64, f);
}
Self { opened_files, path }
}
}

View File

@@ -0,0 +1,21 @@
use std::path::Path;
use fuser::FileAttr;
use crate::error::Result;
pub trait EgaFile {
// Attributes
fn fh(&self) -> Vec<u64>;
fn path(&self) -> Box<Path>;
// Filesystem
fn open(&mut self, flags: i32) -> Result<i32>;
fn read(&mut self, fh: u64, offset: i64, size: u32) -> Result<Vec<u8>>;
fn flush(&mut self, fh: u64) -> Result<()>;
fn write(&mut self, fh: u64, data: &[u8]) -> Result<usize>;
fn truncate(&mut self, fh: Option<u64>, size: u64) -> Result<()>;
fn close(&mut self, fh: u64) -> Result<()>;
fn rename(&mut self, new_path: &Path);
fn attrs(&self, uid: u32, gid: u32) -> Result<FileAttr>;
}

View File

@@ -0,0 +1,316 @@
use std::collections::{HashMap, HashSet};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::io::AsRawFd;
use std::path::Path;
use chacha20poly1305_ietf::{Key, Nonce};
use crypt4gh::header::DecryptedHeaderPackets;
use crypt4gh::{Keys, SEGMENT_SIZE};
use itertools::Itertools;
use sodiumoxide::crypto::aead::chacha20poly1305_ietf;
use sodiumoxide::randombytes::randombytes;
use crate::egafile::EgaFile;
use crate::error::{Crypt4GHFSError, Result};
use crate::utils;
const CIPHER_SEGMENT_SIZE: usize = SEGMENT_SIZE + 28;
pub struct EncryptedFile {
opened_files: HashMap<u64, Box<File>>,
path: Box<Path>,
session_key: [u8; 32],
keys: Vec<Keys>,
recipient_keys: HashSet<Keys>,
write_buffer: Vec<u8>,
only_read: bool,
// Optimization
buffer: HashMap<u64, EncryptionBuffer>,
session_keys: Vec<Vec<u8>>,
header_len: u64,
}
#[derive(Debug, Default)]
struct EncryptionBuffer {
data: Vec<u8>,
pos: usize,
valid: bool,
}
impl EgaFile for EncryptedFile {
fn fh(&self) -> Vec<u64> {
self.opened_files.iter().map(|(&fh, _)| fh).collect()
}
fn path(&self) -> Box<std::path::Path> {
self.path.clone()
}
fn open(&mut self, flags: i32) -> Result<i32> {
// Get path
let mut path_str = self.path().to_string_lossy().to_string();
path_str.push_str(".c4gh");
let path = Path::new(&path_str);
// Open file
let mut file = utils::open(path, flags)?;
let fh = file.as_raw_fd();
// Buffer
self.buffer.insert(fh as u64, EncryptionBuffer::default());
let (keys, header_length) = self.read_header(&mut file).unwrap();
self.session_keys = keys;
self.header_len = u64::from(header_length);
// Add to opened files
self.opened_files.insert(fh as u64, Box::new(file));
// Return
Ok(fh)
}
fn read(&mut self, fh: u64, offset: i64, size: u32) -> Result<Vec<u8>> {
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
let first_segment = offset as usize / SEGMENT_SIZE;
let mut off = offset as usize % SEGMENT_SIZE;
let length = off + size as usize;
let mut nsegments = length / SEGMENT_SIZE;
if length % SEGMENT_SIZE != 0 {
nsegments += 1;
}
let start_pos = first_segment * SEGMENT_SIZE;
log::debug!("first_segment: {}", first_segment);
log::debug!("off: {}", off);
log::debug!("length: {}", length);
log::debug!("length % SEGMENT_SIZE != 0: {}", length % SEGMENT_SIZE != 0);
log::debug!("nsegments: {}", nsegments);
log::debug!("start_pos: {}", start_pos);
let buf = self.buffer.get(&fh).expect("No buffer");
if buf.valid && buf.pos <= start_pos && ((start_pos - buf.pos) + length) <= buf.data.len() {
log::debug!("Already have decrypted enough data");
off += start_pos - buf.pos;
Ok(buf.data[off..off + size as usize].into())
}
else {
f.seek(SeekFrom::Start(
self.header_len + first_segment as u64 * CIPHER_SEGMENT_SIZE as u64,
))
.unwrap();
let mut output = Vec::new();
for _ in 0..nsegments {
let mut chunk = Vec::with_capacity(CIPHER_SEGMENT_SIZE);
let n = f.take(CIPHER_SEGMENT_SIZE as u64).read_to_end(&mut chunk).unwrap();
if n == 0 {
break;
}
let segment = Self::decrypt_block(&chunk, &self.session_keys);
output.extend_from_slice(&segment);
if n < CIPHER_SEGMENT_SIZE {
break;
}
}
log::debug!("Output: {}", output.len());
Ok(output[off..(off + size as usize).min(output.len())].into())
}
}
fn flush(&mut self, fh: u64) -> Result<()> {
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
if !self.write_buffer.is_empty() {
log::info!("Writing PARTIAL segment");
let nonce = Nonce::from_slice(&randombytes(12)).expect("Unable to create nonce from randombytes");
let key = Key::from_slice(&self.session_key).expect("Unable to create key from session_key");
let encrypted_segment = crypt4gh::encrypt_segment(&self.write_buffer, nonce, &key);
f.write_all(&encrypted_segment)?;
self.write_buffer.clear();
}
f.flush()?;
Ok(())
}
fn write(&mut self, fh: u64, data: &[u8]) -> Result<usize> {
// Write header
if self.only_read {
// Build header
log::debug!("Writing HEADER");
let header_bytes = crypt4gh::encrypt_header(&self.recipient_keys, &Some(self.session_key))
.map_err(|e| Crypt4GHFSError::Crypt4GHError(e.to_string()))?;
log::debug!("Header size = {}", header_bytes.len());
// Write header
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
f.write_all(&header_bytes)?;
// Update status
self.only_read = false;
}
log::debug!(
"write_buffer.len() = {}, data.len() = {}",
self.write_buffer.len(),
data.len()
);
// Chain write buffer with data
let last_segment = self.write_buffer.clone();
let write_data = last_segment.into_iter().chain(data.iter().copied());
let mut new_last_segment = Vec::new();
for segment in &write_data.chunks(SEGMENT_SIZE) {
// Collect segment
let segment_slice = segment.collect::<Vec<_>>();
log::debug!("segment_slice.len() = {}", segment_slice.len());
// This is the last segment, add to the struct
if segment_slice.len() < SEGMENT_SIZE {
log::info!("Storing PARTIAL segment");
new_last_segment = segment_slice;
}
else {
log::info!("Writing FULL segment");
// Full segment, write to the file
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
// Build encrypted segment
let nonce = Nonce::from_slice(&randombytes(12)).expect("Unable to create nonce from randombytes");
let key = Key::from_slice(&self.session_key).expect("Unable to create key from session_key");
let encrypted_segment = crypt4gh::encrypt_segment(&segment_slice, nonce, &key);
// Write segment
f.write_all(&encrypted_segment)?;
}
}
// Replace segment buffer
self.write_buffer = new_last_segment;
// Return
Ok(data.len())
}
fn truncate(&mut self, fh: Option<u64>, size: u64) -> Result<()> {
log::debug!("Truncate: size = {}", size);
self.opened_files
.iter_mut()
.filter(|(&ffh, _)| fh.is_none() || fh == Some(ffh))
.try_for_each(|(_, f)| f.set_len(size))?;
Ok(())
}
fn close(&mut self, fh: u64) -> Result<()> {
let f = self.opened_files.get(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
assert_eq!(f.as_raw_fd(), fh as i32);
self.opened_files.remove(&fh);
self.write_buffer.clear();
Ok(())
}
fn rename(&mut self, new_path: &Path) {
self.path = new_path.into();
}
fn attrs(&self, uid: u32, gid: u32) -> Result<fuser::FileAttr> {
let mut path_str = self.path.display().to_string();
path_str.push_str(".c4gh");
let stat = utils::lstat(Path::new(&path_str))?;
Ok(utils::stat_to_fileatr(stat, uid, gid))
}
}
impl EncryptedFile {
pub fn new(file: Option<Box<File>>, path: Box<Path>, keys: &[Keys], recipient_keys: &HashSet<Keys>) -> Self {
// Build session_key
let mut session_key = [0_u8; 32];
sodiumoxide::randombytes::randombytes_into(&mut session_key);
// Build open files
let mut opened_files = HashMap::new();
if let Some(f) = file {
opened_files.insert(f.as_raw_fd() as u64, f);
}
Self {
opened_files,
path,
session_key,
keys: keys.to_vec(),
recipient_keys: recipient_keys.clone(),
write_buffer: Vec::new(),
only_read: true,
buffer: HashMap::new(),
session_keys: Vec::new(),
header_len: 0,
}
}
fn read_header(&self, file: &mut File) -> Result<(Vec<Vec<u8>>, u32)> {
// Get header info
let mut header_length = 16;
let mut temp_buf = [0_u8; 16]; // Size of the header
file.read_exact(&mut temp_buf)?;
let header_info = crypt4gh::header::deconstruct_header_info(&temp_buf).unwrap();
// Calculate header packets
let encrypted_packets = (0..header_info.packets_count)
.map(|_| {
// Get length
let mut length_buffer = [0_u8; 4];
file.read_exact(&mut length_buffer).unwrap();
let length = u32::from_le_bytes(length_buffer);
header_length += length;
let length = length - 4;
log::debug!("Packet length: {}", length);
// Get data
let mut encrypted_data = vec![0_u8; length as usize];
file.read_exact(&mut encrypted_data).unwrap();
Ok(encrypted_data)
})
.collect::<Result<Vec<Vec<u8>>>>()?;
let DecryptedHeaderPackets {
data_enc_packets: session_keys,
edit_list_packet: edit_list_content,
} = crypt4gh::header::deconstruct_header_body(encrypted_packets, &self.keys, &None).unwrap();
assert!(edit_list_content.is_none());
Ok((session_keys, header_length))
}
fn decrypt_block(ciphersegment: &[u8], session_keys: &[Vec<u8>]) -> Vec<u8> {
let (nonce_slice, data) = ciphersegment.split_at(12);
let nonce = chacha20poly1305_ietf::Nonce::from_slice(nonce_slice).unwrap();
log::debug!("Nonce slice: {:02x?}", nonce_slice.iter().format(""));
log::debug!("Data len = {}", data.len());
for key in session_keys {
log::debug!("Session keys: {:02x?}", key.iter().format(""));
}
session_keys
.iter()
.find_map(|key| {
chacha20poly1305_ietf::Key::from_slice(key)
.and_then(|key| chacha20poly1305_ietf::open(data, None, &nonce, &key).ok())
})
.unwrap()
}
}

View File

@@ -0,0 +1,55 @@
use std::io;
use std::path::PathBuf;
use nix::errno::Errno;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Crypt4GHFSError>;
const DEFAULT_LIBC_ERROR: Errno = Errno::ECANCELED;
#[derive(Error, Debug)]
pub enum Crypt4GHFSError {
#[error("Path does not exist (path: {0})")]
PathDoesNotExist(PathBuf),
#[error("Mounting process failed (ERROR = {0})")]
MountError(String),
#[error("Fork failed")]
ForkFailed,
#[error("Secret key not found (path: {0})")]
SecretNotFound(PathBuf),
#[error("Error reading config (ERROR = {0})")]
BadConfig(String),
#[error("Unable to extract secret key (ERROR = {0})")]
SecretKeyError(String),
#[error("Connection url bad format")]
BadConfigConnectionUrl,
#[error("Invalid checksum format")]
InvalidChecksumFormat,
#[error("No checksum found")]
NoChecksum,
#[error("Invalid connection_url scheme: {0}")]
InvalidScheme(String),
#[error("IO Error: {0}")]
IoError(#[from] io::Error),
#[error("CLI configuration failed (ERROR = {0})")]
ConfigError(#[from] clap::Error),
#[error("Config could not configure syslog (ERROR = {0})")]
SysLogError(#[from] syslog::Error),
#[error("Setting logger failed (ERROR = {0})")]
LogError(#[from] log::SetLoggerError),
#[error("File not opened")]
FileNotOpened,
#[error("Crypt4GH failed (ERROR = {0})")]
Crypt4GHError(String),
#[error("Libc failed (ERROR = {0})")]
LibcError(#[from] nix::Error),
}
impl Crypt4GHFSError {
pub fn to_raw_os_error(&self) -> i32 {
match self {
Self::IoError(io_error) => io_error.raw_os_error().unwrap_or(DEFAULT_LIBC_ERROR as i32),
_ => DEFAULT_LIBC_ERROR as i32,
}
}
}

View File

@@ -0,0 +1,70 @@
use std::collections::BTreeMap;
use std::path::Path;
use crate::directory::Directory;
use crate::egafile::EgaFile;
type Inode = u64;
pub struct FileAdmin {
pub inode2file: BTreeMap<Inode, Box<dyn EgaFile>>,
path2inode: BTreeMap<Box<Path>, Inode>,
}
impl FileAdmin {
pub fn new(rootdir: &str) -> Self {
// Create file admin
let mut file_admin = Self {
inode2file: BTreeMap::new(),
path2inode: BTreeMap::new(),
};
// Add rootdir
file_admin.add(1, Box::new(Directory::new(None, Path::new(rootdir).into())));
// Return
file_admin
}
pub fn add(&mut self, ino: u64, file: Box<dyn EgaFile>) {
self.path2inode.insert(file.path(), ino);
self.inode2file.insert(ino, file);
}
pub fn get_by_path(&self, path: &Path) -> Option<&dyn EgaFile> {
self.path2inode.get(path).map(|ino| self.get_file(*ino))
}
pub fn get_by_path_mut(&mut self, path: &Path) -> Option<&mut dyn EgaFile> {
self.path2inode
.get(path)
.copied()
.map(move |ino| self.get_file_mut(ino))
}
pub fn get_file(&self, ino: u64) -> &dyn EgaFile {
self.inode2file.get(&ino).expect("Unable to get file").as_ref()
}
pub fn get_file_mut(&mut self, ino: u64) -> &mut dyn EgaFile {
self.inode2file.get_mut(&ino).expect("Unable to get file").as_mut()
}
pub fn remove(&mut self, ino: u64) -> Option<Box<dyn EgaFile>> {
self.inode2file.remove(&ino)
}
pub fn remove_by_path(&mut self, path: &Path) -> Option<Box<dyn EgaFile>> {
// Find inode+
let mut inode = None;
for (ino, file) in &mut self.inode2file {
if file.path() == path.into() {
inode = Some(*ino);
break;
}
}
// Remove inode
self.remove(inode?)
}
}

View File

@@ -0,0 +1,458 @@
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs::DirEntry;
use std::os::unix::fs::DirEntryExt;
use std::time::{Duration, SystemTime};
use crypt4gh::Keys;
use fuser::{
Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty, ReplyEntry, ReplyOpen, ReplyStatfs,
ReplyWrite, Request, TimeOrNow,
};
use nix::errno::Errno;
use nix::unistd::{Gid, Uid};
use crate::file_admin::FileAdmin;
use crate::utils;
// use std::os::linux::fs::MetadataExt;
const TTL: Duration = Duration::from_secs(300);
pub struct Crypt4ghFS {
file_admin: FileAdmin,
keys: Vec<Keys>,
recipients: HashSet<Keys>,
uid: Uid,
gid: Gid,
entries: HashMap<u64, Vec<Result<DirEntry, std::io::Error>>>,
duration1: std::time::Duration,
// TODO: implement cache directories functionality
}
impl Crypt4ghFS {
pub fn new(rootdir: &str, seckey: Vec<u8>, recipients: HashSet<Keys>, uid: Uid, gid: Gid) -> Self {
Self {
file_admin: FileAdmin::new(rootdir),
keys: vec![Keys {
method: 0,
privkey: seckey,
recipient_pubkey: vec![],
}],
recipients,
uid,
gid,
entries: HashMap::new(),
duration1: Duration::from_secs(0),
}
}
}
impl Filesystem for Crypt4ghFS {
// FILESYSTEM
fn destroy(&mut self) {
log::info!("1 - Elapsed: {:?}", self.duration1);
}
fn getattr(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyAttr) {
let file = self.file_admin.get_file(ino);
match file.attrs(self.uid.as_raw(), self.gid.as_raw()) {
Ok(attrs) => reply.attr(&TTL, &attrs),
Err(_) => reply.error(1000),
}
}
fn setattr(
&mut self,
req: &Request<'_>,
ino: u64,
mode: Option<u32>,
uid: Option<u32>,
gid: Option<u32>,
size: Option<u64>,
atime: Option<TimeOrNow>,
mtime: Option<TimeOrNow>,
_ctime: Option<SystemTime>,
fh: Option<u64>,
crtime: Option<SystemTime>,
chgtime: Option<SystemTime>,
bkuptime: Option<SystemTime>,
flags: Option<u32>,
reply: ReplyAttr,
) {
let mut err = None;
if mode.is_some() {
reply.error(Errno::EPERM as i32);
return;
}
if uid.is_some() || gid.is_some() {
reply.error(Errno::EPERM as i32);
return;
}
if atime.is_some() || mtime.is_some() {
reply.error(Errno::EOPNOTSUPP as i32);
return;
}
if crtime.is_some() || chgtime.is_some() || bkuptime.is_some() || flags.is_some() {
reply.error(Errno::EOPNOTSUPP as i32);
return;
}
let file = self.file_admin.get_file_mut(ino);
if let Some(size) = size {
if let Err(e) = file.truncate(fh, size) {
err = Some(e);
}
}
match err {
None => match file.attrs(req.uid(), req.gid()) {
Ok(attrs) => reply.attr(&TTL, &attrs),
Err(e) => reply.error(e.to_raw_os_error()),
},
Some(e) => reply.error(e.to_raw_os_error()),
}
}
fn lookup(&mut self, req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEntry) {
let parent_file = self.file_admin.get_file(parent);
let name_string = name.to_string_lossy().to_string();
let new_name = name_string.strip_suffix(".c4gh").unwrap_or(&name_string);
let path = parent_file.path().join(new_name);
match self.file_admin.get_by_path(path.as_path()) {
Some(child_file) => match child_file.attrs(req.uid(), req.gid()) {
Ok(attr) => reply.entry(&TTL, &attr, 0),
Err(e) => reply.error(e.to_raw_os_error()),
},
None => {
reply.error(Errno::ENOENT as i32);
},
}
}
fn statfs(&mut self, _req: &Request<'_>, ino: u64, reply: ReplyStatfs) {
let file = self.file_admin.get_file(ino);
match utils::statfs(&file.path()) {
Ok(statfs) => reply.statfs(
u64::from(statfs.blocks()),
u64::from(statfs.blocks_free()),
u64::from(statfs.blocks_available()),
u64::from(statfs.files()),
u64::from(statfs.files_free()),
statfs.block_size() as u32,
statfs.name_max() as u32,
statfs.fragment_size() as u32,
),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
// FILE
fn open(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
let file = self.file_admin.get_file_mut(ino);
match file.open(flags) {
Ok(fh) => reply.opened(fh as u64, flags as u32),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
fn read(
&mut self,
_req: &Request<'_>,
ino: u64,
fh: u64,
offset: i64,
size: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyData,
) {
let file = self.file_admin.get_file_mut(ino);
match file.read(fh, offset, size) {
Ok(data) => reply.data(&data),
Err(e) => {
log::error!("{:?}", e);
reply.error(e.to_raw_os_error());
},
}
}
fn flush(&mut self, _req: &Request<'_>, ino: u64, fh: u64, _lock_owner: u64, reply: ReplyEmpty) {
let file = self.file_admin.get_file_mut(ino);
match file.flush(fh) {
Ok(_) => reply.ok(),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
fn create(
&mut self,
req: &Request<'_>,
parent: u64,
name: &OsStr,
mode: u32,
umask: u32,
flags: i32,
reply: ReplyCreate,
) {
// Get path
let parent_file = self.file_admin.get_file(parent);
let parent_path = parent_file.path();
let path = parent_path.join(name).into_boxed_path();
let mut inbox_path = path.to_path_buf();
if path.extension().is_none() || path.extension().unwrap() != "c4gh" {
let mut filename = inbox_path.file_name().unwrap().to_string_lossy().to_string();
filename.push_str(".c4gh");
inbox_path.set_file_name(filename);
}
// Create file
log::debug!("Create new file with path: {:?}", inbox_path);
let file = utils::create(&inbox_path, flags, mode & umask).unwrap();
// Build file admin entry
let ino = utils::lstat(&inbox_path).unwrap().st_ino;
let egafile = utils::wrap_file(&path, file, &self.keys, &self.recipients);
// Build reply
let attrs = egafile.attrs(req.uid(), req.gid()).unwrap();
let fh = *egafile.fh().last().unwrap();
// Add and reply
self.file_admin.add(ino, egafile);
reply.created(&TTL, &attrs, 0, fh, flags as u32);
}
fn write(
&mut self,
_req: &Request<'_>,
ino: u64,
fh: u64,
_offset: i64,
data: &[u8],
_write_flags: u32,
_flags: i32,
_lock_owner: Option<u64>,
reply: ReplyWrite,
) {
let file = self.file_admin.get_file_mut(ino);
// TODO: Warn if offset != 0 => not allowed
match file.write(fh, data) {
Ok(size) => reply.written(size as u32),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
fn release(
&mut self,
_req: &Request<'_>,
ino: u64,
fh: u64,
_flags: i32,
_lock_owner: Option<u64>,
_flush: bool,
reply: ReplyEmpty,
) {
let file = self.file_admin.get_file_mut(ino);
file.close(fh).unwrap();
reply.ok();
}
fn rename(
&mut self,
_req: &Request<'_>,
parent: u64,
name: &OsStr,
newparent: u64,
newname: &OsStr,
_flags: u32,
reply: ReplyEmpty,
) {
// Build paths
let old_parent_file = self.file_admin.get_file(parent).path();
let new_parent_path = self.file_admin.get_file(newparent).path();
let old_path = old_parent_file.join(name);
let new_path = new_parent_path.join(newname);
// Change paths
let file = self.file_admin.get_by_path_mut(&old_path).unwrap();
file.rename(new_path.as_path());
// Rename
match std::fs::rename(&old_path, new_path) {
Ok(_) => reply.ok(),
Err(e) => reply.error(e.raw_os_error().unwrap()),
}
}
fn unlink(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
// Build paths
let parent_file = self.file_admin.get_file(parent);
let parent_path = parent_file.path();
let path = parent_path.join(name);
// Remove file
match std::fs::remove_file(path) {
Ok(_) => reply.ok(),
Err(e) => reply.error(e.raw_os_error().unwrap()),
}
}
// DIRECTORY
fn readdir(&mut self, _req: &Request<'_>, ino: u64, _fh: u64, skipped: i64, mut reply: ReplyDirectory) {
let _dir = self.file_admin.get_file(ino);
let entries = self.entries.get_mut(&ino).unwrap();
// TODO: Make sure that they have always the same order
let mut last_error = None;
for (offset, entry) in entries[skipped as usize..].iter().enumerate() {
log::debug!("Entry {} - {:?}", offset, entry);
match entry {
Ok(dir_entry) => {
// Track file
let egafile = utils::wrap_path(dir_entry.path().as_path(), &self.keys, &self.recipients);
self.file_admin.add(dir_entry.ino(), egafile);
// Kind
let kind = utils::get_type(dir_entry);
// Name
let name = dir_entry.file_name().clone();
let name_str = name.to_string_lossy().to_string();
let new_name = name_str.strip_suffix(".c4gh").unwrap_or(&name_str);
// Add entry
let buffer_full = reply.add(dir_entry.ino(), (skipped + offset as i64 + 1) as i64, kind, new_name);
if buffer_full {
break;
}
},
Err(e) => {
log::error!("Error on entry {} (ERROR = {:?})", offset, e);
last_error = Some(e.raw_os_error().expect("Unable to convert to error"));
},
}
}
match last_error {
None => reply.ok(),
Some(e) => reply.error(e),
}
}
// fn readdirplus(&mut self, req: &Request<'_>, ino: u64, _fh: u64, offset: i64, mut reply: ReplyDirectoryPlus) {
// let _dir = self.file_admin.get_file(ino);
// // TODO: Make sure that they have always the same order
// let entries = self.entries.get_mut(&ino).unwrap();
// let mut last_error = None;
// for (offset, entry) in entries.enumerate().skip(offset as usize) {
// match entry {
// Ok(dir_entry) => {
// let egafile = utils::wrap_path(dir_entry.path().as_path(), &self.keys, &self.recipients);
// self.file_admin.add(dir_entry.ino(), egafile);
// let kind = utils::get_type(&dir_entry);
// let name = dir_entry.file_name().clone();
// let metadata = dir_entry.metadata().unwrap();
// let attrs = FileAttr {
// ino: dir_entry.ino(),
// size: dir_entry.metadata().unwrap().len(),
// blocks: metadata.blocks(),
// atime: metadata.accessed().unwrap_or(std::time::UNIX_EPOCH),
// mtime: metadata.modified().unwrap_or(std::time::UNIX_EPOCH),
// ctime: metadata.created().unwrap_or(std::time::UNIX_EPOCH),
// crtime: std::time::UNIX_EPOCH,
// kind,
// perm: metadata.permissions().mode() as u16,
// #[cfg(target_os = "linux")]
// nlink: metadata.st_nlink() as u32,
// #[cfg(target_os = "macos")]
// nlink: 0,
// uid: req.uid(),
// gid: req.gid(),
// #[cfg(target_os = "linux")]
// rdev: metadata.st_rdev() as u32,
// #[cfg(target_os = "macos")]
// rdev: metadata.rdev() as u32,
// #[cfg(target_os = "linux")]
// blksize: metadata.st_blksize() as u32,
// #[cfg(target_os = "macos")]
// blksize: metadata.blksize() as u32,
// padding: 0,
// flags: 0,
// };
// let buffer_full = reply.add(dir_entry.ino(), (offset + 1) as i64, name, &TTL, &attrs, 0);
// if buffer_full {
// break;
// }
// },
// Err(e) => {
// last_error = Some(e.raw_os_error().expect("Unable to convert to error"));
// },
// }
// }
// match last_error {
// None => reply.ok(),
// Some(e) => reply.error(e),
// }
// }
fn mkdir(&mut self, req: &Request<'_>, parent: u64, name: &OsStr, _mode: u32, _umask: u32, reply: ReplyEntry) {
let parent_file = self.file_admin.get_file(parent);
let parent_path = parent_file.path();
let path = parent_path.join(name);
match std::fs::create_dir(&path) {
Ok(_) => {
let stat = utils::lstat(&path).unwrap();
let attrs = utils::stat_to_fileatr(stat, req.uid(), req.gid());
reply.entry(&TTL, &attrs, 0);
},
Err(e) => reply.error(e.raw_os_error().expect("Unable to retrieve raw OS error")),
}
}
fn rmdir(&mut self, _req: &Request<'_>, parent: u64, name: &OsStr, reply: ReplyEmpty) {
let parent_file = self.file_admin.get_file(parent);
let parent_path = parent_file.path();
let path = parent_path.join(name);
self.file_admin.remove_by_path(&path);
match std::fs::remove_dir(path) {
Ok(_) => reply.ok(),
Err(e) => reply.error(e.raw_os_error().expect("Unable to retrieve raw OS error")),
}
}
fn opendir(&mut self, _req: &Request<'_>, ino: u64, flags: i32, reply: ReplyOpen) {
let file = self.file_admin.get_file_mut(ino);
self.entries.insert(
ino,
std::fs::read_dir(file.path())
.expect("Unable to read directory")
.collect(),
);
match file.open(flags) {
Ok(fh) => reply.opened(fh as u64, flags as u32),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
fn releasedir(&mut self, _req: &Request<'_>, ino: u64, fh: u64, _flags: i32, reply: ReplyEmpty) {
let file = self.file_admin.get_file_mut(ino);
self.entries.remove(&ino);
match file.close(fh) {
Ok(_) => reply.ok(),
Err(e) => reply.error(e.to_raw_os_error()),
}
}
}

View File

@@ -0,0 +1,63 @@
#![allow(
clippy::upper_case_acronyms,
clippy::missing_panics_doc,
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::module_name_repetitions,
clippy::cast_sign_loss,
clippy::cast_possible_truncation,
clippy::similar_names
)]
use std::path::{Path, PathBuf};
use config::Config;
use error::Crypt4GHFSError;
pub mod config;
mod directory;
mod egafile;
mod encrypted_file;
pub mod error;
mod file_admin;
mod filesystem;
mod regular_file;
mod utils;
pub fn run_with_config(conf: &Config, mountpoint: PathBuf) -> Result<(), Crypt4GHFSError> {
// Set log level and logger
conf.setup_logger()?;
let rootdir = conf.get_rootdir();
if !Path::new(&rootdir).exists() {
return Err(Crypt4GHFSError::PathDoesNotExist(Path::new(&rootdir).into()));
}
// Encryption / Decryption keys
let seckey = (conf.get_secret_key()?).map_or_else(
|| {
log::warn!("No seckey specified");
vec![0_u8; 32]
},
|key| key,
);
let recipients = conf.get_recipients(&seckey);
// Get options
let options = conf.get_options();
let fs = filesystem::Crypt4ghFS::new(
&rootdir,
seckey,
recipients,
nix::unistd::getuid(),
nix::unistd::getgid(),
);
if !mountpoint.exists() {
return Err(Crypt4GHFSError::PathDoesNotExist(mountpoint));
}
fuser::mount2(fs, &mountpoint, &options).map_err(|e| Crypt4GHFSError::MountError(e.to_string()))
}

View File

@@ -0,0 +1,35 @@
use std::fs::File;
use clap::StructOpt;
use crypt4ghfs::error::Crypt4GHFSError;
use crypt4ghfs::{config, run_with_config};
use crate::cli::Args;
mod cli;
fn run() -> Result<(), Crypt4GHFSError> {
// Init CLI
let matches = Args::parse();
let mountpoint = matches.mountpoint;
// Read config
let config_path = matches.conf;
log::info!("Loading config: {:?}", config_path);
let config_file = File::open(config_path)?;
let conf = config::Config::from_file(config_file)?;
log::debug!("Config = {:?}", conf);
// Run
run_with_config(&conf, mountpoint)
}
fn main() {
if let Err(err) = run() {
let _ = pretty_env_logger::try_init();
log::error!("{}", err);
std::process::exit(1);
}
}

View File

@@ -0,0 +1,99 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::os::unix::io::AsRawFd;
use std::path::Path;
use crate::egafile::EgaFile;
use crate::error::{Crypt4GHFSError, Result};
use crate::utils;
pub struct RegularFile {
pub opened_files: HashMap<u64, Box<File>>,
pub path: Box<Path>,
pub only_read: bool,
}
impl EgaFile for RegularFile {
fn fh(&self) -> Vec<u64> {
self.opened_files.iter().map(|(&fh, _)| fh).collect()
}
fn path(&self) -> Box<Path> {
self.path.clone()
}
fn open(&mut self, flags: i32) -> Result<i32> {
let path = self.path();
let file = utils::open(&path, flags)?;
let fh = file.as_raw_fd();
self.opened_files.insert(fh as u64, Box::new(file));
Ok(fh)
}
fn read(&mut self, fh: u64, offset: i64, size: u32) -> Result<Vec<u8>> {
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
let mut data = Vec::new();
f.seek(SeekFrom::Start(offset as u64))?;
f.as_ref().take(u64::from(size)).read_to_end(&mut data)?;
Ok(data)
}
fn flush(&mut self, fh: u64) -> Result<()> {
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
f.flush()?;
Ok(())
}
fn write(&mut self, fh: u64, data: &[u8]) -> Result<usize> {
self.only_read = false;
let f = self.opened_files.get_mut(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
// Write data
f.write_all(data)?;
Ok(data.len())
}
fn truncate(&mut self, fh: Option<u64>, size: u64) -> Result<()> {
log::debug!("Truncate: size = {}", size);
self.opened_files
.iter_mut()
.filter(|(&ffh, _)| fh.is_none() || fh == Some(ffh))
.try_for_each(|(_, f)| f.set_len(size))?;
Ok(())
}
fn close(&mut self, fh: u64) -> Result<()> {
let f = self.opened_files.get(&fh).ok_or(Crypt4GHFSError::FileNotOpened)?;
assert_eq!(f.as_raw_fd(), fh as i32);
self.opened_files.remove(&fh);
self.only_read = true;
Ok(())
}
fn rename(&mut self, new_path: &Path) {
self.path = new_path.into();
}
fn attrs(&self, uid: u32, gid: u32) -> Result<fuser::FileAttr> {
let stat = utils::lstat(&self.path)?;
Ok(utils::stat_to_fileatr(stat, uid, gid))
}
}
impl RegularFile {
pub fn new(file: Option<Box<File>>, path: Box<Path>) -> Self {
// Build open files
let mut opened_files = HashMap::new();
if let Some(f) = file {
opened_files.insert(f.as_raw_fd() as u64, f);
}
// Build RegularFile object
Self {
opened_files,
path,
only_read: true,
}
}
}

View File

@@ -0,0 +1,149 @@
use std::collections::HashSet;
use std::fs::{DirEntry, File, OpenOptions};
use std::io;
use std::os::unix::fs::OpenOptionsExt;
use std::path::Path;
use std::time::{Duration, SystemTime};
use crypt4gh::Keys;
use fuser::{FileAttr, FileType};
use nix::fcntl::OFlag;
use nix::sys::stat::{FileStat, Mode, SFlag};
use nix::sys::statvfs::Statvfs;
use crate::directory::Directory;
use crate::egafile::EgaFile;
use crate::encrypted_file::EncryptedFile;
use crate::error::Result;
use crate::regular_file::RegularFile;
pub fn lstat(path: &Path) -> Result<FileStat> {
let stat = nix::sys::stat::lstat(path)?;
Ok(stat)
}
pub fn stat_to_fileatr(stat: FileStat, uid: u32, gid: u32) -> FileAttr {
let mut perm = Mode::from_bits_truncate(stat.st_mode);
perm.set(Mode::S_IRWXG, false);
perm.set(Mode::S_IRWXO, false);
let kind = match SFlag::from_bits_truncate(stat.st_mode) & SFlag::S_IFMT {
SFlag::S_IFDIR => FileType::Directory,
SFlag::S_IFREG => FileType::RegularFile,
SFlag::S_IFLNK => FileType::Symlink,
SFlag::S_IFBLK => FileType::BlockDevice,
SFlag::S_IFCHR => FileType::CharDevice,
SFlag::S_IFIFO => FileType::NamedPipe,
SFlag::S_IFSOCK => FileType::Socket,
_ => panic!("Unknown file type"),
};
FileAttr {
ino: stat.st_ino,
size: stat.st_size as u64,
blocks: stat.st_blocks as u64,
atime: SystemTime::UNIX_EPOCH
+ Duration::from_secs(stat.st_atime as u64)
+ Duration::from_nanos(stat.st_atime_nsec as u64),
mtime: SystemTime::UNIX_EPOCH
+ Duration::from_secs(stat.st_mtime as u64)
+ Duration::from_nanos(stat.st_mtime_nsec as u64),
ctime: SystemTime::UNIX_EPOCH
+ Duration::from_secs(stat.st_ctime as u64)
+ Duration::from_nanos(stat.st_ctime_nsec as u64),
crtime: SystemTime::UNIX_EPOCH, // TODO: Is this one okay?
kind,
perm: perm.bits() as u16,
#[cfg(target_os = "macos")]
nlink: u32::from(stat.st_nlink),
#[cfg(target_os = "linux")]
nlink: stat.st_nlink as u32,
uid,
gid,
rdev: stat.st_rdev as u32,
blksize: stat.st_blksize as u32,
flags: 0,
}
}
pub fn get_type(entry: &DirEntry) -> fuser::FileType {
let kind = entry.file_type().expect("Unable to get file type");
if kind.is_file() {
fuser::FileType::RegularFile
}
else if kind.is_dir() {
fuser::FileType::Directory
}
else if kind.is_symlink() {
fuser::FileType::Symlink
}
else {
panic!("Unknown file type");
}
}
pub fn open(path: &Path, flags: i32) -> io::Result<File> {
let open_flags = OFlag::from_bits_truncate(flags);
OpenOptions::new()
.custom_flags(flags)
.read(open_flags.contains(OFlag::O_RDONLY) || open_flags.contains(OFlag::O_RDWR))
.write(open_flags.contains(OFlag::O_WRONLY) || open_flags.contains(OFlag::O_RDWR))
.open(path)
}
pub fn create(path: &Path, flags: i32, _mode: u32) -> io::Result<File> {
let open_flags = OFlag::from_bits_truncate(flags);
OpenOptions::new()
.custom_flags(flags)
//.mode(mode)
.read(open_flags.contains(OFlag::O_RDONLY) || open_flags.contains(OFlag::O_RDWR))
.write(open_flags.contains(OFlag::O_WRONLY) || open_flags.contains(OFlag::O_RDWR))
.open(path)
}
pub fn wrap_file(path: &Path, file: File, keys: &[Keys], recipient_keys: &HashSet<Keys>) -> Box<dyn EgaFile> {
wrapper(path.into(), Some(Box::new(file)), keys, recipient_keys)
}
pub fn wrap_path(path: &Path, keys: &[Keys], recipient_keys: &HashSet<Keys>) -> Box<dyn EgaFile> {
wrapper(path.into(), None, keys, recipient_keys)
}
fn wrapper(
path: Box<Path>,
file: Option<Box<File>>,
keys: &[Keys],
recipient_keys: &HashSet<Keys>,
) -> Box<dyn EgaFile> {
match path.extension() {
Some(ext) if ext != "c4gh" => Box::new(RegularFile::new(file, path)),
Some(_) => {
let mut inbox_path = path.to_path_buf();
let mut filename = inbox_path.file_name().unwrap().to_string_lossy().to_string();
filename = filename.strip_suffix(".c4gh").unwrap().to_string();
inbox_path.set_file_name(filename);
Box::new(EncryptedFile::new(
file,
inbox_path.into_boxed_path(),
keys,
recipient_keys,
))
},
None => {
if path.is_file() {
Box::new(EncryptedFile::new(file, path, keys, recipient_keys))
}
else if path.is_dir() {
Box::new(Directory::new(file, path))
}
else {
panic!("Unknown file: {:?}", path)
}
},
}
}
pub fn statfs(path: &Path) -> Result<Statvfs> {
let statvfs = nix::sys::statvfs::statvfs(path)?;
Ok(statvfs)
}